文章目录

    • 项目准备
      • 安装 amfe-flexible
      • 安装 第三方插件 postcss-pxtorem
      • 配置vue.config.js
      • reset.css 重置样式表
      • 安装less 预编译语言
      • 安装vant-ui
    • 端口配置(可以不做)
    • axios 工具封装
      • 下载安装axios
      • utils/request.js
      • https/http.js
    • 跨域代理配置 vue.config.js
    • 项目路由规划配置
      • 下载vue-router
      • 在index.js 文件中安装使用vue-router
      • router/index.js
      • main.js: router 挂载到根实例对象上
      • components /AppTabBar.vue
    • 项目目录结构
    • 定义所有的接口请求 src\https\http.js
  • 首页
    • 效果
    • 一、首页展示的数据
      • home/home.vue
      • 组件---轮播图SwiperCom
      • 组件---grid 居家-志趣组件
        • 类别页Channel.vue
      • 组件---品牌制造商直供 Support
        • 品牌详情页Brand.vue
      • 组件---周一周四新品首发 Weekproduct
      • 组件---人气推荐 top组件
      • 组件---专题精选TopicBox
    • 二、首页搜索功能
      • 1、搜索页面
        • 1.在http.js文件中定义接口请求
        • 2.views /SearchPopup.vue
      • 2、历史记录和热门搜索组件 components/HistoryHot.vue
      • 3、搜索框提示列表组件 components/SearchTipsList.vue
      • 4、综合-价格-分类 components/SearchProducts.vue
      • 5、搜索出的产品展示 components/Products.vue
      • 6、关于重复点击同一个路由出现的报错问题解决
      • 7、路由拦截/路由守卫
  • 详情页
    • 1.在http.js 文件中定义详情页请求接口
    • 2.router/index.js
    • 3.components /ProductDetail.vue
  • 专题页
    • 1.在http.js文件中定义接口请求:
    • 2.Topic.vue 组件
  • 分类页
    • 1.在http.js 文件中,定义接口请求
    • 2.Category.vue 组件
  • 购物车页
    • 1.在http.js 文件中定义接口
    • 2.Cart.vue
      • 发送获取购物车数据列表时的响应数据
      • 购物车商品步进器功能接口
      • 切换购物车商品选中状态功能接口(含全选)响应数据
  • 我的页
    • 1.在http.js文件中定义接口请求
    • 2.User.vue
  • 路由守卫
  • 项目中的bug
  • 项目优化---路由懒加载

gitee地址:

https://gitee.com/sansan533/bk2115-yanxuan

项目准备

项目使用vue-cli + vant+ less +axios 开发

安装 amfe-flexible

在main.js 主入口文件引入 amfe-flexible, 它会自动设置html的font-size为屏幕宽度除以10,也就是1rem等于html根节点的font-size。假如设计稿的宽度是750px,此时1rem应该等于75px。假如量的某个元素的宽度是150px,那么在css里面定义这个元素的宽度就是 width: 2rem

<header><!-- 在页面 header标签中设置移动端视口--><meta name='viewport' content='width=device-width,initial-scale=1.0, maximum-      scale=1.0,user-scalable=no'>
</header>
npm install amfe-flexible --save//在main.js 引入amfe-flexible
import 'amfe-flexible';

安装 第三方插件 postcss-pxtorem

会自动将css代码中的px单位根据规则转换成rem 单位

注意: 如果css样式中 有不需要转成rem 的单位,只需将单位写成大写PX 即可。

//注意需要安装5.11 版本,否则报错
npm i postcss-pxtorem@5.1.1

配置vue.config.js

在项目根目录创建vue.config.js文件,设置如下配置

注意:修改完项目根目录下的配置文件后,一定要重启项目,这样配置文件才生效

module.exports = {lintOnSave:false,// eslint-loader 是否在保存的时候检查css: {loaderOptions: {postcss: {plugins: [// 把px单位换算成rem单位require("postcss-pxtorem")({// 换算的基数 375的设计稿,换算基数就是37.5rootValue: 37.5, selectorBlackList: [".van"],// 要忽略的选择器并保留为px。propList: ["*"], //可以从px更改为rem的属性。minPixelValue: 1 // 设置要替换的最小像素值。})]}}}
}

reset.css 重置样式表

在main.js 文件中, 引入重置样式表,去掉标签的默认样式

// 引入重置样式表
import '@/assets/css/reset.css'

reset.css 重置样式表代码:

