[vue移动端项目] 严选商城项目 使用vue+vant做的移动端商城小项目
文章目录项目准备安装 amfe-flexible安装 第三方插件postcss-pxtorem配置vue.config.jsreset.css重置样式表安装less 预编译语言安装vant-ui端口配置(可以不做)axios 工具封装下载安装axiosutils/request.jshttps/http.js跨域代理配置 vue.config.js项目路由规划配置下载vue-router在inde
文章目录
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.5
rootValue: 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: { // 用来判断该组件对应的页面是否显示底部tabbar
isShowTabbar: true
}
},
{
path: '/topic',//专题
name: 'Topic',
component: () => import('@/views/Topic'),
meta: { // 用来判断该组件对应的页面是否显示底部tabbar
isShowTabbar: true
}
},
{
path: '/category',//分类
name: 'Category',
component: () => import('@/views/Category'),
meta: { // 用来判断该组件对应的页面是否显示底部tabbar
isShowTabbar: true
}
},
{
path: '/cart',//购物车
name: 'Cart',
component: () => import('@/views/Cart'),
meta: { // 用来判断该组件对应的页面是否显示底部tabbar
isShowTabbar: true
}
},
{
path: '/user',//我的
name: 'User',
component: () => import('@/views/User'),
meta: { // 用来判断该组件对应的页面是否显示底部tabbar
isShowTabbar: 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-tabbar
v-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-search
shape="round"
v-model="value"
show-action
placeholder="请输入搜索关键词"
@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 -->
<Weekproduct
v-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-item
v-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", // 当前类别的id
page: 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>
<li
v-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-item
v-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: { // 用来判断该组件对应的页面是否显示底部tabbar
isShowTabbar: 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-search
v-model="value"
show-action
:placeholder="placeholderVal"
@search="onSearch"
@cancel="onCancel"
@input="onInput"
/>
<!-- 历史记录和热门搜索 组件 -->
<HistoryHot
v-if="blockShow == 1"
:searchHistoryData="searchHistoryData"
:searchHotData="searchHotData"
@goSearch="setValue"
></HistoryHot>
<!-- 搜索提示列表 组件-->
<SearchTipsList
v-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或price
goodsList: [], // 搜索出来的商品列表
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, //分类排序id
sort: 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
// 点击搜索列表提示触发 也触发setValue
console.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-tag
plain
type="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-tag
plain
: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-list
v-model="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<van-cell
v-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-item
v-model="value2"
:options="option2"
title="价格排序"
@change="priceChange"
/>
<van-dropdown-item
v-model="value3"
:options="filterCategory"
title="分类"
@change="categoryChange"
/>
</van-dropdown-menu>
<!-- 如有没有搜索到商品显示 empty-->
<van-empty
v-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、路由拦截/路由守卫
路由拦截(导航守卫:前置导航守卫和后置导航守卫)
前置导航守卫有三个参数
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'){
// 此时必须要有token
if(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-sku
v-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-icon
icon="cart-o"
text="购物车"
:badge="badge"
@click="$router.push('/cart')"
/>
<van-goods-action-button
type="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.id
obj.productId = this.productList[0].id
obj.number = this.$refs.sku.getSkuData().selectedNum
AddToCart(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_url
this.sku.price = res.data.info.retail_price
this.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_flag
if (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-pagination
v-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() {
// 会直接改变currentPage
console.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-item
v-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-stepper
v-model="item.number"
@change="onChange(item.number, item.id)"
/>
</template>
<!-- 自定义标题,删除按钮 -->
<template #title>
<span>{{ item.goods_name }}</span>
<van-icon
name="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会接收变化的商品id
onChange(value, id_) {
this.cartList.forEach(item => {
// 找出对应的goods_id,number
if (item.id === id_) {
this.id_ = id_
this.goodsId = item.goods_id
this.number = item.number
this.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-item
v-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-field
v-model="username"
name="用户名"
label="用户名"
placeholder="用户名"
:rules="[{ required: true, message: '请填写用户名' }]"
/>
<van-field
v-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 confirm
this.ifLogined = false; // 不显示用户名
this.avatarSrc = headImg; //头像
// 清除token
localStorage.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') {
// 此时必须要有token
if (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.path
switch(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: { // 用来判断该组件对应的页面是否显示底部tabbar
isShowTabbar: true
}
},
更多推荐
所有评论(0)