采用cli方式,手机端是上下结构,底部tab,上面内容,内容主要包括首页(轮播,列表商品),商品详情页(可以添加到购物车,这里需要在底部tab和购物车页面同步展示添加的商品,层层props传值太麻烦,所以直接存到响应式vuex里面),购物车清算页,还有我的信息。请求用axios。
首页列表数据太多最好缓存一下keep-alive,同时可以通过vuex的计算属性getNavState通过mapGetters辅助函数导出去判断是否展示底部导航
<!-- app.vue 主体内容 keep-alive缓存 当前只缓存首页,因为首页东西很多-->
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view>
<!-- 底部导航 -->
<TabBottom v-show="getNavState" />
// app.vue js部分
import {mapGetters} from 'vuex' // 辅助函数
computed:{
...mapGetters(['getNavState']),
num(){
// 其他计算属性
}
},
components:{
TabBottom
}
底部导航页面页需要对购物车数量进行展示,通过mapGetters拿到vuex的getter计算属性totalNum来展示,为0就不展示
<!-- 底部导航TabBottom.vue -->
<template>
<div class="nav-bottom">
<router-link to="/home">
<i class="iconfont s"></i>
<label>首页</label>
</router-link>
<router-link to="/order">
<i class="iconfont x"></i>
<i v-if="totalNum>0">{{totalNum}}</i>
<label>购物车</label>
</router-link>
</div>
</template>
<script>
import {mapGetters} from 'vuex'
export default {
data(){
return {
msg:{}
}
},
computed:mapGetters(['totalNum']),
methods:{
},
}
</script>
<style scoped>
.active {
color:red;
}
</style>
首页图片太多也需要懒加载vue-lazyload
// 懒加载动态图片 固定静态图片用字体图标
yarn add vue-lazyload -S
yarn add axios -S
// main.js
import VueLazyload from 'vue-lazyload' //懒加载
Vue.use(VueLazyload);
// 使用的组件list.vue 初始化访问固定@/assets/img/loading.gif图片
<img src="@/assets/img/loading.gif" v-lazy="item.url">
首页通常有轮播(每隔一定时间展示后面一张图(num++),找到最后一张图之后又从第一张开始num=0,这里适合用v-show)可以提到公共组件用props传入数据展示,大家一起用,轮播在组件挂载mounted时设置定时器setInterval播放autoPlay,同时需要在组件destroyed时移除定时器clearInterval
<template>
<div class="banner">
<img v-for="(v,i) in img" :key="i" :src="v" v-show="nowNum==i" />
<div class="banner-circle">
<ul>
<li :class="nowNum==i ?'selected':'' " v-for="(v,i) in img" :key="i"></li>
</ul>
</div>
</div>
</template>
<script>
export default {
data(){
return {
nowNum:0,// 控制当前展示那个baner图
timer:null,// 定时器,控制多久播放,组件destroy的时候清除
}
},
props:{
img: {
type: Array, // 对象或数组默认值必须从一个工厂函数获取
default: function () {
return []
}
},
},
methods:{
play(){
this.timer = setInterval(this.autoPlay,2000);
},
autoPlay(){
this.nowNum++;
if(this.nowNum >=this.img.length){
this.nowNum = 0;
}
}
},
mounted(){ //挂载完成
this.play();
},
destroyed(){ //销毁
clearInterval(this.timer);
}
}
</script>
<style scoped>
...
</style>
router部分,主要是VueRouter,这里也用到了缓存组件keepAlive和匹配样式linkExactActiveClass
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)// 符合vue规范的就需要use一下
const routes = [
{
path: '/',
redirect:'/home',// /重定向到首页
},
{
path: '/home', //首页
name: 'home',
meta:{
keepAlive:true , //需要缓存
title:'分页'
},
component: () => import( '../views/module/Home/index.vue')
},
{
path: '/detail/:id', //详情页
name: 'detail',
component: () => import( '../views/module/Detail/index.vue')
},
....
]
const router = new VueRouter({
linkActiveClass:'active',// 或者 linkExactActiveClass 精确匹配样式
routes
})
export default router
axios请求处理
// request/index.js 导出到main.js使用
import axios from 'axios'
axios.defaults.baseURL = `http://xxx:8080`;
// 添加请求拦截器
axios.interceptors.request.use(config=>{
// 加header,动画处理什么什么的
return config
})
// 添加响应拦截器
axios.interceptors.response.use(response=>{
// 返回响应码处理,token存储,服务器时间header里面获取date去存在vuex对比过期时间等
return response
},err=>{
// 对响应错误做点什么
return Promise.reject(err);
})
export default axios
添加的具体业务请求
// request/moduels/list.js
import axios from '../moduels/index.js' // from '.'等同于 './index'
export function getList(pageNo,pageSize){ //分页列表数据
return axios({
method:'get',
url:`/home/page/${pageNo}/${pageSize}`
})
}
export function getTotal(){ //所有列表的数据
return axios({
method:'get',
url:`/home`
})
}
...
store部分
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
var state = {
isNavShow:true, //底部导航显示与隐藏
cartAddData:[], //购物车数据
}
var getters={
getNavState(state){ //获取底部导航状态的值
return state.isNavShow
},
cartData(state){
return state.cartAddData// 购物车数据
},
totalNum(state){
return state.cartAddData.reduce((total,now)=>total+now.count,0)// 购物车总数
},
// (state, getters)或者也可以通过getter返回一个函数,来实现getter传参,price为单价
totalMoney: (state) => (price) => {
return state.cartAddData.reduce((total,now)=>total+now.count*price,0)// 购物车总金额
}
}
var mutations ={
ADD(state){
state.num +=1;
},
SHOWNAV(state){ //显示
state.isNavShow = true;
},
HIDENAV(state){ //隐藏
state.isNavShow = false;
},
CARTADD(state,data){ //购物车数据添加
if(state.cartAddData.length>0){
let item = state.cartAddData.filter(i => i.product_id===data.product_id)// 根据id对比cartAddData数据中是否有等于当前添加的数据
if(item.length>0){
item[0].count+=1// 购物车的数据加1
return
}else{
// 没有匹配id的数据
state.cartAddData.push(data);// 直接添加
}
}else{
state.cartAddData.push(data);
}
}
}
var actions= {
ADD({commit}){
commit('ADD')
},
SHOWNAV({commit}){ //显示
commit('SHOWNAV')
},
HIDENAV({commit}){ //隐藏
commit('HIDENAV')
},
CARTADD({commit},data){ //购物车数据添加
commit('CARTADD',data)
},
}
export default new Vuex.Store({
state,
getters,
mutations,
actions
})
详情页,这里需要注意进入就隐藏底部tab和添加数据vuex后跳转router到购物车,每次添加商品时,需要用$set()添加商品数量让购物车页渲染
// detail.vue
<template>
<div class="detail">
<div>
<a class="goback" @click="goBack()">返回</a>
<div>
<img :src="item.url" />
</div>
<div>
<p>{{item.name}}</p>
<p>{{item.detail}}</p>
</div>
</div>
<div class="shoppCart" >
<button @click="addToCart()">加入购物车</button>
</div>
</div>
</template>
<script>
import {getDetail} from '@/request/xxx'
export default {
data(){
return {
item:{}
}
},
methods:{
getData(id=1){
getDetail({id}).then(res=>{
this.item = res?res:{}// 假设这就是返回的对象
})
},
goBack(){
this.$router.push('/home');// 返回到首页
},
addToCart(){
// 每次添加商品时,需要添加商品数量
this.$set(this.item,'count',1)// 需要用set去操作对象让页面监测到渲染
this.$store.dispatch('CARTADD',this.item);// 添加商品
this.$router.push('/order');// 跳转到购物车页面tab
}
},
mounted(){
this.getData(this.$route.params.id);
// 这里需要router设置path: '/detail/:id', 才能拿到params下的id,否则用query方式路由传参
this.$store.dispatch('HIDENAV');// 详情页需要隐藏底部导航
},
destroyed(){
this.$store.dispatch('SHOWNAV');// 离开详情页需要显示底部导航
}
}
</script>
购物车页面,vuex里面的geter计算属性来计算总价和总数量,actions操作全选,添加/减少商品数量
<template>
<div class="order">
<ul>
<li v-for="(item, index) in cartData" :key="index">
<div class="flex">
<div class="shoppCart-list-l">
<input type="checkbox" />
<img :src="item.url" alt=""/>
</div>
<div class="shoppCart-list-r">
<p>{{item.product_name}}</p>
<div class="flex">
<div>
<span class="price-text">原价{{item.product_price}}</span>
<span class="red"> {{item.product_uprice}}</span>
</div>
<label>
<button @click="countChange(item,'-')">-</button>{{item.count}}<button @click="countChange(item,'+')">+</button>
</label>
</div>
</div>
</div>
</li>
</ul>
<div class="shoppCart-total">
<div class="flex">
<div><input type="checkbox" />全选</div>
<label>
<span>合计:</span>
<b class="red">{{totalMoney(100)}}</b>
<button class="btn">结算</button>
</label>
</div>
</div>
</div>
</template>
<script>
import {mapGetters} from 'vuex'
export default {
data(){
return {
msg:{}
}
},
computed:mapGetters(['cartData','totalMoney']),
methods:{
// ...mapActions(['ADD','SHOWNAV']),
add(){
// this.$store.dispatch('ADD');
},
// 有时间再来优化
countChange(item,type){
switch(type){
case '+':
item.count+=1;
break;
case '-':
item.count-=1;
break;
default:
return
}
}
},
}
</script>
<style scoped>
.flex{
display:flex;
justify-content: space-between;
margin:5%;
}
.shoppCart-list-l{
display:flex;
}
.shoppCart-list-l img{
height:4rem;
width:4rem
}
.red{
color: red;
}
.price-text{
text-decoration:line-through
}
.order{
position:relative;
height: 100vh;
}
.shoppCart-total{
position: absolute;
bottom: 4rem;
width:100%;
}
</style>
// main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'// 路由的引入
import store from './store'// vuex状态管理的引入
import VueLazyload from 'vue-lazyload' //懒加载动态图片
import './assets/font/iconfont.css'// 字体图样式
import './components/global' //引入全局组件定义banner,这里少用还是,毕竟main一开始就会被加载
Vue.use(VueLazyload);
Vue.config.productionTip = false// 一些productionTip禁掉
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')