本文将介绍一款仿“京东商城”商品信息展示的电商类App。该案例是基于 Vue2.0 + Vue Router + webpack + ES6
等技术栈实现的一款App,很适合初学者进行学习。
项目源码在文章末尾
1 项目概述
项目是一款仿“京东商城”的商品信息展示的App,主要实现了以下功能。
- 商城首页轮播效果,热销商品展示,公共底部导航。
- 搜索页面搜索关键词智能提示,保存搜索记录。
- 商品一级分类与二级分类导航的展示。
- 商品列表页面的商品展示。
- 点击商品添加购物车。
- 购物车页面的商品购买数量加减,统计购买商品的总价。
1.1 开发环境
首先需要安装Node.js 12以上的版本,因为Node.js中已经继承了NPM,所以无需在单独安装NPM。然后再安装Vue脚手架(Vue-CLI)以及创建项目。
项目的调试使用Google Chrome浏览器的控制台进行,在浏览器中按下F12键,然后单击“切换设备工具栏”,进入移动端的调试界面,可以选择相应的设备进行调试,效果如图1 所示。
图 1 项目调试效果
1.2 项目结构
项目结构如图2所示,其中src文件夹是项目的源文件目录,src文件夹下的项目结构如图3所示。
图2 项目结构
图3 src文件夹
项目结构中主要文件说明如下。
- dist:项目打包后的静态文件存放目录。
- node_modules:项目依赖管理目录。
- public:项目的静态文件存放目录,也是本地服务器的根目录。
- src:项目源文件存放目录。
- package.json:项目npm配置文件。
src文件夹目录说明如下。
- assets:静态资源文件存放目。
- components:公共组件存放目录。
- router:路由配置文件存放目录。
- store:状态管理配置存放目录。
- views:视图组件存放目录。
- App.vue:项目的根组件。
- main.js:项目的入口文件。
项目的所有页面组件都存放在 src/views 目录下,该目录下的文件说明如下。
- Carts.vue:定义购物车页面的视图组件。
- Classify.vue:定义商品分类页面的视图组件。
- GoodsList.vue:定义商品列表页面的视图组件。
- Index.vue:定义项目的公共底部导航与三级路由的视图组件。
- Main.vue:定义商城的首页布局的视图组件。
- Myself.vue:定义“我的”页面的视图组件。
- Search.vue:定义搜索页面的视图组件。
2 入口文件
项目的入口文件有 index.html、main.js和App.vue三个文件,这些入口文件的具体内容介绍如下。
2.1 项目入口页面
index.html是项目默认的主渲染页面文件,主要用于Vue实例挂载点的声明与DOM渲染。代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<div id="app"></div>
</body>
</html>
2.2 程序入口文件
main.js是程序的入口文件,主要用于加载各种公共组件和初始化Vue实例。本项目中的路由设置和引用的Vant UI组件库就是在该文件中定义的。代码如下:
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import Vant from 'vant'
import 'vant/lib/index.css'
import axios from 'axios'
//引入Vant组件库
Vue.use(Vant);
Vue.config.productionTip = false
//引入axios模块
Vue.prototype.$axios = axios
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
2.3 组件入口文件
App.vue是项目的根组件,所有的页面都是在App.vue下面切换的,所有的页面组件都是App.vue的子组件。在App.vue组件内只需要使用 组件作为占位符,就可以实现各个页面的引入,代码如下:
<template>
<div>
<router-view></router-view>
</div>
</template>
3 项目组件
项目中所有页面组件都在views文件夹中定义,具体组件内容介绍如下。
3.1 底部导航组件
在京东商城APP中,底部导航使用的是 Vant UI组件库中的Tabbar 标签栏组件,组件引入代码如下:
import Vue from 'vue';
import { Tabbar, TabbarItem } from 'vant';
Vue.use(Tabbar);
Vue.use(TabbarItem);
公共底部导航组件的代码如下:
<van-tabbar v-model="active" route active-color="#F6230E" inactive-color="#8B8B8B">
<van-tabbar-item icon="home-o"
to='/main'>
首页
</van-tabbar-item>
<van-tabbar-item icon="apps-o"
to='/classify'>
分类
</van-tabbar-item>
<van-tabbar-item icon="shopping-cart-o"
to='/carts'
:badge="cartList.length">
购物车
</van-tabbar-item>
<van-tabbar-item icon="contact"
to='/myself'>
我的
</van-tabbar-item>
</van-tabbar>
运行效果如图4所示。
图4 底部导航效果图
3.2 商城首页
首页是项目的核心页面之一,在首页中主要展示商城的重要商品信息,首页中布局也是相对比较复杂的。京东商城APP首页效果如图5所示。
图5 商品首页效果图
在整个项目中,一级路由编写在App.vue根组件中,关于首页的二级路由则编写在Index.js组件中。
Index.vue文件代码如下:
<template>
<div>
<router-view></router-view>
<van-tabbar v-model="active"
route
active-color="#F6230E"
inactive-color="#8B8B8B">
<van-tabbar-item icon="home-o"
to='/main'>
首页
</van-tabbar-item>
<van-tabbar-item icon="apps-o"
to='/classify'>
分类
</van-tabbar-item>
<van-tabbar-item icon="shopping-cart-o"
to='/carts'
:badge="cartList.length">
购物车
</van-tabbar-item>
<van-tabbar-item icon="contact"
to='/myself'>
我的
</van-tabbar-item>
</van-tabbar>
</div>
</template>
<script>
export default {
data(){
return {
active: 0,
cartList: []
}
},
created(){
//获取购物车数据
let list = localStorage.cartList
if(list){
this.cartList = JSON.parse(list)
}
}
}
</script>
在上面代码中,点击底部导航的“首页”按钮,会将对应的首页视图组件Main.vue文件加载到组件中。
Main.vue文件代码如下:
<template>
<div>
<van-search placeholder="请输入搜索关键词"
shape="round"
background="#E43130"
show-action
@focus="searchFocus"
style="position: fixed;width:100%;top: 0;z-index: 999;"
>
<template #left>
<van-icon name="wap-nav"
color="#fff"
size="25px"
style="margin-right: 8px;" />
</template>
<template #label>
<span class="search-logo">JD</span>
</template>
<template #action>
<span style="font-size: 16px;color:#fff;">登录</span>
</template>
</van-search>
<!-- 轮播广告 -->
<div class="swiper-banner" style="margin-top: 70px">
<van-swipe class="my-swipe"
:autoplay="3000"
indicator-color="white">
<van-swipe-item v-for="url in imgs" :key="url">
<img :src="url" width="100%" >
</van-swipe-item>
</van-swipe>
</div>
<!-- 宫格 -->
<div class="swiper-banner">
<van-swipe class="my-swipe"
:autoplay="3000"
indicator-color="red"
:loop="false"
style="padding: 10px 0px" >
<van-swipe-item>
<van-grid :column-num="5" :border="false">
<van-grid-item v-for="(item,index) in navData"
:key="index"
:text="item.title"
v-show="index < 10">
<template #icon>
<img :src="item.imgurl" width="50px">
</template>
</van-grid-item>
</van-grid>
</van-swipe-item>
<van-swipe-item>
<van-grid :column-num="5" :border="false">
<van-grid-item v-for="(item,index) in navData"
:key="index"
:text="item.title"
v-show="index >=10 && index < 20">
<template #icon>
<img :src="item.imgurl" width="50px">
</template>
</van-grid-item>
</van-grid>
</van-swipe-item>
</van-swipe>
</div>
<!-- 商品推荐 -->
<img src="/imgs/tuijian.png" width="100%">
<div class="goods-list">
<goods-card v-for="i in 10"
:key="i"
img="/imgs/goods.jpg"
title="商品标题"
:price="10">
</goods-card>
</div>
</div>
</template>
<script>
import GoodsCard from '@/components/GoodsCard'
import imgurls from '@/assets/imgs.js'
export default {
components: {
'goods-card': GoodsCard
},
data(){
return {
imgs: [],
navData: []
}
},
created() {
this.$axios.get('/data/home.json').then(res=>{
this.imgs = res.data.bannerImgs;
this.navData = res.data.icons;
}).catch(err=>{
console.error(err)
})
},
methods: {
searchFocus(){
this.$router.push({
path: '/search'
})
}
}
}
</script>
<style scoped>
.search-logo{
font-size: 18px;
color: #EA4546;
font-weight: bold;
padding: 0px 10px;
border-right: 1px solid #E7E7E7;
}
.swiper-banner{
width: 90%;
margin: 10px auto;
border-radius: 10px;
overflow: hidden;
}
.goods-list{
display: flex;
justify-content: space-between;
box-sizing: border-box;
padding: 0px 10px;
flex-wrap: wrap;
margin-bottom: 80px;
}
</style>
在上面代码中,轮播图和宫格使用了本地JSON数据,通过 axios 获取本地数据,本地JSON文件代码如下:
/public/data/home.json文件内容如下:
{
"bannerImgs": [
"/imgs/banner01.jpg",
"/imgs/banner02.jpg",
"/imgs/banner03.jpg"
],
"icons": [
{
"title": "京东超市",
"imgurl": "/imgs/icon-01.png"
},
{
"title": "数码电器",
"imgurl": "/imgs/icon-02.png"
},
{
"title": "京东服饰",
"imgurl": "/imgs/icon-03.png"
},
{
"title": "京东生鲜",
"imgurl": "/imgs/icon-04.png"
},
{
"title": "京东到家",
"imgurl": "/imgs/icon-05.png"
},
{
"title": "充值缴费",
"imgurl": "/imgs/icon-06.png"
},
{
"title": "9.9元拼",
"imgurl": "/imgs/icon-07.png"
},
{
"title": "领券",
"imgurl": "/imgs/icon-08.png"
},
{
"title": "领金贴",
"imgurl": "/imgs/icon-09.png"
},
{
"title": "PLUS会员",
"imgurl": "/imgs/icon-10.png"
},
{
"title": "京东国际",
"imgurl": "/imgs/icon-11.png"
},
{
"title": "京东拍卖",
"imgurl": "/imgs/icon-12.png"
},
{
"title": "唯品会",
"imgurl": "/imgs/icon-13.png"
},
{
"title": "玩3C",
"imgurl": "/imgs/icon-14.png"
},
{
"title": "沃尔玛",
"imgurl": "/imgs/icon-15.png"
},
{
"title": "美妆馆",
"imgurl": "/imgs/icon-16.png"
},
{
"title": "京东旅行",
"imgurl": "/imgs/icon-17.png"
},
{
"title": "拍拍二手",
"imgurl": "/imgs/icon-18.png"
},
{
"title": "物流查询",
"imgurl": "/imgs/icon-19.png"
},
{
"title": "全部",
"imgurl": "/imgs/icon-20.png"
}
]
}
3.3 搜索页面
搜索页面的功能稍微复杂一些,当在搜索框输入关键词时,需要有智能提示,然后点击搜索按钮,跳转到搜索结果页面。对于搜索的所有关键词,要保存到本地存储文件中,在搜索页面以“搜索记录”的形式进行展示,并实现对搜索记录删除的功能。搜索页面效果如图6所示,搜索智能提示效果如图7所示。
图6 搜索页面效果
图7 搜索智能提示效果
搜索框使用Vant UI的Search 搜索组件,在使用组件前要引入 Search 组件,代码如下:
import Vue from 'vue';
import { Search } from 'vant';
Vue.use(Search);
Search.vue组件代码如下:
<template>
<div>
<van-search v-model="searchValue"
placeholder="请输入搜索关键词"
shape="round"
background="#fff"
show-action
autofocus
@search="onSearch"
>
<template #left>
<van-icon @click="pageBack"
name="arrow-left"
size="25px"
style="margin-right: 8px;" />
</template>
<template #action>
<van-button color="#E93B3D"
size="small"
style="border-radius: 5px"
@click="onSearch">
搜索
</van-button>
</template>
</van-search>
<!-- 搜索记录 -->
<div class="search-history">
<div class="search-history-title">
<span>最近搜索</span>
<van-icon name="delete" @click="clearHistory" />
</div>
<div class="search-history-list">
<van-tag v-for="(item,index) in historyList"
:key="index"
color="#F0F2F5"
text-color="#68687F"
size="large"
style="margin:0px 10px 10px 0px;"
>
{{item}}
</van-tag>
</div>
</div>
<!-- 搜索智能提示区域 -->
<div class="kw-list" v-show="showKwList">
<van-cell v-for="kw in showList"
:key="kw"
:title="kw"
value="内容"
@click="onSearch(kw)" />
</div>
</div>
</template>
<script>
export default {
data(){
return {
searchValue: '', // 搜索的内容
showKwList: false ,//控制智能搜索区域的显示
historyList: [] ,//搜索记录列表
showList: [], //要展示的内容
data: [
"html",
"css",
"javascript",
"jquery",
"node.js",
"vue.js",
"swiper",
"bootstrap",
"php",
"mongodb",
"mysql",
"react.js",
"github",
"glup",
"webpack",
"sass",
"echarts",
"vant"
]
}
},
created(){ //初始化搜索记录
let historyList = localStorage.historyList
if(historyList){
this.historyList = JSON.parse(historyList)
}
},
watch: {
searchValue(kw){
this.showKwList = kw.length > 0 ? true : false
this.showList = this.data.filter(item=>{
return item.includes(kw)
})
}
},
methods: {
pageBack(){
//返回上一页
window.history.back()
},
onSearch(kw){ //搜索事件
let keyword = ''
if(typeof kw === 'string'){ //点击智能提示或回车搜索
keyword = kw
}else if(typeof kw === 'object') { //点击搜索按钮
if(this.searchValue.trim() == '') return
keyword = this.searchValue
}
//执行搜索功能
this.$router.push({
path: '/list',
query: {
kw: keyword
}
})
//保存搜索记录
this.saveSeachKw(keyword)
},
saveSeachKw(kw){ //保存搜索关键字
if(this.historyList.includes(kw)){
let index = this.historyList.indexOf(kw)
this.historyList.splice(index,1)
this.historyList.unshift(kw)
}else{
this.historyList.unshift(kw)
}
//将更新后的historyList同步到本地
localStorage.historyList = JSON.stringify(this.historyList)
},
clearHistory(){ //清空搜索记录
this.$dialog.confirm({
message: '确定要清空吗?',
closeOnClickOverlay: true,
confirmButtonText: '清空'
}).then(()=>{
this.historyList = []
localStorage.historyList = JSON.stringify(this.historyList)
}).catch(()=>{
})
}
}
}
</script>
<style scoped>
/* 搜索记录区域 */
.search-history{
border-top: 1px solid #F2F2F2;
}
.search-history-title{
height: 50px;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
padding: 0px 15px;
}
.search-history-list{
box-sizing: border-box;
padding: 0px 15px;
}
/* 智能搜索提示区域 */
.kw-list{
background: #fff;
position: absolute;
width: 100%;
top: 67px;
min-height: 150px;
}
</style>
3.4 分类导航页面
商品分类页面的头部和底部都是使用的公共UI组件,底部使用的是Vant UI组件库中的 Tabbar 标签栏组件,头部则使用的是 Search 搜索组件。在分类页面,主要实现一级商品分类导航和二级商品分类导航的布局与关联。分类页面的核心部分的效果如图8所示。
图8 分类导航页面效果
在分类页面中,所有的分类数据都是通过本地模拟出来的,然后使用axios模块异步加载本地服务器上的json数据。
Classify.vue 分类页面核心部分的代码如下:
<template>
<div class="nav-body">
<!-- 一级分类导航 -->
<ul class="navOne">
<li v-for="(item) in navs"
:key="item.cid"
@click="clickNavOne(item)">
<img :src="item.cpic" width="30px" height="30px">
{{item.cname}}
</li>
</ul>
<!-- 二级分类导航 -->
<ul class="sonNav">
<li v-for="(item) in sonNav"
:key="item.subcid"
@click="toPage">
<img :src="item.scpic" width="50px" height="50px">
{{item.subcname}}
</li>
</ul>
</div>
</template>
<script>
export default {
data(){
return {
navs: [], //所有的导航数据
sonNav: [] //二级导航数据
}
},
created(){
//获取服务端数据
this.$axios.get("http://localhost:8080/data/navs.json")
.then((res)=>{
console.log(res.data.data.data)
this.navs = res.data.data.data
this.sonNav = this.navs[0].subcategories
})
},
methods: {
clickNavOne(item){
this.sonNav = item.subcategories
},
toPage(){ //跳转
this.$router.push({
path: '/list'
})
}
}
}
</script>
<style scoped>
/* 垂直导航样式 */
.nav-body{
display: flex;
}
.navOne li{
margin: 10px 0px;
text-indent: 10px;
display: flex;
align-items: center;
height: 40px;
}
.sonNav li{
margin: 10px 0px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 50%;
}
.navOne{
position: relative;
flex: 1.5;
border-right: 1px solid #eee;
height: 600px;
overflow-y: scroll;
}
/* 隐藏滚动条 */
::-webkit-scrollbar{
display: none;
}
.sonNav{
flex: 3;
display: flex;
flex-wrap: wrap;
}
</style>
3.5 商品列表页面
在点击商品分类和关键词搜索后,会跳转到商品列表页面,该页面的所有数据都是通过条件查询获取到的,例如,关键词查询、分类查询等等。商品列表页面效果如图9所示。
图9 商品列表页面效果
在商品列表页面中,头部使用了Vant UI组件库中的NavBar 导航栏组件,该UI组件的引入代码如下:
import Vue from 'vue';
import { NavBar } from 'vant';
Vue.use(NavBar);
GoodsList.vue商品列表页面的代码如下:
<template>
<div>
<van-nav-bar
title="商品列表"
left-text="返回"
:right-text="`购物车(${cartList.length})`"
left-arrow
fixed
@click-left="onClickLeft"
@click-right="onClickRight"
/>
<div class="list-body">
<goods-card v-for="(item) in goods"
:key="item.goodsId"
:title="item.title"
:price="item.originalPrice"
:img="item.mainPic"
@click="addCarts(item)"
>
</goods-card>
</div>
</div>
</template>
<script>
import GoodsCard from '@/components/GoodsCard'
export default {
components: {
'goods-card': GoodsCard
},
data(){
return {
goods: [], //所有商品数据
cartList: [] //购物车商品数据
}
},
watch: {
cartList: {
handler(list){
//保存到本地
localStorage.cartList = JSON.stringify(list)
},
deep: true
}
},
created(){
//获取商品数据
this.$axios.get('/data/list.json').then(res=>{
// console.log(res.data.data.data.list)
this.goods = res.data.data.data.list
})
//获取购物车数据
let list = localStorage.cartList
if(list){
this.cartList = JSON.parse(list)
}
},
methods: {
onClickLeft(){ //返回按钮
window.history.back()
},
onClickRight(){ //查看购物车按钮
this.$router.push({
path: '/carts'
})
},
addCarts(item){ //添加购物车
//判断是否重复添加,true为重复添加,false为第一次添加
let double = false
this.cartList.map(cart=>{
if(cart.goods.goodsId == item.goodsId){
//该商品已经添加过购物车了
//把该商品的购买数量 +1
cart.num ++
double = true
return
}
})
//第一次添加
if(!double){
this.cartList.push({
goods: item,
num: 1
})
}
}
}
}
</script>
<style scoped>
.list-body{
display: flex;
justify-content: space-between;
flex-wrap: wrap;
margin-top: 50px;
}
</style>
3.6 购物车页面
点击商品卡片,可以将该商品加入到购物车中,在本项目案例中,购物车使用了localStorage本地存储实现对购物车商品的保存。购物车的头部使用了Vant UI组件库中的NavBar 导航栏组件,引入代码如下:
import Vue from 'vue';
import { NavBar } from 'vant';
Vue.use(NavBar);
在购物车页面中,商品列表的每个商品卡片,使用了Vant UI组件库中的Card 卡片业务组件,引入代码如下:
import Vue from 'vue';
import { Card } from 'vant';
Vue.use(Card);
购物车页面底部的购买商品总价计算与全选按钮,使用了Vant UI组件库的SubmitBar 提交订单栏业务组件,引入代码如下:
import Vue from 'vue';
import { SubmitBar } from 'vant';
Vue.use(SubmitBar);
购物车页面的商品展示效果如图10所示;点击全选按钮后,选中所有商品并计算商品总价,效果如图11所示。
图10 购物车页面效果
图11 商品全选效果
Carts.vue购物车页面的核心代码如下:
<template>
<div>
<van-nav-bar
title="购物车"
fixed
left-text="返回"
left-arrow
@click-left="onClickLeft"
/>
<!-- 购物车商品列表 -->
<div style="margin-top: 60px;margin-bottom: 70px">
<!-- 复选框组 -->
<van-checkbox-group v-model="result">
<div class="goods-item"
v-for="item in cartList"
:key="item.goodsId">
<van-swipe-cell>
<van-checkbox :name="item"></van-checkbox>
<van-card
:price="item.goods.originalPrice"
:desc="item.goods.dtitle"
:title="item.goods.title"
:thumb="item.goods.mainPic"
>
<template #num>
<van-stepper v-model="item.num" />
</template>
</van-card>
<template #right>
<van-button square
text="删除"
type="danger"
class="delete-button" />
</template>
</van-swipe-cell>
</div>
</van-checkbox-group>
</div>
<!-- 提交订单 -->
<van-submit-bar :price="totalPrice" button-text="提交订单">
<van-checkbox v-model="selAll" @click="handleSellAll">
全选
</van-checkbox>
</van-submit-bar>
</div>
</template>
<script>
export default {
data(){
return {
cartList: [] ,//购物车数据
selAll: false ,//全选状态
totalPrice: 0 ,//总价,单位分
result: [] ,//当前被选中的商品对象数组
}
},
watch: {
cartList: {
handler(list){
//保存到本地
localStorage.cartList = JSON.stringify(list)
},
deep: true
},
result: { //监听商品选中
handler(list){
//判断当前选中的商品数量是否和购物车中一致
if(list.length == this.cartList.length){
this.selAll = true
}else{
this.selAll = false
}
this.comTotalPrice()
},
deep:true
}
},
created(){
//获取购物车数据
let list = localStorage.cartList
if(list){
this.cartList = JSON.parse(list)
}
},
methods: {
onClickLeft(){ //返回
window.history.back()
},
comTotalPrice(){ //计算总价
let totalPrice = 0
this.result.map(item=>{
totalPrice += item.goods.originalPrice * item.num
})
this.totalPrice = totalPrice * 100
},
handleSellAll(){ //全选按钮
if(this.selAll){
this.result = this.cartList
}else{
this.result = []
}
}
}
}
</script>
<style scoped>
.goods-item{
margin: 10px 0px;
box-sizing: border-box;
padding-left: 10px;
display: flex;
}
.delete-button{
height: 100%;
}
</style>
项目源码下载地址:
https://download.csdn.net/download/p445098355/89570490