文章目录


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文件中编写项目路由基础配置

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

image-20211010230903251

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>

项目目录结构

image-20211017215140846

在这里插入图片描述

定义所有的接口请求 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、路由拦截/路由守卫

文档地址: 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'){
    // 此时必须要有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
     }
 },
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