@charset "utf-8";html{background-color:#fff;color:#000;font-size:12px}
body,ul,ol,dl,dd,h1,h2,h3,h4,h5,h6,figure,form,fieldset,legend,input,textarea,button,p,blockquote,th,td,pre,xmp{margin:0;padding:0}
body,input,textarea,button,select,pre,xmp,tt,code,kbd,samp{line-height:1.5;font-family:tahoma,arial,"Hiragino Sans GB",simsun,sans-serif}
h1,h2,h3,h4,h5,h6,small,big,input,textarea,button,select{font-size:100%}
h1,h2,h3,h4,h5,h6{font-family:tahoma,arial,"Hiragino Sans GB","微软雅黑",simsun,sans-serif}
h1,h2,h3,h4,h5,h6,b,strong{font-weight:normal}
address,cite,dfn,em,i,optgroup,var{font-style:normal}
table{border-collapse:collapse;border-spacing:0;text-align:left}
caption,th{text-align:inherit}
ul,ol,menu{list-style:none}
fieldset,img{border:0}
img,object,input,textarea,button,select{vertical-align:middle}
article,aside,footer,header,section,nav,figure,figcaption,hgroup,details,menu{display:block}
audio,canvas,video{display:inline-block;*display:inline;*zoom:1}
blockquote:before,blockquote:after,q:before,q:after{content:"\0020"}
textarea{overflow:auto;resize:vertical}
input,textarea,button,select,a{outline:0 none;border: none;}
button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}
mark{background-color:transparent}
a,ins,s,u,del{text-decoration:none}
sup,sub{vertical-align:baseline}
html {overflow-x: hidden;height: 100%;font-size: 50px;-webkit-tap-highlight-color: transparent;}
body {font-family: Arial, "Microsoft Yahei", "Helvetica Neue", Helvetica, sans-serif;color: #333;font-size: .28em;line-height: 1;-webkit-text-size-adjust: none;}
hr {height: .02rem;margin: .1rem 0;border: medium none;border-top: .02rem solid #cacaca;}
a {color: #25a4bb;text-decoration: none;}

安装less 预编译语言

编译成css, 在main.js 引入使用

注意:此处安装less-loader 版本需是5.x版本,否则报错,默认安装的是最新版本,所以安装需指定版本号

npm install --save less
npm install less-loader@5.0.0 --save//在main.js中,引入less并使用
import less from 'less'
Vue.use(less)
// 代码中使用
<style lang='less' scoped></style>

安装vant-ui

官网地址: https://vant-contrib.gitee.io/vant/#/zh-CN/

项目目录下安装vant:

npm i vant
或者
yarn add vant

​ 在package.json文件中看到上面效果即安装成功

​ 在main.js 文件中引入vant 对应的js和css 文件

//导入所有组件。
import Vant from 'vant';
import 'vant/lib/index.css';
Vue.use(Vant);

** 也可以在对应组件的script标签中按需导入**

import Vue from "vue";
import { Button } from "vant";
import "vant/lib/button/style";
Vue.use(Button);

在views/Home.vue的template标签中:

<div class="home"><p>Home组件测试vant组件</p><van-button type="primary">主要按钮</van-button>
</div>

即可运行项目看到效果

端口配置(可以不做)

如果想要更改8080端口,可以在根目录下新建 vue.config.js 中:

module.exports = {devServer: {port: 5000}
}

重新运行 npm run serve 就可以在 http://localhost:5000 中访问项目了。

axios 工具封装

下载安装axios

npm install axios

utils/request.js

在src目录下创建utils目录, 创建request.js 文件.

// 导入axios
import axios from 'axios';
// 使用自定义配置新建一个axios 实例,对axios 做一些基础配置
const instance = axios.create({baseURL: 'http://kumanxuan1.f3322.net:8001/',timeout: 5000,headers: { 'X-Custom-Header': 'foobar' }
});// 添加请求拦截器
instance.interceptors.request.use(function (config) {//请求之前执行该函数, 一般在该处设置token let token = localStorage.getItem("token");if (token) {config.headers["token"] = token}// 在发送请求之前做些什么return config;
}, function (error) {// 对请求错误做些什么return Promise.reject(error);
});//响应拦截器
instance.interceptors.response.use(response => {//1.非200响应//2.token过期//3.异地登陆//4.非对象加密的解密return response.data
})
export default instance

https/http.js

在src目录下创建https 目录, 目录下创建http.js 文件,该文件主要用来管理所有的http请求的,如下:

// 所有的请求都放在该目录
import instance from "../utils/request";
//首页所有请求
//1. 获取首页数据列表
export function getIndexList() {return instance.get('/index/index')
}//]专题页 Topic
//专题请求
export function getTopicList(params) {return instance({url: '/topic/list',method: 'get',params})
}//6. 分类页 Category
// 全部分类数据接口
export function GetChannelDataApi(params) {return instance({url: '/api/catalog/index',method: 'get',params})
}
// 获取当前分类数据
export function GetFenleiDataApi(params) {return instance({url: '/catalog/current',method: 'get',params})
}//我的页面 User
//登陆
export function GoLogin(params) {return instance({url: '/auth/loginByWeb',method: 'post',data: params})
}// 搜索页
// 根据关键字搜索接口
export function GetSearchData(params) {return instance.get('/goods/list', {params})
}// 详情页
//根据id查询对应数据接口
export function getDetailData(params) {return instance.get('/goods/detail', {params})
}//详情页相关产品
export function GetGoodsRelatedData(params) {return instance({url: '/goods/related',method: 'get',params})
}// 2.搜索页 SearchPopup
// 历史记录列表和热门搜索列表
export function GetPopupData(params) {return instance({url: '/search/index',method: 'get',params})
}//删除历史记录
export function Clearhistory(params) {return instance({url: '/search/clearhistory',method: 'post',data: params})
}//搜索提示列表
export function GetSearchTipsListData(params) {return instance({url: '/search/helper',method: 'get',params})
}//4.分类数据获取 Channel
export function GetCateGoryData(params) {return instance({url: '/goods/category',method: 'get',params})
}// 分类页面商品列表请求
export function GetCateGoryList(params) {return instance({url: '/goods/list',method: 'get',params})
}// 获取品牌详情数据列表请求
export function GetBrandList(params) {return instance({url: '/brand/detail',method: 'get',params})
}// 获取分页品牌详情中的产品列表
export function GetBrandListData(params) {return instance({url: '/goods/list',method: 'get',params})
}//购物车页 Cart
// 购物车列表
export function GetCartData(params) {return instance({url: '/cart/index',method: 'get',params})
}// 加入购物车
export function AddToCart(params) {return instance.post('/cart/add', params)
}// 获取购物车产品数量
export function GetCartCountData(params) {return instance({url: '/cart/goodscount',method: 'get',params})
}// 加入购物车
export function UpdateCartData(params) {return instance({url: '/cart/update',method: 'post',data: params})
}// 删除购物车商品
export function DeleteCartData(params) {return instance({url: '/cart/delete',method: 'get',params})
}// 删除购物车商品
export function DeleteCartData2(params) {return instance({url: '/cart/delete',method: 'post',data: params})
}// 切换购物车商品选中状态功能接口(含全选
export function ToggleCartCheckedData(params) {return instance({url: '/cart/checked',method: 'post',data: params})
}

跨域代理配置 vue.config.js

我们对 vue.config.js 进行配置:

module.exports = {devServer: {port: 8080,proxy: {'/api': {target: "http://kumanxuan1.f3322.net:8001/",pathRewrite: {'^/api': ''}}}}
}

由于配置文件修改了,这里一定要记得重新 npm run serve !!

项目路由规划配置

如果项目中所有的路由都写在入口文件中,那么将不便于编写项目和后期维护。因此路由需要进行模块化处理。

在src目录下创建router目录,该目录下新建index.js 用于 编写路由配置

下载vue-router

npm install vue-router

在index.js 文件中安装使用vue-router

import Vue from 'vue'
import VueRouter from 'vue-router'Vue.use(VueRouter)

在index.js文件中编写项目路由基础配置

  • 首页模块
  • 专题模块
  • 分类模块
  • 购物车模块
  • 我的模块

router/index.js


// 配置项目路由
const router = new VueRouter({routes: [{path: '/',redirect: '/home' // 重定向},{path: '/home',//首页name: 'Home',component: () => import('@/views/Home'),meta: { // 用来判断该组件对应的页面是否显示底部tabbarisShowTabbar: true}},{path: '/topic',//专题name: 'Topic',component: () => import('@/views/Topic'),meta: { // 用来判断该组件对应的页面是否显示底部tabbarisShowTabbar: true}},{path: '/category',//分类name: 'Category',component: () => import('@/views/Category'),meta: { // 用来判断该组件对应的页面是否显示底部tabbarisShowTabbar: true}},{path: '/cart',//购物车name: 'Cart',component: () => import('@/views/Cart'),meta: { // 用来判断该组件对应的页面是否显示底部tabbarisShowTabbar: true}},{path: '/user',//我的name: 'User',component: () => import('@/views/User'),meta: { // 用来判断该组件对应的页面是否显示底部tabbarisShowTabbar: true}},]
})export default router

main.js: router 挂载到根实例对象上

根据路由配置,在src目录下的views 文件中,分别创建tabbar 对应的组件文件。

在main.js 文件中引入router 文件下的index.js 路由配置文件,并在根实例中注册。


// 在 入口文件mian.js 引入路由文件
import router from '@/router/index.js';
new Vue({render: h => h(App),router  // router 挂载到根实例对象上
}).$mount('#app')

components /AppTabBar.vue

在components 文件中,创建一个AppTabBar.vue组件,配置底部tabbar,直接可以使用vant的tabbar组件

<!-- 底部tabbar -->
<template><van-tabbarv-model="active"active-color="#ee0a24"inactive-color="#000"@change="onChange"><van-tabbar-item icon="home-o" to="/home">首页</van-tabbar-item><van-tabbar-item icon="label-o" to="/topic">专题</van-tabbar-item><van-tabbar-item icon="apps-o" to="/category">分类</van-tabbar-item><van-tabbar-item icon="shopping-cart-o" to="/cart">购物车</van-tabbar-item><van-tabbar-item icon="user-o" to="/user">我的</van-tabbar-item></van-tabbar>
</template><script>
export default {data() {return {active: 0,};},methods: {onChange(m) {//切换底部分类console.log(m);this.active = m;},},
};
  • 7…将AppTabBar.vue导入app.vue 跟组件。
<template><div id="app"><!-- 路由对应的组件存放到router-view中--><router-view></router-view><!-- 底部tabbar 组件 --><AppTabBar v-show ='$router.meta.isShowTabbar'></AppTabBar></div>
</template><script>
// 引入底部tabbar 组件
import AppTabBar from "@/components/AppTabBar";
export default {name: "App",components: { AppTabBar },
};
</script><style>
#app {font-size: 12px;
}
</style>

项目目录结构

定义所有的接口请求 src\https\http.js

// 所有的请求都放在该目录
import instance from "../utils/request";
//首页所有请求
//1. 获取首页数据列表
export function getIndexList() {return instance.get('/index/index')
}//专题页 Topic
//专题请求
export function getTopicList(params) {return instance({url: '/topic/list',method: 'get',params})
}//6. 分类页 Category
// 全部分类数据接口
export function GetChannelDataApi(params) {return instance({url: '/api/catalog/index',method: 'get',params})
}
// 获取当前分类数据
export function GetFenleiDataApi(params) {return instance({url: '/catalog/current',method: 'get',params})
}//我的页面 User
//登陆
export function GoLogin(params) {return instance({url: '/auth/loginByWeb',method: 'post',data: params})
}// 搜索页
// 根据关键字搜索接口
export function GetSearchData(params) {return instance.get('/goods/list', {params})
}// 详情页
//根据id查询对应数据接口
export function getDetailData(params) {return instance.get('/goods/detail', {params})
}//详情页相关产品
export function GetGoodsRelatedData(params) {return instance({url: '/goods/related',method: 'get',params})
}// 2.搜索页 SearchPopup
// 历史记录列表和热门搜索列表
export function GetPopupData(params) {return instance({url: '/search/index',method: 'get',params})
}//删除历史记录
export function Clearhistory(params) {return instance({url: '/search/clearhistory',method: 'post',data: params})
}//搜索提示列表
export function GetSearchTipsListData(params) {return instance({url: '/search/helper',method: 'get',params})
}//4.分类数据获取 Channel
export function GetCateGoryData(params) {return instance({url: '/goods/category',method: 'get',params})
}
// 分类页面商品列表请求
export function GetCateGoryList(params) {return instance({url: '/goods/list',method: 'get',params})
}// 获取品牌详情数据列表请求
export function GetBrandList(params) {return instance({url: '/brand/detail',method: 'get',params})
}// 获取分页品牌详情中的产品列表
export function GetBrandListData(params) {return instance({url: '/goods/list',method: 'get',params})
}//购物车页 Cart
// 购物车列表
export function GetCartData(params) {return instance({url: '/cart/index',method: 'get',params})
}
// 加入购物车
export function AddToCart(params) {return instance.post('/cart/add', params)
}// 获取购物车产品数量
export function GetCartCountData(params) {return instance({url: '/cart/goodscount',method: 'get',params})
}// 加入购物车
export function UpdateCartData(params) {return instance({url: '/cart/update',method: 'post',data: params})
}// 删除购物车商品
export function DeleteCartData(params) {return instance({url: '/cart/delete',method: 'get',params})
}// 删除购物车商品
export function DeleteCartData2(params) {return instance({url: '/cart/delete',method: 'post',data: params})
}// 切换购物车商品选中状态功能接口(含全选
export function ToggleCartCheckedData(params) {return instance({url: '/cart/checked',method: 'post',data: params})
}

首页

效果

一、首页展示的数据

1.http.js文件中的接口请求

//1. 首页 Home
// 获取首页数据列表
export function getIndexList() {return instance.get('/index/index')
}

home/home.vue

<template><div class="home"><!-- 二级路由坑 --><router-view v-if="$route.path === '/home/searchPopup'"></router-view><main v-else><!--搜索框 --><van-searchshape="round"v-model="value"show-actionplaceholder="请输入搜索关键词"@search="onSearch"@cancel="onCancel"@click="$router.push('/home/searchPopup')"/><!-- 轮播图 --><SwiperCom :banner="banner"></SwiperCom><!-- grid 居家-志趣组件 --><Grid :channel="channel"></Grid><!-- 品牌制造商直供 Support --><Support :brandList="brandList"></Support><!-- 周一周四新品首发 Weekproduct --><Weekproduct:newGoodsList="newGoodsList"title="周一周四新品首发"></Weekproduct><!-- 人气推荐 top组件 --><!-- v-if 保证了有数据才渲染该组件,等渲染该组件的时候,才执行该组件的生命周期函数--><Top :hotGoodsList="hotGoodsList" v-if="hotGoodsList.length>0"></Top><!-- 专题精选 TopicBox --><TopicBox :topicList="topicList" title="专题精选"></TopicBox><!-- 产品列表 Weekproduct --><Weekproductv-for="item in categoryList":key="item.id":newGoodsList="item.goodsList":title="item.name"></Weekproduct></main></div>
</template><script>
// 导入轮播图组件
import SwiperCom from "@/components/SwiperCom";
import { getIndexList } from "@/https/http.js";
// 引入 grid  居家-志趣组件
import Grid from "@/views/home/Grid";
// 引入support 组件-品牌制造商直供
import Support from "@/views/home/Support";
import Weekproduct from "@/views/home/Weekproduct";
import TopicBox from "@/views/home/TopicBox";
import Top from '@/views/home/Top'
export default {name: "home",data() {return {value: "",banner: [],  //轮播图channel: [],  //居家-志趣数据brandList: [],  // 品牌制造商数据newGoodsList: [],  // 周一周四新品首发数据hotGoodsList:[],  // 人气推荐topicList: [],  // 专题精选categoryList: []  //产品列表};},components: {SwiperCom,Grid,Support,Weekproduct,TopicBox,Top,},created() {// 发送请求,获取数据getIndexList().then((res) => {console.log("res", res);this.banner = res.data.banner;this.channel = res.data.channel;this.brandList = res.data.brandList;this.newGoodsList = res.data.newGoodsList;this.topicList = res.data.topicList;this.categoryList = res.data.categoryList;this.hotGoodsList = res.data.hotGoodsList;});},methods: {onSearch() { },onCancel() { },},
};
</script><style lang="less" scoped>
.home {padding-bottom: 100px;box-sizing: border-box;
}
</style>

组件—轮播图SwiperCom

<template><!-- 轮播图 --><div class="swiper-com"><van-swipe class="my-swipe" :autoplay="1000" indicator-color="white"><van-swipe-item v-for="item in banner" :key="item.id" ><img :src="item.image_url" alt=""></van-swipe-item></van-swipe></div>
</template><script>
export default {name:'swiper-com',props:['banner'],}
</script><style lang="less" scoped>.swiper-com{width: 100%;img{width: 100%;}}
</style>

组件—grid 居家-志趣组件

<template><div class="gird"><van-grid :column-num="5" channel.length><van-grid-itemv-for="item in channel":key="item.id":icon="item.icon_url":text="item.name"@click="btn(item.id)"/></van-grid></div>
</template><script>
export default {name: "gird",props: ["channel"],methods: {btn(id){this.$router.push({path: '/channel', query:{id: id}})}}
};
</script><style>
</style>

类别页Channel.vue

Channel.vue

<template><div class="channel-box"><van-tabs sticky @change="changeFn"><van-tab v-for="item in brotherCategory" :key="item.id" :title="item.name" ><h4>{{ item.name }}</h4><p>{{ front_desc }}</p><!-- 产品列表 --><Products :goodsList="goodsList" /></van-tab></van-tabs></div>
</template><script>
import { GetCateGoryData, GetCateGoryList } from "@/https/http";
import Products from "@/components/Products";export default {name: "channel",data() {return {id_: "0", // 当前类别的idpage: 1, // 当前页数size: 1000, //每页显示条数brotherCategory: [], // 分类数组goodsList: [], //当前类别对应的商品列表front_desc: "",};},created() {this.id_ = this.$route.query.id;this.getCategoryData();  //获取所有分类数据this.getCategoryListData();  //获取当前类别对应的产品数组}, methods: {//获取所有分类数据getCategoryData() {GetCateGoryData({ id: this.id_ }).then((res) => {this.brotherCategory = res.data.brotherCategory; // 全部分类数组this.currentCategory = res.data.currentCategory; // 当前分类对象this.front_desc = this.currentCategory.front_desc; // 当前类别文字描述});},//获取当前类别对应的产品数组getCategoryListData() {GetCateGoryList({categoryId: this.id_,page: this.page,size: this.size,}).then((res) => {console.log(33, res);if (res.errno == 0) {this.goodsList = res.data.goodsList;}});},// / 切换分类changeFn(title, name) {// title 下标// name: 分类标题this.brotherCategory.forEach((item) => {if (item.name == name) {this.id_ = item.id;}});this.getCategoryListData();this.getCategoryData();},},components: {Products,},
};
</script><style lang="less" scoped>
.channel-box {text-align: center;font-size: 16px;line-height: 40px;p {color: #666;}
}
</style>

组件—品牌制造商直供 Support

<template><div class="support"><ul><li v-for="item in brandList" :key="item.id" @click="clickFn(item.id)"><img :src="item.pic_url" alt=""><h4>{{item.name}}</h4><p>{{item.floor_price|moneyFlrmat}}</p></li></ul></div>
</template><script>
export default {name:'support',props:['brandList'],methods: {clickFn(id){this.$router.push({path:'/brand', query:{id: id}})}}
}
</script><style lang="less" scoped>.support>ul{width: 100%;display: flex;justify-content: space-between;flex-wrap: wrap;li{width: 49%;position: relative;img{width: 100%;}h4 {font-size: 16px;position: absolute;left: 20px;top: 30px;}p {font-size: 15px;position: absolute;left: 30px;top: 60px;color: red;}}}
</style>

品牌详情页Brand.vue

组件—周一周四新品首发 Weekproduct

<template><div class="week-product"><div class="mytitle"><span></span><h3>{{ title }}</h3></div><ul><liv-for="item in newGoodsList":key="item.id"@click="clickFn(item.id)"><img :src="item.list_pic_url" alt="" /><p>{{ item.name }}</p><p>{{ item.retail_price }}</p></li></ul></div>
</template><script>
export default {name: 'week-product',props: ['newGoodsList', 'title'],methods: {clickFn(id) {this.$router.push({ path: '/productDetail', query: { id: id } })}}
}
</script><style lang="less" scoped>
.week-product {.mytitle {text-align: center;font-size: 16px;margin-top: 20px;position: relative;height: 50px;span {width: 50%;height: 2px;background-color: #ccc;display: inline-block;position: absolute;left: 50%;top: 50%;transform: translate(-50%, -50%);}h3 {width: 30%;background-color: #fff;position: absolute;left: 50%;top: 50%;transform: translate(-50%, -50%);}}ul {display: flex;justify-content: space-between;flex-wrap: wrap;li {width: 49%;img {width: 100%;}p {text-align: center;font-size: 16px;}}}
}
</style>

组件—人气推荐 top组件

<template><div class="top-box"><h3>人气推荐</h3><div class="content" v-for="item in hotGoodsList" :key="item.id"><van-card:key="item.id":price="item.retail_price":desc="item.goods_brief":title="item.name":thumb="item.list_pic_url"@click="clickFn(item.id)"/></div></div>
</template><script>
export default {name: 'top',props: ['hotGoodsList'],created() {console.log(555, this.hotGoodsList);},methods: {clickFn(id) {this.$router.push({ path: '/productDetail', query: { id: id } })}}
}
</script><style lang="less" scoped>
.top-box {h3 {font-size: 22px;line-height: 60px;text-align: center;}
}
</style>

组件—专题精选TopicBox

<template><div class="topic"><h3>{{ title }}</h3><van-swipe class="my-swipe" :autoplay="1000" indicator-color="white"><van-swipe-itemv-for="item in topicList":key="item.id"><img :src="item.item_pic_url" alt="" /><p>{{ item.title }}</p><p>{{ item.subtitle }}</p></van-swipe-item></van-swipe></div>
</template><script>
export default {name: 'topic',props: ['topicList', 'title'],}
</script><style lang="less" scoped>
.topic {width: 100%;text-align: center;font-size: 16px;h3 {font-size: 22px;line-height: 60px;text-align: center;}.my-swipe {display: flex;img {width: 100%;height: 200px;}}
}
</style>

二、首页搜索功能

 {path: '/home',//首页name: 'Home',component: () => import('@/views/Home'),meta: { // 用来判断该组件对应的页面是否显示底部tabbarisShowTabbar: true},children: [{path: 'searchPopup',name: 'SearchPopup',component: () => import('@/views/SearchPopup')}]},

1、搜索页面

1.在http.js文件中定义接口请求

// 2.搜索页 SearchPopup
// 历史记录列表和热门搜索列表
export function GetPopupData(params) {return instance({url: '/search/index',method: 'get',params})
}
//删除历史记录
export function Clearhistory(params) {return instance({url: '/search/clearhistory',method: 'post',data: params})
}//搜索提示列表
export function GetSearchTipsListData(params) {return instance({url: '/search/helper',method: 'get',params})
}
//根据关键字搜索商品
export function GetSearchData(params) {return instance({url: '/goods/list',method: 'get',params})
}

2.views /SearchPopup.vue

在views 目录下创建SearchPopup.vue 页面,作为点击搜索后的页面

<!-- 搜索页 -->
<template><div class="popup"><!-- 搜索框组件 --><van-searchv-model="value"show-action:placeholder="placeholderVal"@search="onSearch"@cancel="onCancel"@input="onInput"/><!-- 历史记录和热门搜索 组件 --><HistoryHotv-if="blockShow == 1":searchHistoryData="searchHistoryData":searchHotData="searchHotData"@goSearch="setValue"></HistoryHot><!-- 搜索提示列表 组件--><SearchTipsListv-else-if="blockShow == 2":searchTipsArr="searchTipsArr"@setValue="setValue"></SearchTipsList><!-- 综合-价格-分类组件 --><SearchProducts:goodsList="goodsList":filterCategory="filterCategory"@priceChange="priceChange"@cateChange="cateChange"v-else></SearchProducts></div>
</template><script>
// 搜索历史记录请求api
import {GetPopupData, //历史记录列表和热门搜索列表GetSearchTipsListData, //搜索提示列表GetSearchData, //根据关键字搜索商品
} from "@/https/http";
// 历史记录和热门搜索 组件
import HistoryHot from "@/components/HistoryHot";
//搜索提示列表组件
import SearchTipsList from "@/components/SearchTipsList";
// 引入 综合-价格-分类组件
import SearchProducts from "@/components/SearchProducts";
export default {data() {return {value: "", // 搜索框内容//1 展示历史记录和热门搜索 HistoryHot,//2 展示搜索提示列表 SearchTipsList//3 展示搜索出来内容blockShow: 1,placeholderVal: "", // 搜索框 placeholder 提示词searchHistoryData: [], // 历史记录列表searchHotData: [], // 热门搜索列表searchTipsArr: [], //搜索提示列表(输入框输入时)order: "desc", // desc 价格由高到底,asc 价格由低到底高categoryId: 0, // 类别id,代表下拉菜单中的全部、居家等选项sort: "id", // 可以是id或pricegoodsList: [], //  搜索出来的商品列表filterCategory: [], //下拉菜单分类数组};},components: {HistoryHot, //  历史记录和热门搜索组件SearchTipsList, //搜索提示列表组件SearchProducts, //综合-价格-分类组件},created() {GetPopupData().then((res) => {console.log(11, res);this.placeholderVal = res.data.defaultKeyword.keyword; // 搜索框 placeholder 提示词this.searchHistoryData = res.data.historyKeywordList; // 历史记录列表this.searchHotData = res.data.hotKeywordList; // 热门搜索列表});},methods: {onSearch() {// 确定搜索console.log("输入确定");let params = {keyword: this.value,page: 1,size: 20,order: this.order, //价格排序categoryId: this.categoryId, //分类排序idsort: this.sort, //分类拍戏  price};GetSearchData(params).then((res) => {console.log(44, res);if (res.errno == 0) {this.blockShow = 3; // 如果由商品显示搜索的商品列表this.goodsList = res.data.goodsList; // 搜索出来的商品列表let cate = res.data.filterCategory; // 下拉菜单分类数组(全部,居家,。。。)cate = JSON.parse(JSON.stringify(cate).replace(/id/g, "value").replace(/name/g, "text"));this.filterCategory = cate;}});},onCancel() {// 点击取消 返回上一页console.log("取消");this.$router.go(-1);},onInput(val) {this.blockShow = 2; // 展示搜索提示列表组件// 输入触发// 搜索提示数据请求GetSearchTipsListData({ keyword: val }).then((res) => {console.log(33, res);this.searchTipsArr = res.data;});},setValue(m) {// 点击历史记录和热门搜索触发 触发setValue// 点击搜索列表提示触发 也触发setValueconsole.log(m); // m 为传过来的参数this.value = m;this.onSearch();},priceChange(m) {// 点击价格进行排序this.order = m;this.sort = "price";this.onSearch(); // 请求数据},cateChange(n) {// 点击分类进行筛选this.categoryId = n;this.sort = "id";this.onSearch(); // 请求数据},},
};
</script>
<style lang='less' scoped >
/* @import url(); 引入css类 */
.popup {width: 100%;height: 100%;display: flex;flex-direction: column;
}
</style>

2、历史记录和热门搜索组件 components/HistoryHot.vue

在components目录下 创建 HistoryHot.vue(历史记录和热门搜索) 组件,引入到SearchPopup.vue中

<template><div class="box"><div class="history-hot" v-if="isShowHistory"><h4>历史记录</h4><van-icon name="delete" class="delete-icon" @click="clearFn" /><van-tagplaintype="primary"v-for="(item, index) in historyKeywordList":key="index"v-if="item"@click="goSearch(item)">{{ item }}</van-tag></div><div class="hot-box"><h4>热门搜索</h4><van-tagplain:type="item.is_hot ? 'danger' : 'primary'"v-for="(item, index) in hotKeywordList":key="index"v-if="item.keyword"@click="goSearch(item.keyword)">{{ item.keyword }}</van-tag></div></div>
</template><script>
import { Clearhistory } from '@/https/http'
export default {name: "history-hot",data() {return {isShowHistory: 1}},props: ["historyKeywordList", "hotKeywordList"],methods: {clearFn() {Clearhistory().then((res) => {console.log(res);this.isShowHistory = 0})},goSearch(value) {this.$emit('goSearch', value)}}
};
</script>
<style lang="less" scoped>
.box {font-size: 16px;span {margin-right: 3px;}.history-hot {margin-bottom: 10px;position: relative;.delete-icon {position: absolute;top: 10px;right: 10px;}}
}
</style>

3、搜索框提示列表组件 components/SearchTipsList.vue

在components目录下 创建 SearchTipsList.vue(搜索框提示列表组件) 组件,引入到SearchPopup.vue中

<template><div class="search-tips-list-box"><van-listv-model="loading":finished="finished"finished-text="没有更多了"@load="onLoad"><van-cellv-for="item in dataList":key="item":title="item"@click="geiValue(item)"/></van-list></div>
</template><script>
export default {name: "search-tips-list",data() {return {list: [],loading: false,  // //是否处于加载状态finished: false, // 是否加载完成};},props: ["dataList"],   父传子数组methods: {onLoad() {},geiValue(value){this.$emit('setValue',value)}},
};
</script><style lang="less" scoped>
</style>

4、综合-价格-分类 components/SearchProducts.vue

在components 下 新建SearchProducts.vue (综合-价格-分类组件)组件,引入到SearchPopup.vue

<template><div class="search-products-box"><van-dropdown-menu><van-dropdown-item disabled title="全部" /><!-- van-dropdown-item的change自带回调参数value --><van-dropdown-itemv-model="value2":options="option2"title="价格排序"@change="priceChange"/><van-dropdown-itemv-model="value3":options="filterCategory"title="分类"@change="categoryChange"/></van-dropdown-menu><!-- 如有没有搜索到商品显示 empty--><van-emptyv-if="goodsList.length == 0"class="custom-image"image="search"description="抱歉,没有搜索到商品"/><!-- 引入下方产品展示组件,如果搜索到商品显示 --><Products :goodsList="goodsList"></Products></div>
</template><script>
import Products from '@/components/Products'
export default {name: "search-products",data() {return {value2: "desc",option2: [{ text: "价格由高到低", value: "desc" },{ text: "价格由低到高", value: "asc" },],};},props: ["filterCategory", "goodsList"],methods: {priceChange(value) {console.log("价格" + value);//   在子组件中触发自定义事件,给父组件传递参数this.$emit("priceChange1", this.value2);},categoryChange(value) {console.log("类别id" + value);//   在子组件中触发自定义事件,给父组件传递参数this.$emit("categoryChange1", value);},},components: {Products,},computed: {value3: {get() {let val = "";this.filterCategory.forEach((item) => {if (item.checked) {val = item.value;}});return val;},set(value) {console.log(value);},},},
};
</script ><style lang="less" scoped>
</style>

5、搜索出的产品展示 components/Products.vue

在components 下 新建 Products.vue(搜索出的产品展示组件),引入到SearchProducts.vue,作为其子组件使用。

<!-- 搜索出的产品展示组件 -->
<template><!-- 数据列表渲染 --><ul class="goods_list"><li v-for="item in goodsList" :key="item.id" @click="gotodetail(item.id)"><img v-lazy="item.list_pic_url" alt="" /><p>{{ item.name }}</p><p>{{ item.retail_price | moneyFlrmat }}</p></li></ul>
</template><script>
export default {name:'products',props:['goodsList'],data(){return{}},methods: {gotodetail(id_){this.$router.push({path:'/productDetail',query:{id:id_}})}}
}
</script><style lang="less" scoped>.goods_list {display: flex;flex-wrap: wrap;justify-content: space-between;font-size: 16px;line-height: 20px;text-align: center;li {width: 48%;img {width: 100%;}}}
</style>

6、关于重复点击同一个路由出现的报错问题解决

在新版本的vue-router中,重复点击同一个路由会出现以下报错:

方案1、vue-router降级处理(但不推荐)

npm i vue-router@3.0.7

方案2、直接在push方法最后添加异常捕获,例如:

<van-search v-model="SearchVal" shape="round" placeholder="请输入搜索关键词" disabled @click="$router.push('/home/searchPopup').catch(err=>{})"/>

方案3、直接修改原型方法push(推荐)

// 把这段代码直接粘贴到router/index.js中的Vue.use(VueRouter)之前
const originalPush = VueRouter.prototype.push;
VueRouter.prototype.push = function(location) {return originalPush.call(this, location).catch(err => {})
};

7、路由拦截/路由守卫

文档地址: https://router.vuejs.org/zh/guide/advanced/navigation-guards.html#%E5%85%A8%E5%B1%80%E5%89%8D%E7%BD%AE%E5%AE%88%E5%8D%AB

路由拦截(导航守卫:前置导航守卫和后置导航守卫)
前置导航守卫有三个参数
to: 表示即将进入的路由
from: 表示即将离开的路由
next() :表示执行进入这个路由

router.beforeEach((to, from, next)=>{// 有token就表示已经登录// 想要进入购物车页面,必须有登录标识token// console.log('to:', to)// console.log('from:', from)let token = localStorage.getItem('token')if(to.path=='/cart'){// 此时必须要有tokenif(token){next(); // next()去到to所对应的路由界面}else{Vue.prototype.$toast('请先登录');// 定时器setTimeout(()=>{next("/user");  // 强制去到"/user"所对应的路由界面}, 1000);}return;}// 如果不是去往购物车的路由,则直接通过守卫,去到to所对应的路由界面next()
})

详情页

1.在http.js 文件中定义详情页请求接口

//3.详情页  ProductDetail
// 产品详情
export function GoodsDetailApi(params) {return instance({url: '/goods/detail',method: 'get',params})
}
//详情页相关产品
export function GetGoodsRelatedData(params) {return instance({url: '/goods/related',method: 'get',params})
}
//获取商品数量
export function GetCartNum(params) {return instance({url: '/cart/goodscount',method: 'get',params})
}
// 添加到购物车
export function AddToCart(params) {return instance({url: '/cart/add',method: 'post',data: params})
}

2.router/index.js

在components 目录下创建ProductDetail.vue (详情页组件),并在路由中配置( 一级路由)

{path: '/productDetail', //产品详情name: 'ProductDetail',component: () => import('@/views/ProductDetail')
}

3.components /ProductDetail.vue

<template><div class="product-detail-box"><van-swipe :autoplay="3000"><van-swipe-item v-for="item in gallery" :key="item.id"><img :src="item.img_url" /></van-swipe-item></van-swipe><div class="info" v-if="info.name"><p class="info-name">{{ info.name }}</p><p class="info-brief">{{ info.goods_brief }}</p><p class="info-price">{{ info.retail_price | moneyFlrmat }}</p></div><div class="attribute"><div class="mytitle"><span></span><h3>商品参数</h3></div><ul><li v-for="item in attribute" :key="item.id"><span class="attribute-name">{{ item.name }}</span><span class="attribute-value">{{ item.value }}</span></li></ul></div><!-- 产品详情 --><div class="mytitle"><span></span><h3>产品详情</h3></div><!-- 产品描述信息 --><div class="goods_desc" v-html="info.goods_desc"></div><div class="mytitle"><span></span><h3>常见问题</h3></div><!-- 常见问题 --><ul class="issue"><li v-for="item in issue" :key="item.id"><h3>{{ item.question }}</h3><p>{{ item.answer }}</p></li></ul><!-- 产品列表 --><Weekproduct :newGoodsList="goodsList" title="相关产品"> </Weekproduct><!-- 添加购物车面板 --><van-skuv-model="show"ref="sku":sku="sku":goods="goods":hide-stock="sku.hide_stock"@add-cart="onAddCartClicked"/><!-- 下方购物车      --><van-goods-action><van-goods-action-icon:icon="star_flag ? 'star' : 'star-o'":text="star_flag ? '已收藏' : '未收藏'":color="star_flag ? '#ff5000' : '#323233'"@click="clickFn"/><van-goods-action-iconicon="cart-o"text="购物车":badge="badge"@click="$router.push('/cart')"/><van-goods-action-buttontype="warning"text="加入购物车"@click="addCar"/><van-goods-action-button type="danger" text="立即购买" /></van-goods-action></div>
</template><script>
import { getDetailData, AddToCart, GetGoodsRelatedData,GetCartCountData } from "@/https/http";
import Weekproduct from "@/views/home/Weekproduct";export default {name: "product-detail",data() {return {gallery: [],info: {},attribute: [], //参数show: false,sku: {tree: [], //规格类目 颜色 尺寸 。。。price: "", // 默认价格(单位元)stock_num: 227, // 商品总库存// 数据结构见下方文档hide_stock: false, //是否隐藏剩余库存},goods: {// 默认商品 sku 缩略图picture: ''},productList: [], // 当前产品信息issue: [],goodsList: [],star_flag: false,badge:0,};},created() {this.GetDetailData()this.getRelatedData()this.getCartData()},methods: {addCar() {this.show = true},// 加入购物车onAddCartClicked() {console.log(666,this.$refs.sku.getSkuData());let obj = {}obj.goodsId = this.$route.query.idobj.productId = this.productList[0].idobj.number = this.$refs.sku.getSkuData().selectedNumAddToCart(obj).then((res) => {console.log(res);// 显示添加成功this.$toast.success("添加成功");this.getCartData()})// 隐藏 商品规格面板this.show = false;},// 获取产品明细数据列表GetDetailData() {getDetailData({ id: this.$route.query.id }).then((res) => {console.log(33, res);this.gallery = res.data.gallery;this.info = res.data.info;this.productList = res.data.productList;this.attribute = res.data.attribute;this.issue = res.data.issue;this.goods.picture = res.data.info.list_pic_urlthis.sku.price = res.data.info.retail_pricethis.sku.stock_num = res.data.info.goods_number});},// 获取相关产品数据列表getRelatedData() {GetGoodsRelatedData({ id: this.$route.query.id }).then((res) => {console.log(3366, res);this.goodsList = res.data.goodsList});},// 获取购物车商品数量getCartData(){GetCartCountData().then((res)=>{console.log(7778,res);this.badge = res.data.cartTotal.goodsCount})},clickFn() {this.star_flag = !this.star_flagif (this.star_flag) {this.$toast('收藏成功')} else {this.$toast('取消宝贝收藏成功')}}},components: {Weekproduct}
};
</script><style lang="less" scoped>
.product-detail-box {font-size: 14px;line-height: 30px;padding-bottom: 100px;img {width: 100%;}.info {text-align: center;.info-brief {color: #666;}.info-price {color: red;}}.attribute {ul {li {border-bottom: 1px solid #eee;font-size: 12px;display: flex;.attribute-name {width: 15%;}.attribute-value {flex: 1;}}}}.mytitle {text-align: center;font-size: 16px;margin-top: 20px;position: relative;height: 50px;span {width: 50%;height: 2px;background-color: #ccc;display: inline-block;position: absolute;left: 50%;top: 50%;transform: translate(-50%, -50%);}h3 {width: 30%;background-color: #fff;position: absolute;left: 50%;top: 50%;transform: translate(-50%, -50%);}}.issue {li {h3 {padding-left: 10px;line-height: 20px;position: relative;&:before {content: "";width: 4px;height: 4px;border-radius: 50%;background-color: red;display: inline-block;position: absolute;left: 2px;top: 50%;margin-top: -2px;}}margin-bottom: 15px;}}/deep/.goods_desc {img {width: 100%;}}
}
</style>

专题页

1.在http.js文件中定义接口请求:

//5. 专题页 Topic
//专题请求
export function GetTopicApi(params) {return instance({url: '/topic/list',method: 'get',params})
}

2.Topic.vue 组件

<!-- 专题页 -->
<template><div class="zhuanti"><div class="box" v-for="item in data" :key="item.id"><img :src="item.scene_pic_url" alt="" /><div class="title">{{ item.title }}</div><div class="tip">{{ item.subtitle }}</div><div class="price">{{ item.price_info | moneyFlrmat }}</div></div><!-- 分页器 --><van-paginationv-model="currentPage":page-count="totalPages"mode="simple"@change="ChangeFn"/></div>
</template><script>
import { getTopicList } from "@/https/http.js";export default {data() {return {currentPage: 1, //当前页pageSize: 10, // 每页的条数data: [], //数据totalPages: "2", //总页数};},methods: {getPage() {getTopicList({page: this.currentPage,size: this.pageSize,}).then((res) => {console.log("res555", this.currentPage);console.log("res555", res);let { count, currentPage, data, pageSize, totalPages } = res.data;this.currentPage = currentPage; //当前页this.data = data; //数据this.totalPages = totalPages; //总页数this.pageSize = pageSize; // 每页的条数// 返回顶部document.documentElement.scrollTop = 0;});},ChangeFn() {// 会直接改变currentPageconsole.log(this.currentPage);this.getPage();},},created() {this.getPage();},
};
</script>
<style lang="less" scoped>
/deep/.van-pagination__page-desc {display: none;
}
.zhuanti {padding-bottom: 100px;box-sizing: border-box;.box {width: 100%;font-size: 14px;line-height: 40px;text-align: center;img {width: 100%;}.title {font-size: 18px;}.price {color: red;}}
}
</style>

分类页

点击左侧导航,更换数据

1.在http.js 文件中,定义接口请求

//6. 分类页 Category
// 全部分类数据接口
export function GetChannelDataApi(params) {return instance({url: '/catalog/index',method: 'get',params})
}
// 获取当前分类数据
export function GetFenleiDataApi(params) {return instance({url: '/catalog/current',method: 'get',params})
}

2.Category.vue 组件

<!-- 分类页 -->
<template><div class="category-box"><!--搜索框 --><van-search v-model="value" show-action placeholder="请输入搜索关键词" /><div class="fenlei"><!-- 左侧导航 --><van-sidebar v-model="activeKey" @change="onChange"><van-sidebar-item:title="item.name"v-for="item in categoryList":key="item.id"/></van-sidebar><!-- 右侧主体 --><main><!-- 上方图片 --><div class="pic-area"><img :src="currentCategory.banner_url" alt="" /><p class="desc">{{ currentCategory.front_desc }}</p></div><!-- 标题 --><div class="mytitle"><span></span><h3>{{ currentCategory.name }}</h3></div><!-- 图文混排 --><van-grid :column-num="3" ><van-grid-itemv-for="item in subCategoryList":key="item.id":icon="item.wap_banner_url":text="item.name"/></van-grid></main></div></div>
</template><script>
import { GetChannelDataApi, GetFenleiDataApi } from "@/https/http";export default {data() {return {activeKey: 0,value: "",categoryList: [], //导航数据currentCategory: {}, //选中的类别数据,currentId: "0",  subCategoryList:[]  //子类数组};},methods: {// 左侧导航被点击(index为选中的类别的索引值),更换类别onChange(index) {this.activeKey = index;this.currentCategory =this.categoryList[this.activeKey]  this.currentId = this.categoryList[this.activeKey].id;  //选中的类别的id// 获取当前分类数据this.GetCurrentCategory()},// 获取全部分类数据GetcategoryList() {GetChannelDataApi().then((res) => {// console.log("res1", res);this.categoryList = res.data.categoryList;  //左侧导航数据//选中的类别的id,默认第一个类别被选中this.currentId = this.categoryList[0].id;  // 当前显示的类别数据,图片和标题使用this.currentCategory = res.data.currentCategory;  //当前显示的类别数据 图文混排区域使用this.subCategoryList = res.data.currentCategory.subCategoryList;  });},// 获取当前分类数据GetCurrentCategory() {GetFenleiDataApi({ id: this.currentId }).then((res) => {// console.log("res12", res);// 当前显示的类别数据,图片和标题使用this.currentCategory = res.data.currentCategory;  //当前显示的类别数据 图文混排区域使用this.subCategoryList = res.data.currentCategory.subCategoryList;});},},created() {this.GetcategoryList();  // 获取全部分类数据}
};
</script>
<style scoped lang="less">
/* @import url(); 引入css类 */
.fenlei {display: flex;main {flex: 1;.pic-area {text-align: center;position: relative;height: 100px;font-size: 15px;img {width: 98%;border-radius: 5px;display: block;}.desc {position: absolute;left: 50%;top: 50%;transform: translate(-50%, -50%);}}.mytitle {text-align: center;font-size: 16px;margin-top: 20px;position: relative;height: 50px;span {width: 50%;height: 2px;background-color: #ccc;display: inline-block;position: absolute;left: 50%;top: 50%;transform: translate(-50%, -50%);}h3 {width: 30%;background-color: #fff;position: absolute;left: 50%;top: 50%;transform: translate(-50%, -50%);}}}
}
</style>

购物车页

1.在http.js 文件中定义接口

//7.购物车页 Cart
// 购物车列表
export function GetCartData(params) {return instance({url: '/cart/index',method: 'get',params})
}

2.Cart.vue

在views目录下,.Cart.vue新建 组件,代码如下:

<!-- 购物车页 -->
<template><div class="cart-box"><div v-for="item in cartList" :key="item.id" class="cart-item"><!-- 每个商品前的按钮 --><van-checkbox:name="item"@click="onchxClickFn(item)"class="checkbox-btn"v-model="item.checked"></van-checkbox><!-- 商品信息 --><van-card :price="item.retail_price" :thumb="item.list_pic_url"><template #num><van-stepperv-model="item.number"@change="onChange(item.number, item.id)"/></template><!-- 自定义标题,删除按钮 --><template #title><span>{{ item.goods_name }}</span><van-iconname="delete-o"class="delete-icon"@click="onDelete(item)"/></template></van-card></div><!-- 按钮 --><!-- 下方结算 --><!-- vant显示的数字不对,9999元会显示成99.99元,所以需要乘以100 --><van-submit-bar:price="checkedGoodsAmount * 100"button-text="提交订单"@submit="onSubmit"><van-checkbox @click="onClickCheckAll" v-model="checkedAll">全选</van-checkbox><template #tip>你的收货地址不支持同城送,<span @click="onClickEditAddress">修改地址</span></template></van-submit-bar></div>
</template><script>
import {GetCartData, UpdateCartData, DeleteCartData,ToggleCartCheckedData, DeleteCartData2
} from "@/https/http";export default {name: "cart",data() {return {cartList: [], //商品总列表cartTotal: {}, //购物车数据// price: 0,goodsId: '',number: '',productId: '',id_: '',isChecked: '1',// productIdsList:[],productIds: '',checkedGoodsAmount: 0,  //选中的商品的总金额checkedAll: 0,};},methods: {// 获取数据getData() {// 发送请求,获取当前购物车的数据GetCartData().then((res) => {console.log(11111, res);this.cartList = res.data.cartList; //商品总列表this.cartTotal = res.data.cartTotal;  //购物车数据//选中的商品的总金额this.checkedGoodsAmount = res.data.cartTotal.checkedGoodsAmount // 如果有选中的商品if (this.cartTotal.checkedGoodsCount > 0) {// 选中的商品数量===购物车内的所有商品总数量 时候,全选按钮就会被选中if (this.cartTotal.checkedGoodsCount == this.cartTotal.goodsCount) {this.checkedAll = true} else {  //不相等的时候,全选按钮就不会被选中this.checkedAll = false}} else { // 如果没有选中的商品,全选按钮就不会被选中this.checkedAll = false}});},// 删除单个商品的时候,发送删除商品的请求onDelete(item) {DeleteCartData2({ productIds: item.product_id.toString() }).then((res) => {if (res.errno === 0) {this.getData()  //重新请求购物车商品数据,渲染}})},//  按下商品+1或者-1按钮, 购物车商品数量变化 ,onChange会接收变化的商品idonChange(value, id_) {this.cartList.forEach(item => {// 找出对应的goods_id,numberif (item.id === id_) {this.id_ = id_this.goodsId = item.goods_idthis.number = item.numberthis.productId = item.product_id}})// 发请求this.updateCartData()},// 购物车商品步进器功能接口  按下商品+1或者-1按钮,updateCartData() {// 直接发送更新数据请求,将当前的商品数量带着UpdateCartData({goodsId: this.goodsId, id: this.id_,number: this.number, productId: this.productId}).then((res) => {console.log(999, res);if (res.errno === 0) {this.getData() //重新请求购物车商品数据,渲染}})},// 点击商品单选按钮,切换购物车商品选中状态,发送请求onchxClickFn(item) {this.isChecked = item.checked ? '1' : '0'this.productIds = item.product_id.toString()this.toggleCartCheckedData()},// 切换购物车商品选中状态,发送请求toggleCartCheckedData() {console.log(this.isChecked);ToggleCartCheckedData({isChecked: this.isChecked,productIds: this.productIds}).then((res) => {console.log(667, res);if (res.errno === 0) {this.getData() //重新请求购物车商品数据,渲染}})},// 点击全选,切换购物车商品选中状态,发送请求onClickCheckAll() {this.isChecked = this.checkedAll ? '1' : '0'let productIdAllList = []this.cartList.forEach((item) => {productIdAllList.push(item.product_id.toString())})this.productIds = productIdAllList.join(',')this.toggleCartCheckedData()},// 提交onSubmit() { },// 编辑地址onClickEditAddress() { },},created() {this.getData();},
};
</script>
<style scoped lang="less">
/deep/.van-checkbox__label {flex: 1;
}
/deep/.van-checkbox {margin-bottom: 2px;
}
/deep/.van-submit-bar {bottom: 50px;
}
.cart-box {padding-bottom: 150px;box-sizing: border-box;.van-card {position: relative;}.delete-icon {position: absolute;top: 5px;right: 5px;}.cart-item {position: relative;padding-left: 40px;.checkbox-btn {position: absolute;left: 20px;top: 50%;transform: translate(-50%, -50%);}}
}
</style>

发送获取购物车数据列表时的响应数据

购物车商品步进器功能接口

切换购物车商品选中状态功能接口(含全选)响应数据

我的页

1.在http.js文件中定义接口请求

//登陆
export function GoLogin(params) {return instance({url: '/auth/loginByWeb',method: 'post',data: params})
}

2.User.vue

在views 目录下,新建User.vue 组件,代码如下:

<!-- 我的 -->
<template><div class="user-box"><div class="user-top"><img :src="avatarSrc" alt="" /><!-- 如果登陆了,就显示用户名,否则显示立即登录 --><h3 v-if="ifLogined">{{ username }}</h3><!-- 点击登录,显示模态框 --><h3 @click="ljdl" v-else>点击登录</h3><van-icon :name="ifLogined ? 'cross' : 'arrow'" @click="loginout" /></div><!-- 九宫格部分 --><van-grid :column-num="3"><van-grid-itemv-for="item in gridArr":key="item.id":icon="item.icon":text="item.type"/></van-grid><!-- 模态框 --><div class="modal" v-if="ifShowModal"><div class="modal-bg" @click="ifShowModal = false"></div><div class="modal-content"><van-form @submit="onSubmit"><van-fieldv-model="username"name="用户名"label="用户名"placeholder="用户名":rules="[{ required: true, message: '请填写用户名' }]"/><van-fieldv-model="pwd"type="password"name="密码"label="密码"placeholder="密码":rules="[{ required: true, message: '请填写密码' }]"/><div style="margin: 16px"><van-button round block type="danger" native-type="submit">提交</van-button></div></van-form></div></div></div>
</template><script>
// 引入登录接口
import { GoLogin } from "@/https/http";
import headImg from "@/assets/images/touxiang.png";  //默认头像export default {name: "user",data() {return {username: "",pwd: "",avatarSrc: headImg,  //头像ifLogined: false, // 登录状态ifShowModal: false, // 是否显示模态框gridArr: [// grid数组{ id: 0, icon: "label-o", type: "我的订单" },{ id: 1, icon: "bill-o", type: "优惠券" },{ id: 2, icon: "goods-collect-o", type: "礼品卡" },{ id: 3, icon: "location-o", type: "我的收藏" },{ id: 4, icon: "flag-o", type: "我的足迹" },{ id: 5, icon: "contact", type: "会员福利" },{ id: 6, icon: "aim", type: "地址管理" },{ id: 7, icon: "warn-o", type: "账号安全" },{ id: 8, icon: "service-o", type: "联系客服" },{ id: 9, icon: "question-o", type: "帮助中心" },{ id: 10, icon: "smile-comment-o", type: "意见反馈" },],};},created() {// 登陆前先看本人是否登陆过let user = JSON.parse(localStorage.getItem("userInfo"));// 用户名存在if (user) {this.username = user.username;  //用户名this.avatarSrc = user.avatar; //头像this.ifLogined = true; // 显示用户名}},methods: {// 点击立即登录,显示登录模态框ljdl() {this.ifShowModal = true;   },// 提交用户名,密码信息onSubmit() {this.getloginData(); //发送数据请求},// 发送数据请求:登录注册getloginData() {GoLogin({ username: this.username, pwd: this.pwd }).then((res) => {console.log(res);if (res.errno === 0) {console.log("登录成功");this.$toast.success("登录成功");localStorage.setItem("token", res.data.token);localStorage.setItem("userInfo", JSON.stringify(res.data.userInfo));this.ifShowModal = false; //不显示模态框this.ifLogined = true; // 显示用户名this.avatarSrc = res.data.userInfo.avatar; //头像this.username = res.data.userInfo.username;}});},// 退出登录loginout() {// 登录了if (this.ifLogined) {this.$dialog.confirm({title: "退出登录",message: "是否退出登录",}).then(() => {// on confirmthis.ifLogined = false; // 不显示用户名this.avatarSrc = headImg; //头像// 清除tokenlocalStorage.removeItem("token");localStorage.removeItem("userInfo");// 刷新当前页this.$router.go(0);// 刷新当前页this.$router.go(0);}).catch(() => {// on cancel});}},},
};
</script>
<style lang="less" scoped>
.van-grid-item {padding: 20px;
}
.user-box {.user-top {display: flex;align-items: center;font-size: 16px;padding: 20px 10px;box-sizing: border-box;background-color: #333;color: white;img {width: 70px;height: 70px;margin-right: 10px;border-radius: 50%;}h3 {flex: 1;}}.modal {width: 100%;height: 100%;position: fixed; //position: fixed让height:100%起作用left: 0;top: 0;.modal-bg {width: 100%;height: 100%;background-color: rgba(0, 0, 0, 0.5);}.modal-content {width: 90%;height: 200px;box-sizing: border-box;// height: 200px;background-color: #fff;padding: 20px;position: absolute;left: 50%;top: 50%;transform: translate(-50%, -50%);z-index: 100;}}
}
</style>

路由守卫

在router 目录下的index.js 文件中,设置路由前置守卫,代码如下,用来判断购物车页面只能在用户登录的情况下才能查看。


// 路由前置守卫
router.beforeEach((to, from, next) => {// 有token就表示已经登录// 想要进入购物车页面,必须有登录标识token// console.log('to:', to)// console.log('from:', from)let token = localStorage.getItem('token')if (to.path == '/cart') {// 此时必须要有tokenif (token) {next(); // next()去到to所对应的路由界面} else {Vue.prototype.$toast('请先登录');// 定时器setTimeout(() => {next("/user");  // 强制去到"/user"所对应的路由界面}, 1000);}} else {// 如果不是去往购物车的路由,则直接通过守卫,去到to所对应的路由界面next()}
})

项目中的bug

1、解决刷新页面,底部tabbar显示错题。

  computed:{active:{get(){console.log(this.$route.path)const path = this.$route.pathswitch(path){case '/home':return 0;case '/topic':return 1;case '/category':return 2;case '/cart':return 3;case '/user':return 4;default:return 0}},set(){}}}

2.编程式导航在跳转到与当前地址一致的URL时会报错,但这个报错不影响功能:

// 该段代码不需要记,理解即可
const originalPush = VueRouter.prototype.push;
VueRouter.prototype.push = function push(location) {return originalPush.call(this, location).catch((err) => err);
};

3.用户页引入头像

直接在标签中引入相对路径图片地址,图片不显示,需要使用如下模块式引入方式。

// import 方式
import headImg from "../assets/touxiang.png";// require 方式
let headImg = require("../assets/touxiang.png")

项目优化—路由懒加载

当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。

 {path: '/home',//首页name: 'Home',component: () => import('@/views/Home'),meta: { // 用来判断该组件对应的页面是否显示底部tabbarisShowTabbar: true}},

[vue移动端项目] 严选商城项目 使用vue+vant做的移动端商城小项目相关推荐

  1. 有什么好赚钱的项目可以做?这8个小项目不错,想赚钱的别错过!

    我想想这疫情几年,大家应该都是挺难的. 上班的上上停停,开店的关关开开,但是成本一点没有减少. 但是,我想就算这样的情况下还是有很多赚钱的. 其实,这个和行业有关系, 选择大于努力, 有些成本低,或者 ...

  2. 基于stm32的简单小项目_适合小白做的创业3个小项目,简单上手

    现在每一位为了能够让自己和家人过上更好地生活,都会不满足于现状的单方面地工作,想要通过创业来改变如今的局面,这种想法也是好的,但是我们需要知道想要创业,并不是那么的容易,需要有一定的技能和管理水平才能 ...

  3. 【项目实战】用 Java 写了一个类QQ界面聊天小项目,可在线聊天!

    1.功能实现 1.修改功能(密码.昵称.个性签名) 2.添加好友.删除好友 3.单聊功能 4.判断好友是否在线 2.模块划分 == 3.使用的知识 netty swing 集合等同步阻塞队列synch ...

  4. java怎么做简易的游戏,Java小项目之《简易桌面小游戏》

    注意小球的运动范围,规定向右为正,如果不限定范围小球可能会跑出银河系-所以限定小球碰到左面左右回弹 画完窗口后运行若还是一片空白可以先最大化然后最小化解决 代码目录 实现代码 import java. ...

  5. 【手把手带你Godot游戏开发】出道即巅峰,先做个盈利千万的小项目

    引言 你没看错,就是这个!FlappyBird! 曾经的网红,2014年前后由越南开发者阮哈东用不到一周时间开发的免费游戏,5000万的下载量,凭一条Banner广告,日入50000美金,一个热门手游 ...

  6. 0301 - 一个比价的小项目

    这两天帮朋友做了个 比价 的小项目,主要是为了练手 Vue 及相关网站开发. 主要功能: 批量查询产品对应的京东价格 手动根据京东价格调整批发价格 将产品及价格信息,以网页形式分享出去 由于是私人项目 ...

  7. android简单小项目实例_自学(系统学)Python了那么久, 想就业? 几个简单小项目让你通过面试!...

    本人从事Python开发多年,精通爬虫,web,熟悉其他方向,好多小伙伴私聊我说,我看了你的文章后,我学习了,但是对于项目这块还是不是很熟悉,如何快速掌握几个小项目,以及几个可以面试的项目.欢迎大家订 ...

  8. 小项目也可以学习到很多东西

    今天分享关于最近做的一个小项目学习一些感悟. 1.小项目也能学习到非常多的知识 最近做的是一个数据整理的一个项目,后台系统需要一些统计的数据,如果从原有的系统中查询,会大大影响到其他端的性能,而且相对 ...

  9. python小项目案例-Python小项目:快速开发出一个简单的学生管理系统

    本文根据实际项目中的一部分api 设计抽象出来,实例化成一个简单小例子,暂且叫作「学生管理系统」. 这个系统主要完成下面增删改查的功能: 包括: 学校信息的管理 教师信息的管理 学生信息的管理 根据A ...

最新文章

  1. 如何在java中调用js方法
  2. Erlang China 大会 - CN Erlounge III - 发起
  3. 2017-2018-1 JAVA实验站 第三周作业
  4. Linux文件属性2——使用stat函数获取文件属性
  5. STAR原则是什么?
  6. Window 错误代码大全
  7. dell笔记本外接显示器_戴尔笔记本怎样外接显示器
  8. Linux编辑grldr文件,使用syslinux完美引导GRLDR。
  9. 两道非常容易理解错的OSPF问题. 加深理解LSA的概念和ABR/ASBR的概念.
  10. python怎么建立项目经理部的基本原则_一个关于项目经理的故事
  11. python之panda模块1
  12. 第五章 光学系统中成像光束的选择
  13. 如何个性化U盘或硬盘图标
  14. 马蜂窝定制游抢单系统设计与功能核心
  15. axure导出html css,通过 Coding 托管 Axure HTML 文件_html/css_WEB-ITnose
  16. js实现提取textarea文本信息,然后进行处理
  17. 高德地图——申请Key
  18. Java实现 蓝桥杯VIP 算法提高 淘淘的名单
  19. 超详细教程——Ubuntu20.04 安装英伟达NVIDIA显卡驱动、CUDA、Cmake以及不同版本的CUDA切换
  20. 2023最新SSM计算机毕业设计选题大全(附源码+LW)之java流浪动物救助平台bgo9b

热门文章

  1. linux常用的基础指令
  2. 《量子物理史话》主线 - 光是什么?(未完)
  3. Understanding congested travel in urban areas
  4. 雷达实战之射频前端配置说明
  5. Android开发中如何设置字体
  6. 使用OZMTool制作 ozmosis BIOS
  7. vue中阻止事件冒泡
  8. LaTex使用笔记(转载)
  9. 【玩转Redis面试第4讲】Redis缓存雪崩、缓存穿透、缓存击穿对比看这一篇就够了
  10. G450/G550蓝牙简单改造以及18CN46WW V2.55去白名单BIOS