{ return requests({ _vue笔记【尚品汇项目">
赞
踩
https://blog.csdn.net/weixin_43424325/article/details/121684101
api/index.js 设定
//当前模块,API进行统一管理,即对请求接口统一管理
import requests from "@/api/request";
//首页三级分类接口
export const reqgetCategoryList = () => {
return requests({
url: '/product/getBaseCategoryList',
method: 'GET'
})
}
api/request.js 配置请求拦截器并开启进度条
import axios from "axios"; //引入进度条 import nprogress from 'nprogress'; //引入进度条样式 import "nprogress/nprogress.css"; //1、对axios二次封装 const requests = axios.create({ //基础路径,requests发出的请求在端口号后面会跟改baseURl baseURL:'/api', timeout: 5000, }) //2、配置请求拦截器 requests.interceptors.request.use(config => { //config内主要是对请求头Header配置 //比如添加token //开启进度条 nprogress.start(); return config; }) //3、配置相应拦截器 requests.interceptors.response.use((res) => { //成功的回调函数 //响应成功,关闭进度条 nprogress.done() return res.data; },(error) => { //失败的回调函数 console.log("响应失败"+error) return Promise.reject(new Error('fail')) }) //4、对外暴露 export default requests;
main.js 测试api接口
import Vue from 'vue' import App from './App.vue' import VueRouter from 'vue-router' import router from './router' import TypeNav from './views/Home/TypeNav' import store from './store' Vue.config.productionTip = false Vue.use(VueRouter) //第一个参数:全局组件名字,第二个参数:全局组件 Vue.component(TypeNav.name,TypeNav) //测试:发起请求 import {reqgetCategoryList} from './api' reqgetCategoryList(); new Vue({ render: h => h(App), router, store }).$mount('#app')
TypeNav/index.vue 读取vuex数据
<script> import { mapState } from "vuex"; export default { name: 'TypeNav', mounted(){ this.$store.dispatch('getCategoryList') }, computed: { //state:他是咱们大仓库中的state(包含home|search) ...mapState({ categoryList: (state) => state.home.categoryList, }), }, } </script>
api/index.js 设定请求api
//当前模块,API进行统一管理,即对请求接口统一管理
import requests from "@/api/request";
//首页三级分类接口
export const reqgetCategoryList = () => {
return requests({
url: '/product/getBaseCategoryList',
method: 'GET'
})
}
store/homes.js设定三连环
import { reqgetCategoryList, } from "@/api"; //home模块的仓库 const state = { //home仓库中存储三级菜单的数据 categoryList: [], }; //mutions是唯一修改state的地方 const mutations = { GETCATEGORYLIST(state, categoryList) { state.categoryList = categoryList; }, }; //action|用户处理派发action地方的,可以书写异步语句、自己逻辑地方 const actions = { async getCategoryList({ commit }) { //reqgetCategoryList返回的是一个Promise对象 //需要用await接受成功返回的结果,await必须要结合async一起使用(CP) let result = await reqgetCategoryList(); if (result.code == 200) { commit("GETCATEGORYLIST", result.data); } }, }; //计算属性 const getters = {}; export default { state, mutations, actions, getters, };
TypeNav/index.vue 设定c1,c2,c3动态商品分类导航
<template> <!-- 商品分类导航 --> <div class="type-nav"> <div class="container"> <h2 class="all">全部商品分类</h2> <nav class="nav"> <a href="###">服装城</a> <a href="###">美妆馆</a> <a href="###">尚品汇超市</a> <a href="###">全球购</a> <a href="###">闪购</a> <a href="###">团购</a> <a href="###">有趣</a> <a href="###">秒杀</a> </nav> <div class="sort"> <div class="all-sort-list2" > <div class="item" v-for="(c1,index) in categoryList" :key="c1.categoryId"> <h3> <a href="">{{ c1.categoryName }}</a> </h3> <div class="item-list clearfix"> <div class="subitem" v-for="(c2,index) in c1.categoryChild" :key="c2.categoryId"> <dl class="fore"> <dt> <a href="">{{ c2.categoryName }}</a> </dt> <dd> <em v-for="(c3,index) in c2.categoryChild" :key="c3.categoryId"> <a href="">{{ c3.categoryName }}</a> </em> </dd> </dl> </div> </div> </div> </div> </div> </div> </div> </template> <script> import { mapState } from "vuex"; export default { name: 'TypeNav', mounted(){ this.$store.dispatch('getCategoryList') }, computed: { //state:他是咱们大仓库中的state(包含home|search) ...mapState({ categoryList: (state) => state.home.categoryList, }), }, } </script>
TypeNav/index.vue 设定index和currentIndex来启动cur class
<template> <h3 @mouseenter="changeIndex(index)" @mouseleave="leaveShow(index)" :class="{ cur: currentIndex == index }"> <a href="">{{ c1.categoryName }}</a> </h3> </template> <script> export default { name: 'TypeNav', data(){ return { currentIndex:-1, } }, mounted(){ this.$store.dispatch('getCategoryList') }, computed: { //state:他是咱们大仓库中的state(包含home|search) ...mapState({ categoryList: (state) => state.home.categoryList, }), }, methods: { changeIndex(index){ this.currentIndex = index }, leaveShow(index){ this.currentIndex = -1 } }, } </script> <style scoped lang="less"> .cur{ background: skyblue; } </style>
TypeNav/index.vue注释掉以下less后,用js来实现
&:hover {
.item-list {
display: block;
}
}
TypeNav/index.vue
<div class="item-list clearfix" :style="{display: currentIndex == index?'block':'none'}">
<div class="subitem" v-for="(c2,index) in c1.categoryChild" :key="c2.categoryId">
<dl class="fore">
<dt>
<a href="">{{ c2.categoryName }}</a>
</dt>
<dd>
<em v-for="(c3,index) in c2.categoryChild" :key="c3.categoryId">
<a href="">{{ c3.categoryName }}</a>
</em>
</dd>
</dl>
</div>
</div>
插件官网https://www.lodashjs.com/
npm i --save lodash
TypeNav/index.vue引用lodash的节流功能
<script> import { mapState } from "vuex"; //按需引入:只是引入节流函数,其他的函数没有引入(模块),这样做的好处是,当你打包项目的时候体积会小一些 import throttle from "lodash/throttle"; export default { name: 'TypeNav', data(){ return { currentIndex:-1, } }, mounted(){ ... }, computed: { ... }, methods: { // changeIndex(index){ // this.currentIndex = index // }, // 引入节流 changeIndex:throttle(function(index){ this.currentIndex = index },500), ... } </script>
方法1:TypeNav/index.vue使用<router-link to="/search">
直接跳转,但是生成组件较多,内存消耗较大。
<template> <!-- 商品分类导航 --> <div class="type-nav"> <div class="container" @mouseleave="leaveShow"> <h2 class="all">全部商品分类</h2> <nav class="nav"> <a href="###">服装城</a> <a href="###">美妆馆</a> <a href="###">尚品汇超市</a> <a href="###">全球购</a> <a href="###">闪购</a> <a href="###">团购</a> <a href="###">有趣</a> <a href="###">秒杀</a> </nav> <div class="sort"> <div class="all-sort-list2" > <div class="item" v-for="(c1,index) in categoryList" :key="c1.categoryId"> <h3 @mouseenter="changeIndex(index)" :class="{ cur: currentIndex == index }"> <router-link :to="{ name:'search', params:$route.params, query:{ categoryName:c1.categoryName, category1Id:c1.categoryId, } }" >{{ c1.categoryName }}</router-link> </h3> <div class="item-list clearfix" :style="{display: currentIndex == index?'block':'none'}"> <div class="subitem" v-for="(c2,index) in c1.categoryChild" :key="c2.categoryId"> <dl class="fore"> <dt> <router-link :to="{ name:'search', params:$route.params, query:{ categoryName:c2.categoryName, category2Id:c2.categoryId, } }" >{{ c2.categoryName }}</router-link> </dt> <dd> <em v-for="(c3,index) in c2.categoryChild" :key="c3.categoryId"> <router-link :to="{ name:'search', params:$route.params, query:{ categoryName:c3.categoryName, category3Id:c3.categoryId, } }" >{{ c3.categoryName }}</router-link> </em> </dd> </dl> </div> </div> </div> </div> </div> </div> </div> </template> <script> import { mapState } from "vuex"; //按需引入:只是引入节流函数,其他的函数没有引入(模块),这样做的好处是,当你打包项目的时候体积会小一些 import throttle from "lodash/throttle"; export default { name: 'TypeNav', data(){ return { currentIndex:-1, } }, mounted(){ this.$store.dispatch('getCategoryList') }, computed: { //state:他是咱们大仓库中的state(包含home|search) ...mapState({ categoryList: (state) => state.home.categoryList, }), }, methods: { // changeIndex(index){ // this.currentIndex = index // }, // 引入节流 changeIndex:throttle(function(index){ this.currentIndex = index },50), leaveShow(){ this.currentIndex = -1 }, }, } </script>
方法2:TypeNav/index.vue使用编程式导航,内存消耗小。
事件委派问题:
(1)如何确定我们点击的一定是a标签呢?如何保证我们只能通过点击a标签才跳转呢?
(2)如何获取子节点标签的商品名称和商品id(我们是通过商品名称和商品id进行页面跳转的)
解决方法:
对于问题1:为三个等级的a标签添加自定义属性date-categoryName绑定商品标签名称来标识a标签(其余的标签是没有该属性的)。
对于问题2:为三个等级的a标签再添加自定义属性data-category1Id、data-category2Id、data-category3Id来获取三个等级a标签的商品id,用于路由跳转。
TypeNav/index.vue
<template> <!-- 商品分类导航 --> <div class="type-nav"> <div class="container" @mouseleave="leaveShow"> <h2 class="all">全部商品分类</h2> <nav class="nav"> <a href="###">服装城</a> <a href="###">美妆馆</a> <a href="###">尚品汇超市</a> <a href="###">全球购</a> <a href="###">闪购</a> <a href="###">团购</a> <a href="###">有趣</a> <a href="###">秒杀</a> </nav> <div class="sort"> <div class="all-sort-list2" @click="goSearch"> <div class="item" v-for="(c1,index) in categoryList" :key="c1.categoryId"> <h3 @mouseenter="changeIndex(index)" :class="{ cur: currentIndex == index }"> <a :data-categoryName="c1.categoryName" :data-category1Id="c1.categoryId" >{{ c1.categoryName }}</a> </h3> <div class="item-list clearfix" :style="{display: currentIndex == index?'block':'none'}"> <div class="subitem" v-for="(c2,index) in c1.categoryChild" :key="c2.categoryId"> <dl class="fore"> <dt> <a :data-categoryName="c2.categoryName" :data-category2Id="c2.categoryId" >{{ c2.categoryName }}</a> </dt> <dd> <em v-for="(c3,index) in c2.categoryChild" :key="c3.categoryId"> <a :data-categoryName="c3.categoryName" :data-category3Id="c3.categoryId" >{{ c3.categoryName }}</a> </em> </dd> </dl> </div> </div> </div> </div> </div> </div> </div> </template> <script> import { mapState } from "vuex"; //按需引入:只是引入节流函数,其他的函数没有引入(模块),这样做的好处是,当你打包项目的时候体积会小一些 import throttle from "lodash/throttle"; export default { name: 'TypeNav', data(){ return { currentIndex:-1, } }, mounted(){ this.$store.dispatch('getCategoryList') }, computed: { //state:他是咱们大仓库中的state(包含home|search) ...mapState({ categoryList: (state) => state.home.categoryList, }), }, methods: { // changeIndex(index){ // this.currentIndex = index // }, // 引入节流 changeIndex:throttle(function(index){ this.currentIndex = index },50), leaveShow(){ this.currentIndex = -1 }, //进行路由跳转的回调函数 goSearch(event) { //event.target:获取到的是出发事件的元素(div、h3、a、em、dt、dl) let node = event.target; //给a标签添加自定义属性data-categoryName,全部的字标签当中只有a标签带有自定义属性,别的标签名字----dataset纯属扯淡 let { categoryname, category1id, category2id, category3id, } = node.dataset; console.log('dataset',node.dataset) //第二个问题解决了:点击的到底是不是a标签(只要这个标签身上带有categoryname)一定是a标签 //当前这个if语句:一定是a标签才会进入 if (categoryname) { //准备路由跳转的参数对象 let loction = { name: "search" }; let query = { categoryName: categoryname }; //一定是a标签:一级目录 if (category1id) { query.category1Id = category1id; //一定是a标签:二级目录 } else if (category2id) { query.category2Id = category2id; //一定是a标签:三级目录 } else { query.category3Id = category3id; } //判断:如果路由跳转的时候,带有params参数,捎带脚传递过去 if (this.$route.params) { loction.params = this.$route.params; //动态给location配置对象添加query属性 loction.query = query; //路由跳转 this.$router.push(loction); } } }, }, } </script>
方法3:我重写了goSearch()方法
其实就是取出data标记的变量,然后用query去接收,用loction变量整合query和params参数,最后传给router。
goSearch(event) { //取变量 let node = event.target.dataset let loction = { name: "search" }; let query = {'categoryName': node.categoryname,} if(node.category1id){query.category1Id = node.category1id} else if(node.category2id){query.category2Id = node.category2id} else{query.category3Id = node.category3id} //带参访问 if (node.categoryname) { if (this.$route.params) { loction.params = this.$route.params; loction.query = query; this.$router.push(loction); } } },
TypeNav/index.vue加上v-show=“show”、mounted()、leaveShow()、enterShow()
TypeNav/index.vue完整代码
<template> <!-- 商品分类导航 --> <div class="type-nav"> <div class="container" @mouseleave="leaveShow" @mouseenter="enterShow"> <h2 class="all">全部商品分类</h2> <nav class="nav"> <a href="###">服装城</a> <a href="###">美妆馆</a> <a href="###">尚品汇超市</a> <a href="###">全球购</a> <a href="###">闪购</a> <a href="###">团购</a> <a href="###">有趣</a> <a href="###">秒杀</a> </nav> <!-- 过渡动画 --> <transition name="sort"> <div class="sort" v-show="show"> <div class="all-sort-list2" @click="goSearch"> <div class="item" v-for="(c1,index) in categoryList" :key="c1.categoryId"> <h3 @mouseenter="changeIndex(index)" :class="{ cur: currentIndex == index }"> <a :data-categoryName="c1.categoryName" :data-category1Id="c1.categoryId" >{{ c1.categoryName }}</a> </h3> <div class="item-list clearfix" :style="{display: currentIndex == index?'block':'none'}"> <div class="subitem" v-for="(c2,index) in c1.categoryChild" :key="c2.categoryId"> <dl class="fore"> <dt> <a :data-categoryName="c2.categoryName" :data-category2Id="c2.categoryId" >{{ c2.categoryName }}</a> </dt> <dd> <em v-for="(c3,index) in c2.categoryChild" :key="c3.categoryId"> <a :data-categoryName="c3.categoryName" :data-category3Id="c3.categoryId" >{{ c3.categoryName }}</a> </em> </dd> </dl> </div> </div> </div> </div> </div> </transition> </div> </div> </template> <script> import { mapState } from "vuex"; //按需引入:只是引入节流函数,其他的函数没有引入(模块),这样做的好处是,当你打包项目的时候体积会小一些 import throttle from "lodash/throttle"; export default { name: 'TypeNav', data(){ return { currentIndex:-1, show:true, } }, mounted(){ this.$store.dispatch('getCategoryList') //当组件挂载完毕,让show属性变为false //如果不是Home路由组件,将typeNav进行隐藏 if (this.$route.path != '/home' && this.$route.path !='/'){ this.show = false } }, computed: { //state:他是咱们大仓库中的state(包含home|search) ...mapState({ categoryList: (state) => state.home.categoryList, }), }, methods: { // changeIndex(index){ // this.currentIndex = index // }, // 引入节流 changeIndex:throttle(function(index){ this.currentIndex = index },50), //当鼠标离开的时候,让商品分类列表进行隐藏 leaveShow(){ this.currentIndex = -1 if (this.$route.path != '/home' && this.$route.path !='/'){ this.show = false } }, //当鼠标移入的时候,让商品分类列表进行展示 enterShow(){ this.show = true }, //进行路由跳转的回调函数 goSearch(event) { //event.target:获取到的是出发事件的元素(div、h3、a、em、dt、dl) let node = event.target; //给a标签添加自定义属性data-categoryName,全部的字标签当中只有a标签带有自定义属性,别的标签名字----dataset纯属扯淡 let { categoryname, category1id, category2id, category3id, } = node.dataset; console.log('dataset',node.dataset) console.log('categoryname',categoryname) //第二个问题解决了:点击的到底是不是a标签(只要这个标签身上带有categoryname)一定是a标签 //当前这个if语句:一定是a标签才会进入 if (categoryname) { //准备路由跳转的参数对象 let loction = { name: "search" }; let query = { categoryName: categoryname }; //一定是a标签:一级目录 if (category1id) { query.category1Id = category1id; //一定是a标签:二级目录 } else if (category2id) { query.category2Id = category2id; //一定是a标签:三级目录 } else { query.category3Id = category3id; } //判断:如果路由跳转的时候,带有params参数,捎带脚传递过去 if (this.$route.params) { loction.params = this.$route.params; //动态给location配置对象添加query属性 loction.query = query; //路由跳转 this.$router.push(loction); } } }, }, } </script>
TypeNav/index.vue加上transition标签。
TypeNav/index.vue设定好less,记得.sort-enter需放在.container大括号里面。
//过渡动画的样式
//过渡动画开始状态(进入)
.sort-enter {
height: 0px;
}
// 过渡动画结束状态(进入)
.sort-enter-to {
height: 461px;
}
// 定义动画时间、速率
.sort-enter-active {
transition: all 0.5s linear;
}
typeNav商品分类列信息都是一样的,出于性能的考虑我们希望该数据只请求一次,所以我们把这次请求放在App.vue的mounted中。
将typeNav/index.vue的mount()改到App.vue的mount()。
App.vue增加mount()
<template> <div> <Header></Header> <router-view></router-view> <Footer v-show="$route.meta.showFooter"></Footer> </div> </template> <script> import Header from './components/Header' import Footer from './components/Footer' export default { name: 'App', components: { Header, Footer, }, mounted() { this.$store.dispatch('getCategoryList') }, } </script> <style lang="less"> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style>
Header/index.vue仿照这一段把query参数带过去。
//判断:如果路由跳转的时候,带有params参数,捎带脚传递过去
if (this.$route.params) {
loction.params = this.$route.params;
//动态给location配置对象添加query属性
loction.query = query;
//路由跳转
this.$router.push(loction);
}
Header/index.vue
<template> <header class="header"> <!-- 头部的第一行 --> <div class="top"> <div class="container"> <div class="loginList"> <p>尚品汇欢迎您!</p> <p> <span>请</span> <router-link to="/login">登录</router-link> <router-link class="register" to="/register">免费注册</router-link> </p> </div> <div class="typeList"> <a href="###">我的订单</a> <a href="###">我的购物车</a> <a href="###">我的尚品汇</a> <a href="###">尚品汇会员</a> <a href="###">企业采购</a> <a href="###">关注尚品汇</a> <a href="###">合作招商</a> <a href="###">商家后台</a> </div> </div> </div> <!--头部第二行 搜索区域--> <div class="bottom"> <h1 class="logoArea"> <router-link class="logo" title="尚品汇" to="/"> <img src="./images/logo.png" alt=""> </router-link> </h1> <div class="searchArea"> <form action="###" class="searchForm"> <input type="text" id="autocomplete" class="input-error input-xxlarge" v-model="keyword"/> <button class="sui-btn btn-xlarge btn-danger" type="button" @click="goSearch">搜索</button> </form> </div> </div> </header> </template> <script> export default { name: 'Header', data() { return { keyword:'', } }, methods:{ goSearch(){ //治标不治本 // this.$router.push(`/search/${this.keyword}`,()=>{},()=>{}) if (this.$route.query) { let location = { name: "search", params:{keyword:this.keyword || undefined} } //动态给location配置对象添加query属性 location.query = this.$route.query; //路由跳转 this.$router.push(location); } }, }, } </script>
第一步:安装依赖包mockjs
安装mockjs `npm install --save mockjs`
第二步:在src文件夹下创建一个文件夹mock。
第三步:准备模拟的数据。!
mock/banner.json
[ { "id": "1", "imgUrl": "/images/banner1.jpg" }, { "id": "2", "imgUrl": "/images/banner2.jpg" }, { "id": "3", "imgUrl": "/images/banner3.jpg" }, { "id": "4", "imgUrl": "/images/banner4.jpg" } ]
mock/floor.json
[ { "id": "001", "name": "家用电器", "keywords": [ "节能补贴", "4K电视", "空气净化器", "IH电饭煲", "滚筒洗衣机", "电热水器" ], "imgUrl": "/images/floor-1-1.png", "navList": [ { "url": "#", "text": "热门" }, { "url": "#", "text": "大家电" }, { "url": "#", "text": "生活电器" }, { "url": "#", "text": "厨房电器" }, { "url": "#", "text": "应季电器" }, { "url": "#", "text": "空气/净水" }, { "url": "#", "text": "高端电器" } ], "carouselList": [ { "id": "0011", "imgUrl": "/images/floor-1-b01.png" }, { "id": "0012", "imgUrl": "/images/floor-1-b02.png" }, { "id": "0013", "imgUrl": "/images/floor-1-b03.png" } ], "recommendList": [ "/images/floor-1-2.png", "/images/floor-1-3.png", "/images/floor-1-5.png", "/images/floor-1-6.png" ], "bigImg": "/images/floor-1-4.png" }, { "id": "002", "name": "手机通讯", "keywords": [ "节能补贴2", "4K电视2", "空气净化器2", "IH电饭煲2", "滚筒洗衣机2", "电热水器2" ], "imgUrl": "/images/floor-1-1.png", "navList": [ { "url": "#", "text": "热门2" }, { "url": "#", "text": "大家电2" }, { "url": "#", "text": "生活电器2" }, { "url": "#", "text": "厨房电器2" }, { "url": "#", "text": "应季电器2" }, { "url": "#", "text": "空气/净水2" }, { "url": "#", "text": "高端电器2" } ], "carouselList": [ { "id": "0011", "imgUrl": "/images/floor-1-b01.png" }, { "id": "0012", "imgUrl": "/images/floor-1-b02.png" }, { "id": "0013", "imgUrl": "/images/floor-1-b03.png" } ], "recommendList": [ "/images/floor-1-2.png", "/images/floor-1-3.png", "/images/floor-1-5.png", "/images/floor-1-6.png" ], "bigImg": "/images/floor-1-4.png" } ]
把mock数据需要的图片放置于public文件夹中
第四步:在mock文件夹中创建一个mockServe.js文件。
(注意:在server.js文件当中对于banner.json||floor.json的数据没有暴露,但是可以在server模块中使用。对于webpack当中一些模块:图片、json,不需要对外暴露,因为默认就是对外暴露。)
第五步:通过mock模块模拟出数据
mockServe.js
import Mock from 'mockjs'
//webpack默认对外暴露:json、图片
import banner from './banner.json'
import floor from './floor.json'
//mock数据:第一个参数请求地址、第二个参:请求数据
Mock.mock("/mock/banner",{code:200,data:banner})
Mock.mock("/mock/floor",{code:200,data:floor})
//记得要在main.js中引入一下
//import ''@/mock/mockServer
第六步:回到入口文件,引入serve.js。
import '@/mock/mockServe'
第七步:在API文件夹中创建mockRequest【axios实例:baseURL:‘/mock’】
1.api/mockAjax.js几乎跟request.js一模一样,默认暴露request,import时重命名为mockRequest。
import axios from "axios"; //引入进度条 import nprogress from 'nprogress'; //引入进度条样式 import "nprogress/nprogress.css"; //1、对axios二次封装 const requests = axios.create({ //基础路径,requests发出的请求在端口号后面会跟改baseURl baseURL:'/mock', timeout: 5000, }) //2、配置请求拦截器 requests.interceptors.request.use(config => { //config内主要是对请求头Header配置 //比如添加token //开启进度条 nprogress.start(); return config; }) //3、配置相应拦截器 requests.interceptors.response.use((res) => { //成功的回调函数 //响应成功,关闭进度条 nprogress.done() return res.data; },(error) => { //失败的回调函数 console.log("响应失败"+error) return Promise.reject(new Error('fail')) }) //4、对外暴露 export default requests;
2.api/index.js暴露mock数据
//当前模块,API进行统一管理,即对请求接口统一管理 import requests from "@/api/request"; import mockRequests from "./mockAjax"; //首页三级分类接口 export const reqgetCategoryList = () => { return requests({ url: '/product/getBaseCategoryList', method: 'GET' }) } //切记:当前函数执行需要把服务器返回结果返回 //获取banner(Home首页轮播图接口) export const reqGetBannerList = () => mockRequests.get("/banner"); //获取floor数据 export const reqFloorList = () => mockRequests.get("/floor");
第八步:Vuex三连(actions、mutations、state)
store/home.js新增 async getBannerList()、GETBANNERLIST()、bannerList,新增 async getFloorList()、GETFLOORLIST()、floorList。
import { reqgetCategoryList,reqGetBannerList } from "@/api"; //home模块的仓库 const state = { //home仓库中存储三级菜单的数据 categoryList: [], //轮播图的数据 bannerList: [], //floor组件的数据 floorList:[] }; //mutions是唯一修改state的地方 const mutations = { GETCATEGORYLIST(state, categoryList) { state.categoryList = categoryList; }, GETBANNERLIST(state, bannerList) { state.bannerList = bannerList; console.log('GETBANNERLIST') }, GETFLOORLIST(state,floorList){ state.floorList = floorList; } }; //action|用户处理派发action地方的,可以书写异步语句、自己逻辑地方 const actions = { async getCategoryList({ commit }) { //reqgetCategoryList返回的是一个Promise对象 //需要用await接受成功返回的结果,await必须要结合async一起使用(CP) let result = await reqgetCategoryList(); if (result.code == 200) { commit("GETCATEGORYLIST", result.data); } }, async getBannerList({ commit }) { let result = await reqGetBannerList(); if (result.code == 200) { commit("GETBANNERLIST", result.data); console.log('result.data',result.data) } }, //获取floor数据 async getFloorList({ commit }) { let result = await reqFloorList(); if (result.code == 200) { //提交mutation commit("GETFLOORLIST", result.data); console.log('result.data',result.data) } }, }; //计算属性 const getters = {}; export default { state, mutations, actions, getters, };
第九步: 组件请求数据
ListContainer/index.vue 请求数据,用计算属性bannerList去接收数据。
<script> import { mapState } from "vuex"; export default { name: 'ListContainer', mounted() { //mounted:组件挂载完毕,正常说组件结构(DOM)已经全有了 //为什么swiper实例在mounted当中直接书写不可以:因为结构还没有完整 this.$store.dispatch("getBannerList"); }, computed: { ...mapState({ bannerList: (state) => state.home.bannerList, }), } } </script>
官方做法请见https://blog.csdn.net/weixin_43424325/article/details/121684101中的【24、swiper插件实现轮播图】。
链接https://element.eleme.cn/#/zh-CN/component/carousel
main.js全局引用ElementUI
//引入ElementUI组件库
import ElementUI from 'element-ui';
//引入ElementUI全部样式
import 'element-ui/lib/theme-chalk/index.css';
//使用ElementUI
Vue.use(ElementUI)
ListContainer/index.vue引用【Carousel 走马灯】
<!--banner轮播-->
<div class="block">
<el-carousel height="455px">
<el-carousel-item v-for="(carousel,index) in bannerList" :key="carousel.id">
<img style="width:100%" :src="carousel.imgUrl" />
</el-carousel-item>
</el-carousel>
</div>
ListContainer/index.vue完整代码
<template> <!--列表--> <div class="list-container"> <div class="sortList clearfix"> <div class="center"> <!--banner轮播--> <div class="block"> <el-carousel height="455px"> <el-carousel-item v-for="(carousel,index) in bannerList" :key="carousel.id"> <img style="width:100%" :src="carousel.imgUrl" /> </el-carousel-item> </el-carousel> </div> </div> <div class="right"> <div class="news"> <h4> <em class="fl">尚品汇快报</em> <span class="fr tip">更多 ></span> </h4> <div class="clearix"></div> <ul class="news-list unstyled"> <li> <span class="bold">[特惠]</span>备战开学季 全民半价购数码 </li> <li> <span class="bold">[公告]</span>备战开学季 全民半价购数码 </li> <li> <span class="bold">[特惠]</span>备战开学季 全民半价购数码 </li> <li> <span class="bold">[公告]</span>备战开学季 全民半价购数码 </li> <li> <span class="bold">[特惠]</span>备战开学季 全民半价购数码 </li> </ul> </div> <ul class="lifeservices"> <li class=" life-item "> <i class="list-item"></i> <span class="service-intro">话费</span> </li> <li class=" life-item "> <i class="list-item"></i> <span class="service-intro">机票</span> </li> <li class=" life-item "> <i class="list-item"></i> <span class="service-intro">电影票</span> </li> <li class=" life-item "> <i class="list-item"></i> <span class="service-intro">游戏</span> </li> <li class=" life-item"> <i class="list-item"></i> <span class="service-intro">彩票</span> </li> <li class=" life-item"> <i class="list-item"></i> <span class="service-intro">加油站</span> </li> <li class=" life-item"> <i class="list-item"></i> <span class="service-intro">酒店</span> </li> <li class=" life-item"> <i class="list-item"></i> <span class="service-intro">火车票</span> </li> <li class=" life-item "> <i class="list-item"></i> <span class="service-intro">众筹</span> </li> <li class=" life-item"> <i class="list-item"></i> <span class="service-intro">理财</span> </li> <li class=" life-item"> <i class="list-item"></i> <span class="service-intro">礼品卡</span> </li> <li class=" life-item"> <i class="list-item"></i> <span class="service-intro">白条</span> </li> </ul> <div class="ads"> <img src="./images/ad1.png" /> </div> </div> </div> </div> </template> <script> import { mapState } from "vuex"; export default { name: 'ListContainer', mounted() { //mounted:组件挂载完毕,正常说组件结构(DOM)已经全有了 //为什么swiper实例在mounted当中直接书写不可以:因为结构还没有完整 this.$store.dispatch("getBannerList"); }, computed: { ...mapState({ bannerList: (state) => state.home.bannerList, }), } } </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped lang="less"> .list-container { width: 1200px; margin: 0 auto; .sortList { height: 464px; padding-left: 210px; .center { box-sizing: border-box; width: 740px; height: 100%; padding: 5px; float: left; .el-carousel__item:nth-child(2n) { background-color: #99a9bf; } .el-carousel__item:nth-child(2n+1) { background-color: #d3dce6; } } .right { float: left; width: 250px; .news { border: 1px solid #e4e4e4; margin-top: 5px; h4 { border-bottom: 1px solid #e4e4e4; padding: 5px 10px; margin: 5px 5px 0; line-height: 22px; overflow: hidden; font-size: 14px; .fl { float: left; } .fr { float: right; font-size: 12px; font-weight: 400; } } .news-list { padding: 5px 15px; line-height: 26px; .bold { font-weight: 700; } } } .lifeservices { border-right: 1px solid #e4e4e4; overflow: hidden; display: flex; flex-wrap: wrap; .life-item { border-left: 1px solid #e4e4e4; border-bottom: 1px solid #e4e4e4; margin-right: -1px; height: 64px; text-align: center; position: relative; cursor: pointer; width: 25%; .list-item { background-image: url(./images/icons.png); width: 61px; height: 40px; display: block; } .service-intro { line-height: 22px; width: 60px; display: block; } &:nth-child(1) { .list-item { background-position: 0px -5px; } } &:nth-child(2) { .list-item { background-position: -62px -5px; } } &:nth-child(3) { .list-item { background-position: -126px -5px; } } &:nth-child(4) { .list-item { background-position: -190px -5px; } } &:nth-child(5) { .list-item { background-position: 0px -76px; } } &:nth-child(6) { .list-item { background-position: -62px -76px; } } &:nth-child(7) { .list-item { background-position: -126px -76px; } } &:nth-child(8) { .list-item { background-position: -190px -76px; } } &:nth-child(9) { .list-item { background-position: 0px -146px; } } &:nth-child(10) { .list-item { background-position: -62px -146px; } } &:nth-child(11) { .list-item { background-position: -126px -146px; } } &:nth-child(12) { .list-item { background-position: -190px -146px; } } } } .ads { margin-top: 5px; img { opacity: 0.8; transition: all 400ms; &:hover { opacity: 1; } } } } } } </style>
最终效果:
Home/index.vue 里面有Floor组件,并用props传值。
<template> <div> <!-- 三级联动全局组件已经注册为全局组件,因此不需要引入--> <TypeNav/> <ListContainer/> <Recommend/> <Rank/> <Like/> <Floor v-for="(floor,index) in floorList" :key="floor.id" :list="floor"/> <Brand/> </div> </template> <script> import ListContainer from '@/views/Home/ListContainer' import Recommend from '@/views/Home/Recommend' import Rank from '@/views/Home/Rank' import Like from '@/views/Home/Like' import Floor from '@/views/Home/Floor' import Brand from '@/views/Home/Brand' import { mapState } from 'vuex' export default { name: 'Home', components:{ ListContainer, Recommend, Rank, Like, Floor, Brand, }, mounted(){ //派发action,获取floor组件的数据 this.$store.dispatch('getFloorList') }, computed: { ...mapState({ floorList: (state) => state.home.floorList, }), } } </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped lang="less"> </style>
Floor/index.vue 动态传值
<template> <!--楼层--> <div class="floor"> <div class="py-container"> <div class="title clearfix"> <h3 class="fl">{{ list.name }}</h3> <div class="fr"> <ul class="nav-tabs clearfix"> <li class="active"> <a :href="item.url" data-toggle="tab" v-for="(item,index) in list.navList" :key="index">{{ item.text }}</a> </li> </ul> </div> </div> <div class="tab-content"> <div class="tab-pane"> <div class="floor-1"> <div class="blockgary"> <ul class="jd-list"> <li v-for="(keyword,index) in list.keywords" :key="index"> {{ keyword }} </li> </ul> <img :src="list.imgUrl" /> </div> <div class="floorBanner"> <!-- 引入轮播图 --> <div class="block"> <el-carousel height="355px" arrow="always"> <el-carousel-item v-for="(carousel,index) in list.carouselList" :key="carousel.id"> <img style="width:100%" :src="carousel.imgUrl" /> </el-carousel-item> </el-carousel> </div> </div> <div class="split"> <span class="floor-x-line"></span> <div class="floor-conver-pit"> <img :src="list.recommendList[0]" /> </div> <div class="floor-conver-pit"> <img :src="list.recommendList[1]" /> </div> </div> <div class="split center"> <img :src="list.bigImg" /> </div> <div class="split"> <span class="floor-x-line"></span> <div class="floor-conver-pit"> <img :src="list.recommendList[2]" /> </div> <div class="floor-conver-pit"> <img :src="list.recommendList[3]" /> </div> </div> </div> </div> </div> </div> </div> </template> <script> export default { name: 'Floor', props:['list'], } </script>
1.编写公共组件
Carousel/index.vue
<template> <!-- 引入轮播图 --> <div class="block"> <el-carousel :height="height" arrow="always"> <el-carousel-item v-for="(carousel,index) in list.carouselList" :key="carousel.id"> <img style="width:100%" :src="carousel.imgUrl" /> </el-carousel-item> </el-carousel> </div> </template> <script> export default { name: 'Carousel', props:['list','height'], } </script> <style scoped lang="less"> </style>
2.全局注册
main.js
import Carousel from '@/components/Carousel'
Vue.component(Carousel.name,Carousel)
3.组件内引用
Floor/index.vue
<!-- 引入轮播图 -->
<!-- <div class="block">
<el-carousel height="355px" arrow="always">
<el-carousel-item v-for="(carousel,index) in list.carouselList" :key="carousel.id">
<img style="width:100%" :src="carousel.imgUrl" />
</el-carousel-item>
</el-carousel>
</div> -->
<Carousel :list="list" :height="`355px`"/>
api/index.js 根据接口文件,给服务器传递一个默认参数【至少是一个空对象】
//当前这个接口(获取搜索模块的数据),给服务器传递一个默认参数【至少是一个空对象】
export const reqGetSearchInfo = (params)=>requests({url:"/list",method:"post",data:params});
store/search.js我没有使用getters
computed:{
...mapState({
goodsList: (state)=> state.search.searchList.goodsList || [],
attrsList: (state)=> state.search.searchList.attrsList,
trademarkList: (state)=> state.search.searchList.trademarkList,
})
},
store/search.js
import { reqGetSearchInfo } from "@/api"; //home模块的仓库 const state = { //仓库初始状态 searchList:{} }; //mutions是唯一修改state的地方 const mutations = { REQGETSEARCHINFO(state, searchList) { state.searchList = searchList; }, }; //action|用户处理派发action地方的,可以书写异步语句、自己逻辑地方 const actions = { async getSearchList({ commit },params={}) { let result = await reqGetSearchInfo(params); if (result.code == 200) { commit("REQGETSEARCHINFO", result.data); } }, }; // 计算属性 const getters = { }; export default { state, mutations, actions, getters, };
Search/index.vue 采取mapState,并动态展现数据
<template> <div> <TypeNav /> <div class="main"> <div class="py-container"> <!--bread--> <div class="bread"> <ul class="fl sui-breadcrumb"> <li> <a href="#">全部结果</a> </li> </ul> <ul class="fl sui-tag"> <li class="with-x">手机</li> <li class="with-x">iphone<i>×</i></li> <li class="with-x">华为<i>×</i></li> <li class="with-x">OPPO<i>×</i></li> </ul> </div> <!--selector--> <SearchSelector /> <!--details--> <div class="details clearfix"> <div class="sui-navbar"> <div class="navbar-inner filter"> <ul class="sui-nav"> <li class="active"> <a href="#">综合</a> </li> <li> <a href="#">销量</a> </li> <li> <a href="#">新品</a> </li> <li> <a href="#">评价</a> </li> <li> <a href="#">价格⬆</a> </li> <li> <a href="#">价格⬇</a> </li> </ul> </div> </div> <div class="goods-list"> <ul class="yui3-g"> <li class="yui3-u-1-5" v-for="(good,index) in goodsList" :key="good.id"> <div class="list-wrap"> <div class="p-img"> <a href="item.html" target="_blank"><img :src="good.defaultImg" /></a> </div> <div class="price"> <strong> <em>¥</em> <i>{{ good.price }}.00</i> </strong> </div> <div class="attr"> <a target="_blank" href="item.html" :title="good.title" v-html="good.title"></a> </div> <div class="commit"> <i class="command">已有<span>{{ good.hotScore }}</span>人评价</i> </div> <div class="operate"> <a href="success-cart.html" target="_blank" class="sui-btn btn-bordered btn-danger">加入购物车</a> <a href="javascript:void(0);" class="sui-btn btn-bordered">收藏</a> </div> </div> </li> </ul> </div> <div class="fr page"> <div class="sui-pagination clearfix"> <ul> <li class="prev disabled"> <a href="#">«上一页</a> </li> <li class="active"> <a href="#">1</a> </li> <li> <a href="#">2</a> </li> <li> <a href="#">3</a> </li> <li> <a href="#">4</a> </li> <li> <a href="#">5</a> </li> <li class="dotted"><span>...</span></li> <li class="next"> <a href="#">下一页»</a> </li> </ul> <div><span>共10页 </span></div> </div> </div> </div> </div> </div> </div> </template> <script> import { mapState,mapGetters } from 'vuex'; import SearchSelector from './SearchSelector/SearchSelector' export default { name: 'Search', data(){ return { searchParams:{ "category1Id": "", "category2Id": "", "category3Id": "", "categoryName": "", "keyword": "小米", //排序 "order": "", "pageNo": 1, "pageSize": 10, //平台属性的操作 "props": [], //品牌 "trademark": "" }, } }, components: { SearchSelector }, mounted() { this.$store.dispatch("getSearchList",this.searchParams); }, computed:{ ...mapState({ goodsList: (state)=> state.search.searchList.goodsList || [], attrsList: (state)=> state.search.searchList.attrsList, trademarkList: (state)=> state.search.searchList.trademarkList, }) }, } </script>
1.POST请求数据,可能会请求数据延时,导致无法取得数据。
假如网络不给力或没有网state.searchList.goodsList应该返回的是undefined
2.goodsList写成goodslist
goodsList: (state)=> state.search.searchList.goodsList
Search/SearchSelector/SearchSelector.vue
<template> <div class="clearfix selector"> <div class="type-wrap logo"> <div class="fl key brand">品牌</div> <div class="value logos"> <ul class="logo-list"> <li v-for="(trademark,index) in trademarkList" :key="trademark.tmId">{{ trademark.tmName }}</li> </ul> </div> <div class="ext"> <a href="javascript:void(0);" class="sui-btn">多选</a> <a href="javascript:void(0);">更多</a> </div> </div> <div class="type-wrap" v-for="(attr,index) in attrsList" :key="attr.attrId"> <div class="fl key">{{ attr.attrName }}</div> <div class="fl value"> <ul class="type-list" v-for="(attrValue,index) in attr.attrValueList" :key="index"> <li> <a>{{ attrValue }}</a> </li> </ul> </div> <div class="fl ext"></div> </div> </div> </template> <script> import { mapState,mapGetters } from 'vuex'; export default { name: 'SearchSelector', computed:{ ...mapState({ attrsList: (state)=> state.search.searchList.attrsList, trademarkList: (state)=> state.search.searchList.trademarkList, }) }, } </script>
Vuex 数据结构
Search/index.vue
watch:{ //监听路由的信息是否发生变化,如果发生变化,再次发起请求 $route(newValue,oldValue){ console.log(newValue) //每一次请求完毕,应该把相应的1、2、3级分类的id置空的,让他接受下一次的相应1、2、3 //再次发请求之前整理带给服务器参数 Object.assign(this.searchParams,this.$route.query,this.$route.params); this.getData() console.log(this.searchParams) //分类名字与关键字不用清理:因为每一次路由发生变化的时候,都会给他赋予新的数据 this.searchParams.category1Id = undefined; this.searchParams.category2Id = undefined; this.searchParams.category3Id = undefined; //分类Season清掉 // this.searchParams.categoryName = undefined; // this.searchParams.keyword = undefined; } }
三个ID只能带一个,所以需要重置。分类名字与关键字不用清理:因为每一次路由发生变化的时候,都会给他赋予新的数据
//分类名字与关键字不用清理:因为每一次路由发生变化的时候,都会给他赋予新的数据
this.searchParams.category1Id = undefined;
this.searchParams.category2Id = undefined;
this.searchParams.category3Id = undefined;
//分类Season清掉
this.searchParams.categoryName = undefined;
this.searchParams.keyword = undefined;
Search\index.vue 把带给服务器的参数置空了,还需要向服务器发请求。undefined字段不会带给服务器。通过自己跳自己,删除query,保留params参数。
<template> <li class="with-x" v-if="searchParams.categoryName">{{ searchParams.categoryName }}<i @click="removeCategoryName">×</i></li> </template> <script> removeCategoryName() { //把带给服务器的参数置空了,还需要向服务器发请求 //带给服务器参数说明可有可无的:如果属性值为空的字符串还是会把相应的字段带给服务器 //但是你把相应的字段变为undefined,当前这个字段不会带给服务器 this.searchParams.category1Id = undefined this.searchParams.category2Id = undefined this.searchParams.category3Id = undefined this.searchParams.categoryName = undefined this.getData() //地址栏也需要需改:进行路由跳转(现在的路由跳转只是跳转到自己这里) //严谨:本意是删除query,如果路径当中出现params不应该删除,路由跳转的时候应该带着 if (this.$route.params) { this.$router.push({ name: "search", params: this.$route.params }); } }, </script>
Search\index.vue 给服务器带的参数searchParams的keyword置空,还需要向服务器发请求。undefined字段不会带给服务器。全局路线总线通知兄弟组件Header清除关键字。通过自己跳自己,删除路径上的params,保留路径上的query参数。
<template> <li class="with-x" v-if="searchParams.categoryName">{{ searchParams.categoryName }}<i @click="removeCategoryName">×</i></li> </template> <script> removeKeyword(){ //给服务器带的参数searchParams的keyword置空 this.searchParams.keyword = undefined //再次发请求 this.getData() //通知兄弟组件Header清除关键字 this.$bus.$emit('clear') //进行路由的跳转 if (this.$route.query) { this.$router.push({ name: "search", query: this.$route.query }); } } </script>
全局路线总线通知Header/index.vue
mounted() {
//通过全局事件总线清除关键字
this.$bus.$on("clear", () => {
this.keyword = "";
});
},
第一种 父子组件通信:$ on、$emit自定义事件实现子组件给父组件传递信息。 props实现父组件给子组件传递数据。
第二种 全局事件总线 $bus(适用于所有的场景)
第三种 Vuex
第四种 插槽(适用于父子组件通信)
点击子组件SearchSelector商标时,父组件Search的数据重新发请求。
Search/SearchSelector/SearchSelector.vue给父组件传递参数。
<template>
<li v-for="(trademark,index) in trademarkList" :key="trademark.tmId" @click="tradeMatkHandler(trademark)" >{{ trademark.tmName }}</li>
</template>
<script>
methods: {
//品牌的事件处理函数
tradeMatkHandler(trademark){
//点击了品牌(苹果),还是需要整理参数,向服务器发请求获取相应的数据进行展示
this.$emit('trademarkInfo',trademark)
},
},
</script>
Search/index.vue删除品牌的信息,更新searchParams.trademark = undefined。采取自定义事件回调,再次发请求获取search模块列表数据进行展示。
<template> <li class="with-x" v-if="searchParams.trademark">{{ searchParams.trademark.split(':')[1] }}<i @click="removeTradeMark">×</i></li> <SearchSelector @trademarkInfo="trademarkInfo" /> </template> <script> methods: { //删除品牌的信息 removeTradeMark(){ this.searchParams.trademark = undefined this.getData() }, //自定义事件回调 trademarkInfo(trademark){ //1:整理品牌字段的参数 "ID:品牌名称" this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}` //再次发请求获取search模块列表数据进行展示 this.getData() }, }, </script>
点击子组件SearchSelector属性时,父组件Search的数据重新发请求。
Search/SearchSelector/SearchSelector.vue给父组件传递参数。
<template> <!-- 平台相应售卖的属性的属性值:粉色,蓝色,黑色... --> <li v-for="(attrValue,index) in attr.attrValueList" :key="index" @click="attrInfo(attr,attrValue)"> <a>{{ attrValue }}</a> </li> </template> <script> methods: { //平台售卖属性值的点击事件 attrInfo(attr,attrValue){ //["属性ID:属性值:属性名"] this.$emit("attrInfo",attr,attrValue); } }, </script>
Search/index.vue采取自定义事件回调,根据所选择的属性,再次发请求获取search模块列表数据进行展示。也可以删除属性,更新this.searchParams.props.splice(index, 1)。
<template> <li class="with-x" v-for="(attrValue, index) in searchParams.props" :key="index"> {{ attrValue.split(":")[1] }}<i @click="removeAttr(index)">×</i> </li> <SearchSelector @trademarkInfo="trademarkInfo" @attrInfo="attrInfo"/> </template> <script> methods: { //收集平台属性地方回调函数(自定义事件) attrInfo(attr, attrValue) { //["属性ID:属性值:属性名"] console.log(attr, attrValue); //参数格式整理好 let props = `${attr.attrId}:${attrValue}:${attr.attrName}`; //数组去重 //if语句里面只有一行代码:可以省略大花括号 if (this.searchParams.props.indexOf(props) == -1) this.searchParams.props.push(props); //再次发请求 this.getData(); }, //removeAttr删除售卖的属性 removeAttr(index) { //再次整理参数splice(index,howmany,item1,…itemx); this.searchParams.props.splice(index, 1); //再次发请求 this.getData(); }, }, </script>
排序的逻辑比较简单,只是改变一下请求参数中的order字段,后端会根据order值返回不同的数据来实现升降序。
order属性值为字符串,例如‘1:asc’、‘2:desc’。1代表综合,2代表价格,asc代表升序,desc代表降序。
我们的升降序是通过箭头图标来辨别的,如图所示:
Search/index.vue
1.在search模块使用该图标
<ul class="sui-nav">
<li :class="{ active: isOne }" @click="changeOrder('1')">
<a >综合
<span v-show="isOne && isAsc">↑</span>
<span v-show="isOne && isDesc">↓</span>
</a>
</li>
<li :class="{ active: isTwo }" @click="changeOrder('2')">
<a >价格
<span v-show="isTwo && isAsc">↑</span>
<span v-show="isTwo && isDesc">↓</span>
</a>
</li>
</ul>
2.isOne、isTwo、isAsc、isDesc计算属性代码
computed:{
...
isOne(){
return this.searchParams.order.indexOf('1') !== -1
},
isTwo(){
return this.searchParams.order.indexOf('2') !== -1
},
isAsc(){
return this.searchParams.order.indexOf('asc') !== -1
},
isDesc(){
return this.searchParams.order.indexOf('desc') !== -1
},
},
3.点击‘综合’或‘价格’的触发函数changeOrder
//排序的操作 changeOrder(flag) { //flag:用户每一次点击li标签的时候,用于区分是综合(1)还是价格(2) //现获取order初始状态【咱们需要通过初始状态去判断接下来做什么】 let originOrder = this.searchParams.order; let orginsFlag = originOrder.split(":")[0]; let originSort = originOrder.split(":")[1]; //新的排序方式 let newOrder = ""; //判断的是多次点击的是不是同一个按钮 if (flag == orginsFlag) { newOrder = `${orginsFlag}:${originSort == "desc" ? "asc" : "desc"}`; } else { //点击不是同一个按钮 newOrder = `${flag}:${"desc"}`; } //需要给order重新赋值 this.searchParams.order = newOrder; //再次发请求 this.getData(); },
如果你想在你的字符串内加入某个变量的值,就需要字符串拼接使用 ``(飘符号),由于 飘在markdown是单行代码标记所以下面我们用··代替。
字符串拼接 ·${}·,使用方法如下:
在js中使用
var a = 1;
console.log(`a的值是:${a}`); //a的值是:1
在html中使用
<router-link :to="`/detail/${goods.id}`"></router-link>
先查看Vuex/search返回数组情况
实际开发中是不会手写的,一般都会用一些开源库封装好的分页,比如element ui。但是这个知识还是值得学习一下的。
核心属性:pageNo(当前页码)、pageSize、total、continues(连续展示的页码)
核心逻辑是获取连续页码的起始页码和末尾页码,通过计算属性获得。(计算属性如果想返回多个数值,可以通过对象形式返回)
当点击页码会将pageNo传递给父组件,然后父组件发起请求,最后渲染。这里还是应用通过自定义事件实现子组件向父组件传递信息。
Pagination/index.vue
//连续页码的起始页码、末尾页码 startNumAndEnd(){ let start = 0 , end = 0; //规定连续页码数字5(totalPage至少5页) //不正常现象 if(this.continues > this.totalPage){ start = 1 end = this.totalPage }else{ //正常现象 Math.floor:想下取整 start = this.pageNo - Math.floor(this.continues/2) end = this.pageNo + Math.floor(this.continues/2) //start出现不正常现象纠正 if(start < 1){ start = 1 end = this.continues } //end出现不正常现象纠正 if(end > this.totalPage){ end = this.totalPage start = this.totalPage - this.continues + 1 } } return {start,end} }
Pagination/index.vue使用ElementUI的分页器,简单很多。
<template> <div class="block"> <el-pagination @current-change="handleCurrentChange" :current-page.sync="currentPage3" :page-size="pageSize" layout="prev, pager, next, jumper" :total="total"> </el-pagination> </div> </template> <script> export default { name: "Pagination", props: ["pageNo", "pageSize", "total", "continues"], methods: { handleCurrentChange(val) { console.log(`当前页: ${val}`); this.$emit('getPageNo', val) } }, data() { return { //当前页数 currentPage3: 1, }; } } </script>
Search\index.vue传入分页器所需内容,并注册getPageNo自定义事件。
<template> <div> <TypeNav /> <div class="main"> <div class="py-container"> <!--bread--> <div class="bread"> <ul class="fl sui-breadcrumb"> <li> <a href="#">全部结果</a> </li> </ul> <ul class="fl sui-tag"> <li class="with-x" v-if="searchParams.categoryName">{{ searchParams.categoryName }}<i @click="removeCategoryName">×</i></li> <li class="with-x" v-if="searchParams.keyword">{{ searchParams.keyword }}<i @click="removeKeyword">×</i></li> <li class="with-x" v-if="searchParams.trademark">{{ searchParams.trademark.split(':')[1] }}<i @click="removeTradeMark">×</i></li> <li class="with-x" v-for="(attrValue, index) in searchParams.props" :key="index"> {{ attrValue.split(":")[1] }}<i @click="removeAttr(index)">×</i> </li> </ul> </div> <!--selector--> <SearchSelector @trademarkInfo="trademarkInfo" @attrInfo="attrInfo"/> <!--details--> <div class="details clearfix"> <div class="sui-navbar"> <div class="navbar-inner filter"> <ul class="sui-nav"> <li :class="{ active: isOne }" @click="changeOrder('1')"> <a >综合 <span v-show="isOne && isAsc">↑</span> <span v-show="isOne && isDesc">↓</span> </a> </li> <li :class="{ active: isTwo }" @click="changeOrder('2')"> <a >价格 <span v-show="isTwo && isAsc">↑</span> <span v-show="isTwo && isDesc">↓</span> </a> </li> </ul> </div> </div> <div class="goods-list"> <ul class="yui3-g"> <li class="yui3-u-1-5" v-for="(good,index) in goodsList" :key="good.id"> <div class="list-wrap"> <div class="p-img"> <a href="item.html" target="_blank"><img :src="good.defaultImg" /></a> </div> <div class="price"> <strong> <em>¥</em> <i>{{ good.price }}.00</i> </strong> </div> <div class="attr"> <a target="_blank" href="item.html" :title="good.title" v-html="good.title"></a> </div> <div class="commit"> <i class="command">已有<span>{{ good.hotScore }}</span>人评价</i> </div> <div class="operate"> <a href="success-cart.html" target="_blank" class="sui-btn btn-bordered btn-danger">加入购物车</a> <a href="javascript:void(0);" class="sui-btn btn-bordered">收藏</a> </div> </div> </li> </ul> </div> <Pagination :pageNo="searchParams.pageNo" :pageSize="searchParams.pageSize" :total="total" :continues="5" @getPageNo="getPageNo" /> </div> </div> </div> </div> </template> <script> import { mapState,mapGetters } from 'vuex'; import SearchSelector from './SearchSelector/SearchSelector' export default { name: 'Search', data(){ return { searchParams:{ "category1Id": "", "category2Id": "", "category3Id": "", "categoryName": "", "keyword": "", //排序 "order": "1:desc", "pageNo": 1, "pageSize": 4, //平台属性的操作 "props": [], //品牌 "trademark": "" }, } }, components: { SearchSelector }, beforeMount() { //在发请求之前咱们需要将searchParams里面参数进行修改带给服务器 Object.assign(this.searchParams,this.$route.query,this.$route.params) }, mounted() { //在发请求之前咱们需要将searchParams里面参数进行修改带给服务器 this.getData() }, methods: { getData(){ this.$store.dispatch("getSearchList",this.searchParams); }, //删除分类的名字 removeCategoryName() { //把带给服务器的参数置空了,还需要向服务器发请求 //带给服务器参数说明可有可无的:如果属性值为空的字符串还是会把相应的字段带给服务器 //但是你把相应的字段变为undefined,当前这个字段不会带给服务器 this.searchParams.category1Id = undefined this.searchParams.category2Id = undefined this.searchParams.category3Id = undefined this.searchParams.categoryName = undefined this.getData() //地址栏也需要需改:进行路由跳转(现在的路由跳转只是跳转到自己这里) //严谨:本意是删除query,如果路径当中出现params不应该删除,路由跳转的时候应该带着 if (this.$route.params) { this.$router.push({ name: "search", params: this.$route.params }); } }, removeKeyword(){ //给服务器带的参数searchParams的keyword置空 this.searchParams.keyword = undefined //再次发请求 this.getData() //通知兄弟组件Header清除关键字 this.$bus.$emit('clear') //进行路由的跳转 if (this.$route.query) { this.$router.push({ name: "search", query: this.$route.query }); } }, //自定义事件回调 trademarkInfo(trademark){ //1:整理品牌字段的参数 "ID:品牌名称" this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}` //再次发请求获取search模块列表数据进行展示 this.getData() }, //删除品牌的信息 removeTradeMark() { //将品牌信息置空 this.searchParams.trademark = undefined; //再次发请求 this.getData(); }, //收集平台属性地方回调函数(自定义事件) attrInfo(attr, attrValue) { //["属性ID:属性值:属性名"] console.log(attr, attrValue); //参数格式整理好 let props = `${attr.attrId}:${attrValue}:${attr.attrName}`; //数组去重 //if语句里面只有一行代码:可以省略大花括号 if (this.searchParams.props.indexOf(props) == -1) this.searchParams.props.push(props); //再次发请求 this.getData(); }, //removeAttr删除售卖的属性 removeAttr(index) { //再次整理参数splice(index,howmany,item1,…itemx); this.searchParams.props.splice(index, 1); //再次发请求 this.getData(); }, //排序的操作 changeOrder(flag) { //flag:用户每一次点击li标签的时候,用于区分是综合(1)还是价格(2) //现获取order初始状态【咱们需要通过初始状态去判断接下来做什么】 let originOrder = this.searchParams.order; let orginsFlag = originOrder.split(":")[0]; let originSort = originOrder.split(":")[1]; //新的排序方式 let newOrder = ""; //判断的是多次点击的是不是同一个按钮 if (flag == orginsFlag) { newOrder = `${orginsFlag}:${originSort == "desc" ? "asc" : "desc"}`; } else { //点击不是同一个按钮 newOrder = `${flag}:${"desc"}`; } //需要给order重新赋值 this.searchParams.order = newOrder; //再次发请求 this.getData(); }, //自定义事件的回调函数---获取当前第几页 getPageNo(pageNo) { //整理带给服务器参数 this.searchParams.pageNo = pageNo; //再次发请求 this.getData(); }, }, computed:{ ...mapState({ goodsList: (state)=> state.search.searchList.goodsList || [], attrsList: (state)=> state.search.searchList.attrsList, trademarkList: (state)=> state.search.searchList.trademarkList, total:(state)=> state.search.searchList.total, }), isOne(){ return this.searchParams.order.indexOf('1') !== -1 }, isTwo(){ return this.searchParams.order.indexOf('2') !== -1 }, isAsc(){ return this.searchParams.order.indexOf('asc') !== -1 }, isDesc(){ return this.searchParams.order.indexOf('desc') !== -1 }, }, watch:{ //监听路由的信息是否发生变化,如果发生变化,再次发起请求 $route(newValue,oldValue){ console.log(newValue) //每一次请求完毕,应该把相应的1、2、3级分类的id置空的,让他接受下一次的相应1、2、3 //再次发请求之前整理带给服务器参数 Object.assign(this.searchParams,this.$route.query,this.$route.params); this.getData() console.log(this.searchParams) //分类名字与关键字不用清理:因为每一次路由发生变化的时候,都会给他赋予新的数据 this.searchParams.category1Id = undefined; this.searchParams.category2Id = undefined; this.searchParams.category3Id = undefined; } } } </script>
main.js:Pagination注册为全局组件。
import Pagination from '@/components/Pagination'
Vue.component(Pagination.name,Pagination)
使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。 vue-router 能做到,而且更好,它让你可以自定义路由切换时页面如何滚动。
https://router.vuejs.org/zh/guide/advanced/scroll-behavior.html
router/index.js
// 向外默认暴露路由器对象
export default new VueRouter({
mode: 'history', // 没有#的模式
routes, // 注册所有路由
scrollBehavior(to, from, savedPosition) {
// 始终滚动到顶部
return { y: 0 }
},
})
访问undefined的属性值会引起红色警告,可以不处理,但是要明白警告的原因。
以获取商品categoryView信息为例,categoryView是一个对象。
对应的getters代码
const getters = {
categoryView(state){
return state.goodInfo.categoryView
}
}
对应的computed代码
computed:{
...mapGetters(['categoryView'])
}
html代码
<div class="conPoin">
<span v-show="categoryView.category1Name" >{{categoryView.category1Name}}</span>
<span v-show="categoryView.category2Name" >{{categoryView.category2Name}}</span>
<span v-show="categoryView.category3Name" >{{categoryView.category3Name}}</span>
</div>
注意下细节在于getters的返回值。如果getters按上面代码写为return state.goodInfo.categoryView,页面可以正常运行,但是会出现红色警告。
原因:假设我们网络故障,导致goodInfo的数据没有请求到,即goodInfo是一个空的对象,当我们去调用getters中的return state.goodInfo.categoryView时,因为goodInfo为空,所以也不存在categoryView,即我们getters得到的categoryView为undefined。所以我们在html使用该变量时就会出现没有该属性的报错。
即:网络正常时不会出错,一旦无网络或者网络问题就会报错。
总结:所以我们在写getters的时候要养成一个习惯在返回值后面加一个||条件。即当属性值undefined时,会返回||后面的数据,这样就不会报错。如果返回值为对象加||{},数组:||[ ]。此处categoryView为对象,所以将getters代码改为return state.goodInfo.categoryView||{}
1.接口文档说明:参数是params的skuId。
2.router/routes.js
{
path:'/detail/:skuId',//params参数需要占位
component:Detail,
meta:{
showFooter: true,
}
},
3.Search/index.vue
<router-link :to="`/detail/${good.id}`"><img :src="good.defaultImg" /></router-link>
4.api/index.js
//产品详情
export const reqGoodsInfo = (skuId)=>requests({url:`/item/${skuId}`,method:"get"});
1.api已经写了。
2.vuex三连
store/detail.js
import { reqgetCategoryList,reqGetBannerList,reqFloorList,reqGoodsInfo } from "@/api"; //仓库 const state = { goodInfo: {}, }; //mutions是唯一修改state的地方 const mutations = { GETGOODINFO(state, goodInfo) { state.goodInfo = goodInfo; }, }; //action|用户处理派发action地方的,可以书写异步语句、自己逻辑地方 const actions = { async getGoodInfo({ commit },skuId) { //reqgetCategoryList返回的是一个Promise对象 //需要用await接受成功返回的结果,await必须要结合async一起使用(CP) let result = await reqGoodsInfo(skuId); if (result.code == 200) { commit("GETGOODINFO", result.data); } }, }; //计算属性 const getters = {}; export default { state, mutations, actions, getters, };
3.组件取得数据
Detail/index.vue
<script> import ImageList from './ImageList/ImageList' import Zoom from './Zoom/Zoom' import { mapState } from 'vuex' export default { name: 'Detail', mounted() { this.$store.dispatch('getGoodInfo',this.$route.params.skuId) }, components: { ImageList, Zoom }, computed:{ ...mapState({ goodInfo: (state) => state.detail.goodInfo, }) }, } </script>
Detail/index.vue展示部分数据(面包屑、价格、商品名称、商品详情)
<!-- 导航路径区域 --> <div class="conPoin"> <span v-show="goodInfo.categoryView.category1Name">{{ goodInfo.categoryView.category1Name }}</span> <span v-show="goodInfo.categoryView.category2Name">{{ goodInfo.categoryView.category2Name }}</span> <span v-show="goodInfo.categoryView.category3Name">{{ goodInfo.categoryView.category3Name }}</span> </div> <!-- 左侧放大镜区域 --> <div class="previewWrap"> <!--放大镜效果--> <Zoom :skuImageList="goodInfo.skuInfo.skuImageList" /> <!-- 小图列表 --> <ImageList :skuImageList="goodInfo.skuInfo.skuImageList" /> </div> <!-- 右侧选择区域布局 --> <div class="goodsDetail"> <h3 class="InfoName">{{ goodInfo.skuInfo.skuName }}</h3> <p class="news">{{ goodInfo.skuInfo.skuDesc }}</p> <div class="priceArea"> <div class="priceArea1"> <div class="title">价 格</div> <div class="price"> <i>¥</i> <em>{{ goodInfo.skuInfo.price }}</em> <span>降价通知</span> </div> <div class="remark"> <i>累计评价</i> <em>65545</em> </div> </div> <div class="priceArea2"> <div class="title"> <i>促 销</i> </div> <div class="fixWidth"> <i class="red-bg">加价购</i> <em class="t-gray">满999.00另加20.00元,或满1999.00另加30.00元,或满2999.00另加40.00元,即可在购物车换购热销商品</em> </div> </div> </div> <div class="support"> <div class="supportArea"> <div class="title">支 持</div> <div class="fixWidth">以旧换新,闲置手机回收 4G套餐超值抢 礼品购</div> </div> <div class="supportArea"> <div class="title">配 送 至</div> <div class="fixWidth">广东省 深圳市 宝安区</div> </div> </div> </div>
老师的方法很巧妙:在轮播图组件中设置一个currendIndex,用来记录所点击图片的下标,并用currendIndex实现点击图片高亮设置。当符合图片的下标满足currentIndex===index时,该图片就会被标记为选中。
ImageList.vue用ElementUI实现轮播图
<template> <div class="swiper-container"> <!-- <div class="swiper-wrapper"> <div class="swiper-slide" v-for="(slide, index) in skuImageList" :key="slide.id"> <img :src="slide.imgUrl"> </div> </div> <div class="swiper-button-next"></div> <div class="swiper-button-prev"></div> --> <!-- ElementUI写法 --> <el-carousel :autoplay="false" type="card" height="50px" arrow="always" @change="change" trigger="click"> <el-carousel-item v-for="(slide, index) in skuImageList" :key="slide.id" > <img :src="slide.imgUrl" height="40px" :class="{active:currentIndex==index}" style="margin-left: 70px;" @click="changeCurrentIndex(index)"> </el-carousel-item> </el-carousel> </div> </template> <script> import Swiper from 'swiper' export default { name: "ImageList", data(){ return { currentIndex:0, } }, props:['skuImageList',], methods: { changeCurrentIndex(index){ this.currentIndex = index //通知兄弟组件:当前的索引值为几 this.$bus.$emit('getIndex',index) }, change(newIndex,oldIndex){ // console.log(newIndex,oldIndex) this.changeCurrentIndex(newIndex) }, }, } </script> <style lang="less" scoped> .active { border: 2px solid #f60; padding: 1px; } </style>
Zoom.vue 展示照片
<template> <div class="spec-preview"> <img :src="skuImageList[currentIndex].imgUrl" /> <div class="event"></div> <div class="big"> <img :src="skuImageList[currentIndex].imgUrl" /> </div> <div class="mask"></div> </div> </template> <script> export default { name: "Zoom", data(){ return{ currentIndex:0, } }, props:['skuImageList'], mounted() { //全局事件总线:获取兄弟组件传递过来的索引值 this.$bus.$on('getIndex',(index)=>this.currentIndex=index) }, } </script>
zoom.vue添加handler,使用big和mask。
<template> <div class="spec-preview"> <img :src="skuImageList[currentIndex].imgUrl" /> <div class="event" @mousemove="handler"></div> <div class="big"> <img :src="skuImageList[currentIndex].imgUrl" ref="big"/> </div> <div class="mask" ref="mask"></div> </div> </template> <script> export default { name: "Zoom", data(){ return{ currentIndex:0, } }, props:['skuImageList'], mounted() { //全局事件总线:获取兄弟组件传递过来的索引值 this.$bus.$on('getIndex',(index)=>this.currentIndex=index) }, methods: { handler(event) { let mask = this.$refs.mask; let big = this.$refs.big; let left = event.offsetX - mask.offsetWidth/2; let top = event.offsetY - mask.offsetHeight/2; //约束范围 if(left <=0) left = 0; if(left >=mask.offsetWidth) left = mask.offsetWidth; if(top<=0)top = 0; if(top>=mask.offsetHeight) top = mask.offsetHeight; //修改元素的left|top属性值 mask.style.left = left+'px'; mask.style.top = top +'px'; big.style.left = - 2 * left+'px'; big.style.top = -2 * top +'px'; }, }, } </script> <style lang="less"> .spec-preview { position: relative; width: 400px; height: 400px; border: 1px solid #ccc; img { width: 100%; height: 100%; } .event { width: 100%; height: 100%; position: absolute; top: 0; left: 0; z-index: 998; } .mask { width: 50%; height: 50%; background-color: rgba(0, 255, 0, 0.3); position: absolute; left: 0; top: 0; display: none; } .big { width: 100%; height: 100%; position: absolute; top: -1px; left: 100%; border: 1px solid #aaa; overflow: hidden; z-index: 998; display: none; background: white; img { width: 200%; max-width: 200%; height: 200%; position: absolute; left: 0; top: 0; } } .event:hover~.mask, .event:hover~.big { display: block; } } </style>
单纯zoom的插件 zoom2.vue
<template> <div class="spec-preview"> <img src="./images/intro01.png" /> <div class="event" @mousemove="handler"></div> <div class="big"> <img src="./images/intro01.png" ref="big"/> </div> <div class="mask" ref="mask"></div> </div> </template> <script> export default { name: "Zoom2", methods: { handler(event) { let mask = this.$refs.mask; let big = this.$refs.big; let left = event.offsetX - mask.offsetWidth/2; let top = event.offsetY - mask.offsetHeight/2; //约束范围 if(left <=0) left = 0; if(left >=mask.offsetWidth) left = mask.offsetWidth; if(top<=0)top = 0; if(top>=mask.offsetHeight) top = mask.offsetHeight; //修改元素的left|top属性值 mask.style.left = left+'px'; mask.style.top = top +'px'; big.style.left = - 2 * left+'px'; big.style.top = -2 * top +'px'; }, }, } </script> <style lang="less"> .spec-preview { position: relative; width: 400px; height: 400px; border: 1px solid #ccc; img { width: 100%; height: 100%; } .event { width: 100%; height: 100%; position: absolute; top: 0; left: 0; z-index: 998; } .mask { width: 50%; height: 50%; background-color: rgba(0, 255, 0, 0.3); position: absolute; left: 0; top: 0; display: none; } .big { width: 100%; height: 100%; position: absolute; top: -1px; left: 100%; border: 1px solid #aaa; overflow: hidden; z-index: 998; display: none; background: white; img { width: 200%; max-width: 200%; height: 200%; position: absolute; left: 0; top: 0; } } .event:hover~.mask, .event:hover~.big { display: block; } } </style>
使用spuSaleAttrList数据。
Detail/index.vue动态展现属性
<div class="chooseArea">
<div class="choosed"></div>
<dl v-for="(spuSaleAttr,index) in goodInfo.spuSaleAttrList" :key="index">
<dt class="title">{{ spuSaleAttr.saleAttrName }}</dt>
<dd changepirce="0" class="active" v-for="(spuSaleAttrValue,index) in spuSaleAttr.spuSaleAttrValueList" :key="index">{{ spuSaleAttrValue.saleAttrValueName }}</dd>
</dl>
</div>
computed:{
...mapState({
goodInfo: (state) => state.detail.goodInfo,
})
},
Detail/index.vue选择数量和输入值校对
<div class="cartWrap"> <div class="controls"> <input autocomplete="off" class="itxt" v-model="skuNum" @change="changeSkuNum"> <a href="javascript:" @click="skuNum++" class="plus">+</a> <a href="javascript:" @click="skuNum>1 ?skuNum--:1" class="mins">-</a> </div> <div class="add"> <a href="javascript:">加入购物车</a> </div> </div> <script> export default { name: 'Detail', data() { return { skuNum: 1, }; }, methods: { //表单元素修改产品个数 changeSkuNum(event) { //用户输入进来的文本 * 1 let value = event.target.value * 1; //如果用户输入进来的非法,出现NaN或者小于1 if (isNaN(value) || value < 1) { this.skuNum = 1; } else { //正常大于1【大于1整数不能出现小数】 this.skuNum = parseInt(value); } }, }, } </script>
数据结构
Detail/index.vue遍历全部售卖属性值isChecked为零没有高亮了,点击的那个售卖属性值变为1
<div class="choose"> <div class="chooseArea"> <div class="choosed"></div> <dl v-for="(spuSaleAttr,index) in goodInfo.spuSaleAttrList" :key="index"> <dt class="title">{{ spuSaleAttr.saleAttrName }}</dt> <dd changepirce="0" :class="{ active: spuSaleAttrValue.isChecked == 1 }" @click="changeActive(spuSaleAttrValue,spuSaleAttr.spuSaleAttrValueList)" v-for="(spuSaleAttrValue,index) in spuSaleAttr.spuSaleAttrValueList" :key="index">{{ spuSaleAttrValue.saleAttrValueName }}</dd> </dl> </div> methods: { changeActive(saleAttrValue, arr){ arr.forEach(function (item, index) { //遍历全部售卖属性值isChecked为零没有高亮了 item.isChecked = 0 }) //点击的那个售卖属性值变为1 saleAttrValue.isChecked = 1 console.log(saleAttrValue.saleAttrValueName) }, },
api/index.js
//加入购物车 /api/cart/addToCart/{ skuId }/{ skuNum }
export const reqAddOrUpdateShopCart = (skuId,skuNum)=>requests({url:`/cart/addToCart/${skuId}/${skuNum}`,method:"post"});
Detail/index.vue调用api后,根据返回状态直接进行路由跳转,不用去vuex三连。(下面那个params参数skuId只是试验我会传参,是要删掉的)
<div class="add"> <a href="javascript:" @click="addShopcar(goodInfo.skuInfo.id,skuNum)">加入购物车</a> </div> <script> methods: { async addShopcar(skuId,skuNum){ let result = await reqAddOrUpdateShopCart(skuId,skuNum) if (result.code == 200){ this.$router.push({name:'AddCartSuccess',params:{'skuId':skuId},query:{'skuNum':skuNum}}) console.log(this.$route) }else{ alert('error') } } } </script>
router/routes.js设置params参数占位(记得删掉占位skuId)
{
path:'/AddCartSuccess/:skuId?',
name:'AddCartSuccess',
component:AddCartSuccess,
meta:{
showFooter: true,
}
},
当我们想要实现两个毫无关系的组件传递数据时,首相想到的就是路由的query传递参数,但是query适合传递单个数值的简单参数,所以如果想要传递对象之类的复杂信息,就可以通过Web Storage实现。
sessionStorage:为每一个给定的源维持一个独立的存储区域,该区域在页面会话期间可用(即只要浏览器处于打开状态,包括页面重新加载和恢复)。
localStorage:同样的功能,但是在浏览器关闭,然后重新打开后数据仍然存在。
注意:无论是session还是local存储的值都是字符串形式。如果我们想要存储对象,需要在存储前JSON.stringify()将对象转为字符串,在取数据后通过JSON.parse()将字符串转为对象。
Detail/index.vue将数据存储在本地。
async addShopcar(skuId,skuNum){
//直接发请求
let result = await reqAddOrUpdateShopCart(skuId,skuNum)
console.log('result',result)
if (result.code == 200){
// 本地存储
sessionStorage.setItem('GOODINFO',JSON.stringify(this.goodInfo))
// 路由跳转
this.$router.push({name:'AddCartSuccess',query:{'skuNum':skuNum}})
// console.log(this.$route)
}else{
alert('error')
}
}
AddCartSuccess/index.vue读取本地存储并展现。
<template> <div class="cart-complete-wrap"> <div class="cart-complete"> <h3><i class="sui-icon icon-pc-right"></i>商品已成功加入购物车!</h3> <div class="goods"> <div class="left-good"> <div class="left-pic"> <img src="good.skuDefaultImg"> </div> <div class="right-info"> <p class="title">{{ goodInfo.skuInfo.skuName }}</p> <p class="attr">{{ goodInfo.skuInfo.skuDesc }} 数量:{{ $route.query.skuNum }}</p> </div> </div> <div class="right-gocart"> <a href="javascript:" class="sui-btn btn-xlarge">查看商品详情</a> <a href="javascript:" >去购物车结算 > </a> </div> </div> </div> </div> </template> <script> export default { name: 'AddCartSuccess', computed:{ goodInfo(){ console.log(11111,JSON.parse(sessionStorage.getItem('GOODINFO'))) return JSON.parse(sessionStorage.getItem('GOODINFO')) } } } </script>
AddCartSuccess/index.vue
<a href="javascript:" class="sui-btn btn-xlarge" @click="goBack">查看商品详情</a>
也可以这样写
<router-link class="sui-btn btn-xlarge" :to="`/detail/${skuInfo.id}`">查看商品详情</router-link>
AddCartSuccess/index.vue
<router-link href="javascript:" to="/ShopCart">去购物车结算 > </router-link>
但是如果想要获取详细信息,还需要一个用户的uuidToken,用来验证用户身份。但是该请求函数没有参数,所以我们只能把uuidToken加在请求头中。
创建utils工具包文件夹,创建生成uuid的js文件,对外暴露为函数(记得导入uuid => npm install uuid)。
生成临时游客的uuid(随机字符串),每个用户的uuid不能发生变化,还要持久存储。
utils/uuid_token.js
import {v4 as uuidv4} from 'uuid' //生成临时游客的uuid(随机字符串),每个用户的uuid不能发生变化,还要持久存储 export const getUUID = () => { //1、判断本地存储是否由uuid let uuid_token = localStorage.getItem('UUIDTOKEN') //2、本地存储没有uuid if(!uuid_token){ //2.1生成uuid uuid_token = uuidv4() //2.2存储本地 localStorage.setItem("UUIDTOKEN",uuid_token) } //当用户有uuid时就不会再生成 return uuid_token }
用户的uuid_token定义在store中的detail模块
const state = {
goodInfo:{},
//游客身份
uuid_token: getUUID()
}
在request.js中设置请求头
import store from '@/store'; requests.interceptors.request.use(config => { //config内主要是对请求头Header配置 //1、先判断uuid_token是否为空 if(store.state.detail.uuid_token){ //2、userTempId字段和后端统一 config.headers['userTempId'] = store.state.detail.uuid_token } //比如添加token //开启进度条 nprogress.start(); return config; })
注意this.$store只能在组件中使用,不能再js文件中使用。如果要在js中使用,需要引入import store from ‘@/store’;
ShopCart/index.vue动态展示数据,增加小计和总价。
<template> <div class="cart"> <h4>全部商品</h4> <div class="cart-main"> <div class="cart-th"> <div class="cart-th1">全部</div> <div class="cart-th2">商品</div> <div class="cart-th3">单价(元)</div> <div class="cart-th4">数量</div> <div class="cart-th5">小计(元)</div> <div class="cart-th6">操作</div> </div> <div class="cart-body"> <ul class="cart-list" v-for="(cartInfo,index) in cartList[0].cartInfoList" :key="cartInfo.id"> <li class="cart-list-con1"> <input type="checkbox" name="chk_list"> </li> <li class="cart-list-con2"> <img :src="cartInfo.imgUrl"> <div class="item-msg">{{ cartInfo.skuName }}</div> </li> <li class="cart-list-con4"> <span class="price">{{ cartInfo.skuPrice }}</span> </li> <li class="cart-list-con5"> <a href="javascript:void(0)" class="mins">-</a> <input autocomplete="off" type="text" :value="cartInfo.skuNum" minnum="1" class="itxt"> <a href="javascript:void(0)" class="plus">+</a> </li> <li class="cart-list-con6"> <span class="sum">{{ cartInfo.skuNum * cartInfo.skuPrice }}</span> </li> <li class="cart-list-con7"> <a href="#none" class="sindelet">删除</a> <br> <a href="#none">移到收藏</a> </li> </ul> </div> </div> <div class="cart-tool"> <div class="select-all"> <input class="chooseAll" type="checkbox"> <span>全选</span> </div> <div class="option"> <a href="#none">删除选中的商品</a> <a href="#none">移到我的关注</a> <a href="#none">清除下柜商品</a> </div> <div class="money-box"> <div class="chosed">已选择 <span>0</span>件商品</div> <div class="sumprice"> <em>总价(不含运费) :</em> <i class="summoney">{{ totalPrice }}</i> </div> <div class="sumbtn"> <a class="sum-btn" href="###" target="_blank">结算</a> </div> </div> </div> </div> </template> <script> import { mapState } from 'vuex'; export default { name: 'ShopCart', mounted() { this.getData() }, methods: { getData(){ this.$store.dispatch('getCartList') }, }, computed:{ ...mapState({ cartList: (state) => state.shop.cartList, }), totalPrice(){ let sum = 0 this.cartList[0].cartInfoList.forEach(element => { sum += element.skuNum * element.skuPrice }); return sum } }, } </script>
every遍历某个数组,判断数组中的元素是否满足表达式,全部为满足返回true,否则返回false
例如判断底部勾选框是否全部勾选代码部分
//判断底部勾选框是否全部勾选
isAllCheck() {
//every遍历某个数组,判断数组中的元素是否满足表达式,全部为满足返回true,否则返回false
return this.cartInfoList.every(item => item.isChecked === 1)
}
<div class="select-all">
<input class="chooseAll" type="checkbox" :checked="this.isAllCheck">
<span>全选</span>
</div>
重点是,API商品数量是变动值(+100),不就是最终数量(105)。
api/index.js
//加入购物车 /api/cart/addToCart/{ skuId }/{ skuNum }
export const reqAddOrUpdateShopCart = (skuId,skuNum)=>requests({url:`/cart/addToCart/${skuId}/${skuNum}`,method:"post"});
ShopCart/index.vue注意看校对输入值部分。handler函数有三个参数,type区分操作,disNum用于表示数量变化(正负),cart商品的信息。
<li class="cart-list-con5">
<a href="javascript:void(0)" class="mins" @click="handler('minus',-1,cartInfo)">-</a>
<input autocomplete="off" type="text" :value="cartInfo.skuNum" minnum="1" class="itxt" @change="handler('change',$event.target.value*1,cartInfo)">
<a href="javascript:void(0)" class="plus" @click="handler('add',1,cartInfo)">+</a>
</li>
async handler(type,disNum,cartInfo){ if(type == 'minus'){ cartInfo.disNum > 0 ? disNum = -1 : disNum = 0 }else if(type == 'add'){ disNum = 1 }else{ // 检验disNum输入值是否合法 (isNaN(disNum) || disNum < 1)? disNum = 0 : disNum = parseInt(disNum) - cartInfo.skuNum } //直接发请求 let result = await reqAddOrUpdateShopCart(cartInfo.skuId,disNum) console.log('result',result) if (result.code == 200){ this.getData() }else{ alert('error') } }
修改产品数据时,实时发请求。
ShopCart/index.vue
import throttle from 'lodash/throttle'; methods: { handler:throttle(async function(type,disNum,cartInfo){ if(type == 'minus'){ cartInfo.disNum > 0 ? disNum = -1 : disNum = 0 }else if(type == 'add'){ disNum = 1 }else{ // 检验disNum输入值是否合法 (isNaN(disNum) || disNum < 1)? disNum = 0 : disNum = parseInt(disNum) - cartInfo.skuNum } //直接发请求 let result = await reqAddOrUpdateShopCart(cartInfo.skuId,disNum) console.log('result',result) if (result.code == 200){ this.getData() }else{ alert('error') } },1000) }
重点是:delete请求
api/index.js
//删除购物车单个商品 /api/cart/deleteCart/{skuId}
export const reqDeleteCartById = (skuId)=>requests({url:`/cart/deleteCart/${skuId}`,method:"DELETE"});
ShopCart/index.vue
<template> <li class="cart-list-con7"> <a href="#none" class="sindelet" @click="deleteCartById(cartInfo.skuId)">删除</a> <br> <a href="#none">移到收藏</a> </li> </template> <script> import { reqAddOrUpdateShopCart,reqDeleteCartById } from '@/api/index' methods: { //删除购物车商品 async deleteCartById(skuId){ console.log('cartInfo',skuId) let result = await reqDeleteCartById(skuId) console.log('result',result) result.code == 200? this.getData():alert('error') } } </script>
因网速原因,删除可能不及时。
链接
办法就是去掉[0],并使用默认值。
ShopCart/index.vue
<script>
import { mapState,mapGetters } from 'vuex';
import { reqAddOrUpdateShopCart,reqDeleteCartById,reqUpdateCheckedByid } from '@/api/index'
import throttle from 'lodash/throttle';
export default {
computed:{
...mapGetters(['cartList']),
cartInfoList(){
console.log('cartInfoList',this.cartList.cartInfoList)
return this.cartList.cartInfoList || []
},
},
}
</script>
shop.js
import { reqCartList } from "@/api"; //home模块的仓库 const state = { //仓库初始状态 cartList:[], }; //mutions是唯一修改state的地方 const mutations = { GETCARTLIST(state, cartList) { state.cartList = cartList; }, }; //action|用户处理派发action地方的,可以书写异步语句、自己逻辑地方 const actions = { async getCartList({ commit }) { let result = await reqCartList(); if (result.code == 200) { commit("GETCARTLIST", result.data); } }, }; // 计算属性 const getters = { cartList(state) { return state.cartList[0] || {} }, }; export default { state, mutations, actions, getters, };
重点是:其实不太懂为什么商品状态还有分选中不选中?
api/index.js
//修改购物车商品选中状态 /api/cart/checkCart/{skuID}/{isChecked}
export const reqUpdateCheckedByid = (skuId,isChecked)=>requests({url:`/cart/checkCart/${skuId}/${isChecked}`,method:"get"});
ShopCart/index.vue将event事件中的值传给方法。
<template> <li class="cart-list-con1"> <input type="checkbox" name="chk_list" :checked="cartInfo.isChecked" @change="updateChecked(cartInfo,$event)"> </li> </template> <script> import { reqUpdateCheckedByid } from '@/api/index' methods: { //修改某个产品的勾选状态 async updateChecked(cartInfo,e){ let isChecked = e.target.checked? '1':'0' console.log('isChecked',isChecked) let result = await reqUpdateCheckedByid(cartInfo.skuId,isChecked) console.log('result',result) result.code == 200? this.getData():alert('error') } } </script>
ShopCart/index.vue循环删除单个商品。this.getData()要放在循环外面。
<template> <a href="#none" class="sindelet" @click="deleteCartById(cartInfo.skuId)">删除</a> </template> <script> import { reqDeleteCartById } from '@/api/index' methods: { //删除全部选中商品 deleteAllCheckedCart(cartInfoList){ cartInfoList.forEach(async function(element){ if (element.isChecked == '1'){ // console.log('isChecked',element.isChecked) let result = await reqDeleteCartById(element.skuId) console.log('result',result) } } ) //再发请求获取购物车列表,需要放在for循环外面 this.getData() }, } </script>
点击【删除选中的商品】
因网速原因,删除可能不及时。
![![在这里插入图片描述](https://img-blog.csdnimg.cn/9653ab556b324e6bb8b327f8b83456b8.png)
办法就是使用mapGetter去掉[0],并使用默认值。
ShopCart/index.vue
<ul class="cart-list" v-for="(cartInfo,index) in cartInfoList" :key="cartInfo.id"> <script> import { mapState,mapGetters } from 'vuex'; import { reqAddOrUpdateShopCart,reqDeleteCartById,reqUpdateCheckedByid } from '@/api/index' import throttle from 'lodash/throttle'; export default { methods: { getData(){ this.$store.dispatch('getCartList') }, //选中全部商品 async updateAllCartChecked(e,cartInfoList){ let isChecked = e.target.checked? '1':'0' cartInfoList.forEach(async function(element){ let result = await reqUpdateCheckedByid(element.skuId,isChecked) console.log('updateChecked',result) }) console.log('this',this) this.getData() }, }, computed:{ ...mapGetters(['cartList']), cartInfoList(){ console.log('cartInfoList',this.cartList.cartInfoList) return this.cartList.cartInfoList || [] }, }, } </script>
shop.js
import { reqCartList } from "@/api"; //home模块的仓库 const state = { //仓库初始状态 cartList:[], }; //mutions是唯一修改state的地方 const mutations = { GETCARTLIST(state, cartList) { state.cartList = cartList; }, }; //action|用户处理派发action地方的,可以书写异步语句、自己逻辑地方 const actions = { async getCartList({ commit }) { let result = await reqCartList(); if (result.code == 200) { commit("GETCARTLIST", result.data); } }, }; // 计算属性 const getters = { cartList(state) { return state.cartList[0] || {} }, }; export default { state, mutations, actions, getters, };
<div class="select-all"> <input class="chooseAll" type="checkbox" :checked="this.isAllCheck" @click="updateAllCartChecked($event,cartList[0].cartInfoList)"> <span>全选</span> </div> //选中全部商品 async updateAllCartChecked(e,cartInfoList){ let isChecked = e.target.checked? '1':'0' cartInfoList.forEach(async function(element){ let result = await reqUpdateCheckedByid(element.skuId,isChecked) console.log('updateChecked',result) if (result.code == 200) { return "ok"; } else { return Promise.reject(new Error("faile")); } }) console.log('this',this) this.getData() },
方法就是写在computed里面,不要写在methods里面。
<div class="select-all"> <input class="chooseAll" type="checkbox" :checked="isAllCheck && cartInfoList.length>0" @click="updateAllCartChecked($event,cartInfoList)"> <span>全选</span> </div> computed:{ //判断底部复选框是否勾选【全部产品都选中,采勾选】 isAllCheck() { //遍历数组里面原理,只要全部元素isChecked属性都为1===>真 true //只要有一个不是1======>假false let result = this.cartInfoList.every((item) => item.isChecked == 1) console.log('isAllCheck',result) return result },
const {comment,index,deleteComment} = this
上面的这句话是一个简写,最终的含义相当于:
const comment = this.comment
const index = this.index
const deleteComment = this.deleteComment
Register/index.vue
<template> <div class="register-container"> <!-- 注册内容 --> <div class="register"> <h3>注册新用户 <span class="go">我有账号,去 <a href="login.html" target="_blank">登陆</a> </span> </h3> <div class="content"> <label>手机号:</label> <input type="text" placeholder="请输入你的手机号"> <span class="error-msg">错误提示信息</span> </div> <div class="content"> <label>验证码:</label> <input type="text" placeholder="请输入验证码"> <img ref="code" src="http://182.92.128.115/api/user/passport/code" alt="code"> <span class="error-msg">错误提示信息</span> </div> <div class="content"> <label>登录密码:</label> <input type="text" placeholder="请输入你的登录密码"> <span class="error-msg">错误提示信息</span> </div> <div class="content"> <label>确认密码:</label> <input type="text" placeholder="请输入确认密码"> <span class="error-msg">错误提示信息</span> </div> <div class="controls"> <input name="m1" type="checkbox"> <span>同意协议并注册《尚品汇用户协议》</span> <span class="error-msg">错误提示信息</span> </div> <div class="btn"> <button>完成注册</button> </div> </div> <!-- 底部 --> <div class="copyright"> <ul> <li>关于我们</li> <li>联系我们</li> <li>联系客服</li> <li>商家入驻</li> <li>营销中心</li> <li>手机尚品汇</li> <li>销售联盟</li> <li>尚品汇社区</li> </ul> <div class="address">地址:北京市昌平区宏福科技园综合楼6层</div> <div class="beian">京ICP备19006430号 </div> </div> </div> </template> <script> export default { name: 'Register' } </script> <style lang="less" scoped> .register-container { .register { width: 1200px; height: 445px; border: 1px solid rgb(223, 223, 223); margin: 0 auto; h3 { background: #ececec; margin: 0; padding: 6px 15px; color: #333; border-bottom: 1px solid #dfdfdf; font-size: 20.04px; line-height: 30.06px; span { font-size: 14px; float: right; a { color: #e1251b; } } } div:nth-of-type(1) { margin-top: 40px; } .content { padding-left: 390px; margin-bottom: 18px; position: relative; label { font-size: 14px; width: 96px; text-align: right; display: inline-block; } input { width: 270px; height: 38px; padding-left: 8px; box-sizing: border-box; margin-left: 5px; outline: none; border: 1px solid #999; } img { vertical-align: sub; } .error-msg { position: absolute; top: 100%; left: 495px; color: red; } } .controls { text-align: center; position: relative; input { vertical-align: middle; } .error-msg { position: absolute; top: 100%; left: 495px; color: red; } } .btn { text-align: center; line-height: 36px; margin: 17px 0 0 55px; button { outline: none; width: 270px; height: 36px; background: #e1251b; color: #fff !important; display: inline-block; font-size: 16px; } } } .copyright { width: 1200px; margin: 0 auto; text-align: center; line-height: 24px; ul { li { display: inline-block; border-right: 1px solid #e4e4e4; padding: 0 20px; margin: 15px 0; } } } } </style>
Login/index.vue
<template> <div class="login-container"> <!-- 登录 --> <div class="login-wrap"> <div class="login"> <div class="loginform"> <ul class="tab clearFix"> <li> <a href="##" style="border-right: 0;">扫描登录</a> </li> <li> <a href="##" class="current">账户登录</a> </li> </ul> <div class="content"> <form action="##"> <div class="input-text clearFix"> <span></span> <input type="text" placeholder="邮箱/用户名/手机号"> </div> <div class="input-text clearFix"> <span class="pwd"></span> <input type="text" placeholder="请输入密码"> </div> <div class="setting clearFix"> <label class="checkbox inline"> <input name="m1" type="checkbox" value="2" checked=""> 自动登录 </label> <span class="forget">忘记密码?</span> </div> <button class="btn">登 录</button> </form> <div class="call clearFix"> <ul> <li><img src="./images/qq.png" alt=""></li> <li><img src="./images/sina.png" alt=""></li> <li><img src="./images/ali.png" alt=""></li> <li><img src="./images/weixin.png" alt=""></li> </ul> <router-link class="register" to="/register">立即注册</router-link> </div> </div> </div> </div> </div> <!-- 底部 --> <div class="copyright"> <ul> <li>关于我们</li> <li>联系我们</li> <li>联系客服</li> <li>商家入驻</li> <li>营销中心</li> <li>手机尚品汇</li> <li>销售联盟</li> <li>尚品汇社区</li> </ul> <div class="address">地址:北京市昌平区宏福科技园综合楼6层</div> <div class="beian">京ICP备19006430号 </div> </div> </div> </template> <script> export default { name: 'Login', } </script> <style lang="less" scoped> .login-container { .login-wrap { height: 487px; background-color: #e93854; .login { width: 1200px; height: 487px; margin: 0 auto; background: url(./images/loginbg.png) no-repeat; } .loginform { width: 420px; height: 406px; box-sizing: border-box; background: #fff; float: right; top: 45px; position: relative; padding: 20px; .tab { li { width: 50%; float: left; text-align: center; a { width: 100%; display: block; height: 50px; line-height: 50px; font-size: 20px; font-weight: 700; color: #333; border: 1px solid #ddd; box-sizing: border-box; text-decoration: none; } .current { border-bottom: none; border-top-color: #28a3ef; color: #e1251b; } } } .content { width: 380px; height: 316px; box-sizing: border-box; border: 1px solid #ddd; border-top: none; padding: 18px; form { margin: 15px 0 18px 0; font-size: 12px; line-height: 18px; .input-text { margin-bottom: 16px; span { float: left; width: 37px; height: 32px; border: 1px solid #ccc; background: url(../../assets/images/icons.png) no-repeat -10px -201px; box-sizing: border-box; border-radius: 2px 0 0 2px; } .pwd { background-position: -72px -201px; } input { width: 302px; height: 32px; box-sizing: border-box; border: 1px solid #ccc; border-left: none; float: left; padding-top: 6px; padding-bottom: 6px; font-size: 14px; line-height: 22px; padding-right: 8px; padding-left: 8px; border-radius: 0 2px 2px 0; outline: none; } } .setting { label { float: left; } .forget { float: right; } } .btn { background-color: #e1251b; padding: 6px; border-radius: 0; font-size: 16px; font-family: 微软雅黑; word-spacing: 4px; border: 1px solid #e1251b; color: #fff; width: 100%; height: 36px; margin-top: 25px; outline: none; } } .call { margin-top: 30px; ul { float: left; li { float: left; margin-right: 5px; } } .register { float: right; font-size: 15px; line-height: 38px; } .register:hover { color: #4cb9fc; text-decoration: underline; } } } } } .copyright { width: 1200px; margin: 0 auto; text-align: center; line-height: 24px; ul { li { display: inline-block; border-right: 1px solid #e4e4e4; padding: 0 20px; margin: 15px 0; } } } } </style>
api/index.js
//获取注册验证码 /api/user/passport/sendCode/{phone}
export const reqGetCode = (phone)=>requests({url:`/user/passport/sendCode/${phone}`,method:"get"});
//获取注册验证码 /api/user/passport/register
export const reqUserRegister = ({phone,password,code})=>requests(
{url:`user/passport/register`,
method:"post",
data:{phone,password,code}}
);
Register/index.vue
<template> <div class="register-container"> <!-- 注册内容 --> <div class="register"> <h3>注册新用户 <span class="go">我有账号,去 <a href="login.html" target="_blank">登陆</a> </span> </h3> <div class="content"> <label>手机号:</label> <input type="text" placeholder="请输入你的手机号" v-model="phone"> <span class="error-msg">错误提示信息</span> </div> <div class="content"> <label>验证码:</label> <input type="text" placeholder="请输入验证码" v-model="code"> <button style="width:100px;height:38px" @click="getCode"> 获取验证码 </button> <span class="error-msg">错误提示信息</span> </div> <div class="content"> <label>登录密码:</label> <input type="text" placeholder="请输入你的登录密码" v-model="password"> <span class="error-msg">错误提示信息</span> </div> <div class="content"> <label>确认密码:</label> <input type="text" placeholder="请输入确认密码" v-model="password1"> <span class="error-msg">错误提示信息</span> </div> <div class="controls"> <input name="m1" type="checkbox" v-model="agree"> <span>同意协议并注册《尚品汇用户协议》</span> <span class="error-msg">错误提示信息</span> </div> <div class="btn"> <button @click="UserRegister">完成注册</button> </div> </div> <!-- 底部 --> <div class="copyright"> <ul> <li>关于我们</li> <li>联系我们</li> <li>联系客服</li> <li>商家入驻</li> <li>营销中心</li> <li>手机尚品汇</li> <li>销售联盟</li> <li>尚品汇社区</li> </ul> <div class="address">地址:北京市昌平区宏福科技园综合楼6层</div> <div class="beian">京ICP备19006430号 </div> </div> </div> </template> <script> export default { name: 'Register', data() { return { phone:'', code:'', password:'', password1:'', agree:true, } }, methods: { //获取验证码 async getCode(){ const { phone } = this; phone && (await this.$store.dispatch('getCode',phone)); this.code = this.$store.state.user.code; }, //注册用户 async UserRegister(){ const { phone,password,password1,code,agree } = this; if ( phone && password && password1==password && code && agree) { let result = await this.$store.dispatch('UserRegister',{phone,password,code}) console.log('UserRegister result',result) if(result){ //注册成功进行路由的跳转 this.$router.push("/login"); } } } }, } </script>
store/user.js
import { reqGetCode,reqUserRegister } from "@/api"; //home模块的仓库 const state = { phone:'', code: '', password:'', agree:'', }; //mutions是唯一修改state的地方 const mutations = { GETCODE(state, code) { state.code = code; }, }; //action|用户处理派发action地方的,可以书写异步语句、自己逻辑地方 const actions = { async getCode({ commit },phone) { let result = await reqGetCode(phone); if (result.code == 200) { commit("GETCODE", result.data); } }, async UserRegister({ commit },{phone,password,code}) { let result = await reqUserRegister({phone,password,code}); console.log('vuex UserRegister',result) if (result.code == 200) { return "ok"; } }, }; //计算属性 const getters = {}; export default { state, mutations, actions, getters, };
注册功能就返回ok,并跳转到登录页面
api/index.js
//用户登录 /api/user/passport/login
export const reqUserLogin = ({phone,password})=>requests(
{url:`user/passport/login`,
method:"post",
data:{phone,password}}
);
Login/index.vue关键点在于参数{phone,password}
<template> <div class="login-container"> <!-- 登录 --> <div class="login-wrap"> <div class="login"> <div class="loginform"> <ul class="tab clearFix"> <li> <a href="##" style="border-right: 0;">扫描登录</a> </li> <li> <a href="##" class="current">账户登录</a> </li> </ul> <div class="content"> <form action="##"> <div class="input-text clearFix"> <span></span> <input type="text" placeholder="邮箱/用户名/手机号" v-model="phone"> </div> <div class="input-text clearFix"> <span class="pwd"></span> <input type="text" placeholder="请输入密码" v-model="password"> </div> <div class="setting clearFix"> <label class="checkbox inline"> <input name="m1" type="checkbox" value="2" checked=""> 自动登录 </label> <span class="forget">忘记密码?</span> </div> <button class="btn" @click="UserLogin">登 录</button> </form> <div class="call clearFix"> <ul> <li><img src="./images/qq.png" alt=""></li> <li><img src="./images/sina.png" alt=""></li> <li><img src="./images/ali.png" alt=""></li> <li><img src="./images/weixin.png" alt=""></li> </ul> <router-link class="register" to="/register">立即注册</router-link> </div> </div> </div> </div> </div> <!-- 底部 --> <div class="copyright"> <ul> <li>关于我们</li> <li>联系我们</li> <li>联系客服</li> <li>商家入驻</li> <li>营销中心</li> <li>手机尚品汇</li> <li>销售联盟</li> <li>尚品汇社区</li> </ul> <div class="address">地址:北京市昌平区宏福科技园综合楼6层</div> <div class="beian">京ICP备19006430号 </div> </div> </div> </template> <script> export default { name: 'Login', data() { return { phone:'', password:'', nickName:'', name:'', token:'', } }, methods: { //用户登录 async UserLogin(){ const { phone,password } = this; if ( phone && password) { console.log('phone',phone) let result = await this.$store.dispatch('UserLogin',{phone,password}) console.log('result',result) if(result){ //注册成功进行路由的跳转 this.$router.push("/home"); } } } }, } </script>
store/user.js
import { reqGetCode,reqUserRegister,reqUserLogin } from "@/api"; //home模块的仓库 const state = { phone:'', code: '', password:'', agree:'', nickName:'', name:'', token:'', }; //mutions是唯一修改state的地方 const mutations = { GETCODE(state, code) { state.code = code; }, USERLOGIN(state, data) { state.nickName = data.nickName; state.name = data.name; state.token = data.token; }, }; //action|用户处理派发action地方的,可以书写异步语句、自己逻辑地方 const actions = { async getCode({ commit },phone) { let result = await reqGetCode(phone); if (result.code == 200) { commit("GETCODE", result.data); } }, async UserRegister({ commit },{phone,password,code}) { let result = await reqUserRegister({phone,password,code}); console.log('vuex UserRegister',result) if (result.code == 200) { return "ok"; } }, async UserLogin({ commit },{phone,password}) { let result = await reqUserLogin({phone,password}); console.log('vuex UserLogin',result) if (result.code == 200) { commit("USERLOGIN", result.data); return "ok"; } }, }; //计算属性 const getters = {}; export default { state, mutations, actions, getters, };
登录成功的截图
api/index.js
//获取用户信息【需要带着用户的token向服务器要用户信息】
//URL:/api/user/passport/auth/getUserInfo method:get
export const reqUserInfo = ()=>requests({url:'/user/passport/auth/getUserInfo',method:'get'});
store/user.js储存数据(UserLogin之setToken(result.data.token), getUserInfo获取用户信息)
import { reqGetCode,reqUserRegister,reqUserLogin,reqUserInfo } from "@/api"; import {setToken,getToken,removeToken } from "@/utils/token" //仓库 const state = { code: '', token: getToken(), userInfo: {}, }; //mutions是唯一修改state的地方 const mutations = { GETCODE(state, code) { state.code = code; }, USERLOGIN(state, token) { state.token = token; }, GETUSERINFO(state, userInfo) { state.userInfo = userInfo; }, }; //action|用户处理派发action地方的,可以书写异步语句、自己逻辑地方 const actions = { async getCode({ commit },phone) { let result = await reqGetCode(phone); if (result.code == 200) { commit("GETCODE", result.data); } }, async UserRegister({ commit },{phone,password,code}) { let result = await reqUserRegister({phone,password,code}); console.log('vuex UserRegister',result) if (result.code == 200) { return "ok"; } }, async UserLogin({ commit },{phone,password}) { let result = await reqUserLogin({phone,password}); console.log('vuex UserLogin',result) if (result.code == 200) { commit("USERLOGIN", result.data.token); //持久化存储token setToken(result.data.token); return "ok"; } }, //获取用户信息 async getUserInfo({ commit }) { let result = await reqUserInfo(); console.log('getUserInfo',result) if (result.code == 200) { //提交用户信息 commit("GETUSERINFO", result.data); return 'ok'; }else{ return Promise.reject(new Error('faile')); } }, // 退出登录 // async userLogout({commit}) { // //只是向服务器发起一次请求,通知服务器清除token // let result = await reqLogout(); // //action里面不能操作state,提交mutation修改state // if(result.code==200){ // commit("CLEAR"); // return 'ok'; // }else{ // return Promise.reject(new Error('faile')); // } // }, }; //计算属性 const getters = {}; export default { state, mutations, actions, getters, };
utils/tokens.js定义token的存储、获取、消除方法
//存储token
export const setToken = (token) => {
localStorage.setItem("TOKEN", token);
};
//获取token
export const getToken = () => {
return localStorage.getItem("TOKEN");
};
//清除本地存储的token
export const removeToken=()=>{
localStorage.removeItem("TOKEN");
}
发请求,读取数据
Home/index.vue在mounted时自动读取用户信息。
<template> <div> <!-- 三级联动全局组件已经注册为全局组件,因此不需要引入--> <TypeNav/> <ListContainer/> <Recommend/> <Rank/> <Like/> <Floor v-for="(floor,index) in floorList" :key="floor.id" :list="floor"/> <Brand/> </div> </template> <script> import ListContainer from '@/views/Home/ListContainer' import Recommend from '@/views/Home/Recommend' import Rank from '@/views/Home/Rank' import Like from '@/views/Home/Like' import Floor from '@/views/Home/Floor' import Brand from '@/views/Home/Brand' import { mapState } from 'vuex' export default { name: 'Home', components:{ ListContainer, Recommend, Rank, Like, Floor, Brand, }, mounted(){ //派发action,获取floor组件的数据 this.$store.dispatch('getFloorList'); this.$store.dispatch('getUserInfo'); }, computed: { ...mapState({ floorList: (state) => state.home.floorList, }), } } </script>
Header展示数据:userName()
<template> <header class="header"> <!-- 头部的第一行 --> <!-- 没有用户名:未登录 --> <p v-if="!userName"> <span>请</span> <router-link to="/login">登录</router-link> <router-link class="register" to="/register">免费注册</router-link> </p> <!-- 已登录 --> <p v-else> <a>{{ userName }}</a> <a>退出登录</a> </p> </div> </header> </template> <script> export default { name: 'Header', data() { return { keyword:'', } }, methods:{ goSearch(){ }, mounted() { }, computed:{ userName(){ return this.$store.state.user.userInfo.name } } } </script>
request.js带请求头token,否则请求失败。
//需要携带token带给服务器
if(store.state.user.token){
config.headers.token = store.state.user.token;
}
未带请求头token,发生208错误
带请求头token,用户验证成功
解法1:将读取用户信息放在公共组件
App.vue在mounted时读取用户信息
mounted(){
this.$store.dispatch('getUserInfo');
},
解法2:路由守卫
api/index.js
//退出登录
//URL:/api/user/passport/logout get
export const reqLogout = ()=> requests({url:'/user/passport/logout',method:'get'});
1.帮仓库中先关用户信息清空
2.本地存储数据清空
utils/tokens.js定义token的存储、获取、消除方法
//存储token
export const setToken = (token) => {
localStorage.setItem("TOKEN", token);
};
//获取token
export const getToken = () => {
return localStorage.getItem("TOKEN");
};
//清除本地存储的token
export const removeToken=()=>{
localStorage.removeItem("TOKEN");
}
vuex三连
import { reqGetCode,reqUserRegister,reqUserLogin,reqUserInfo,reqLogout } from "@/api"; import {setToken,getToken,removeToken } from "@/utils/token" //仓库 const state = { code: '', token: getToken(), userInfo: {}, }; //mutions是唯一修改state的地方 const mutations = { //清除本地数据 CLEAR(state){ //帮仓库中先关用户信息清空 state.token = ''; state.userInfo={}; //本地存储数据清空 removeToken(); } }; //action|用户处理派发action地方的,可以书写异步语句、自己逻辑地方 const actions = { //获取用户信息 async getUserInfo({ commit }) { let result = await reqUserInfo(); console.log('getUserInfo',result) if (result.code == 200) { //提交用户信息 commit("GETUSERINFO", result.data); return 'ok'; }else{ return Promise.reject(new Error('faile')); } }, // 退出登录 async userLogout({commit}) { //只是向服务器发起一次请求,通知服务器清除token let result = await reqLogout(); //action里面不能操作state,提交mutation修改state if(result.code==200){ commit("CLEAR"); return 'ok'; }else{ return Promise.reject(new Error('faile')); } }, }; //计算属性 const getters = {}; export default { state, mutations, actions, getters, };
Header组件定义退出方法logout()
<template> <header class="header"> <!-- 头部的第一行 --> <div class="top"> <div class="container"> <div class="loginList"> <p>尚品汇欢迎您!</p> <!-- 没有用户名:未登录 --> <p v-if="!userName"> <span>请</span> <router-link to="/login">登录</router-link> <router-link class="register" to="/register">免费注册</router-link> </p> <!-- 已登录 --> <p v-else> <a>{{ userName }}</a> <a @click="logout">退出登录</a> </p> </div> <div class="typeList"> <a href="###">我的订单</a> <a href="###">我的购物车</a> <a href="###">我的尚品汇</a> <a href="###">尚品汇会员</a> <a href="###">企业采购</a> <a href="###">关注尚品汇</a> <a href="###">合作招商</a> <a href="###">商家后台</a> </div> </div> </div> <!--头部第二行 搜索区域--> <div class="bottom"> <h1 class="logoArea"> <router-link class="logo" title="尚品汇" to="/"> <img src="./images/logo.png" alt=""> </router-link> </h1> <div class="searchArea"> <form action="###" class="searchForm"> <input type="text" id="autocomplete" class="input-error input-xxlarge" v-model="keyword"/> <button class="sui-btn btn-xlarge btn-danger" type="button" @click="goSearch">搜索</button> </form> </div> </div> </header> </template> <script> export default { name: 'Header', data() { return { keyword:'', } }, methods:{ goSearch(){ //治标不治本 // this.$router.push(`/search/${this.keyword}`,()=>{},()=>{}) if (this.$route.query) { let location = { name: "search", params:{keyword:this.keyword || undefined} } //动态给location配置对象添加query属性 location.query = this.$route.query; //路由跳转 this.$router.push(location); } }, //退出登录 async logout(){ //退出登录需要做的事情 //1:需要发请求,通知服务器退出登录【清除一些数据:token】 //2:清除项目当中的数据【userInfo、token】 try { //如果退出成功 await this.$store.dispatch('userLogout'); //回到首页 this.$router.push('/home'); } catch (error) { } } }, mounted() { //通过全局事件总线清除关键字 this.$bus.$on("clear", () => { this.keyword = ""; }); }, computed:{ userName(){ return this.$store.state.user.userInfo.name } } } </script>
https://blog.csdn.net/GyaoG/article/details/124081770
#user nobody; worker_processes 1; #error_log logs/error.log; #error_log logs/error.log notice; #error_log logs/error.log info; #pid logs/nginx.pid; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' # '$status $body_bytes_sent "$http_referer" ' # '"$http_user_agent" "$http_x_forwarded_for"'; #access_log logs/access.log main; sendfile on; #tcp_nopush on; #keepalive_timeout 0; keepalive_timeout 65; #gzip on; server { listen 5005; server_name localhost; #charset koi8-r; #access_log logs/host.access.log main; location / { root demo/static; index index.html index.htm; } location /api { proxy_pass http://gmall-h5-api.atguigu.cn; } #error_page 404 /404.html; # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } # proxy the PHP scripts to Apache listening on 127.0.0.1:80 # #location ~ \.php$ { # proxy_pass http://127.0.0.1; #} # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 # #location ~ \.php$ { # root html; # fastcgi_pass 127.0.0.1:9000; # fastcgi_index index.php; # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; # include fastcgi_params; #} # deny access to .htaccess files, if Apache's document root # concurs with nginx's one # #location ~ /\.ht { # deny all; #} } # another virtual host using mix of IP-, name-, and port-based configuration # #server { # listen 8000; # listen somename:8080; # server_name somename alias another.alias; # location / { # root html; # index index.html index.htm; # } #} # HTTPS server # #server { # listen 443 ssl; # server_name localhost; # ssl_certificate cert.pem; # ssl_certificate_key cert.key; # ssl_session_cache shared:SSL:1m; # ssl_session_timeout 5m; # ssl_ciphers HIGH:!aNULL:!MD5; # ssl_prefer_server_ciphers on; # location / { # root html; # index index.html index.htm; # } #} }
https://blog.csdn.net/weixin_42116703/article/details/127482331
sudo apt-get install nginx
1.sudo vim修改nginx配置default
season@ZHS-190213650:/$ ls
bin dev home lib lib64 lost+found mnt proc run snap sys usr
boot etc init lib32 libx32 media opt root sbin srv tmp var
season@ZHS-190213650:/$ cd etc
season@ZHS-190213650:/etc$ cd nginx
season@ZHS-190213650:/etc/nginx$ ls
conf.d fastcgi_params koi-win modules-available nginx.conf scgi_params sites-enabled uwsgi_params
fastcgi.conf koi-utf mime.types modules-enabled proxy_params sites-available snippets win-utf
season@ZHS-190213650:/etc/nginx$ cd sites-available
season@ZHS-190213650:/etc/nginx/sites-available$ sudo vim
[sudo] password for season:
season@ZHS-190213650:/etc/nginx/sites-available$ sudo vim sites-available
season@ZHS-190213650:/etc/nginx/sites-available$ ls
default sites-available
season@ZHS-190213650:/etc/nginx/sites-available$ sudo vim default
2.Vim指令https://blog.csdn.net/dotdotyy/article/details/120108308
若权限不足,则用sudo vim打开default。
3.default配置(个人觉得配置用windows那一版也可以)
server { listen 5005 default_server; #listen [::]:80 default_server; # SSL configuration # # listen 443 ssl default_server; # listen [::]:443 ssl default_server; # # Note: You should disable gzip for SSL traffic. # See: https://bugs.debian.org/773332 # # Read up on ssl_ciphers to ensure a secure configuration. # See: https://bugs.debian.org/765782 # # Self signed certs generated by the ssl-cert package # Don't use them in a production server! # # include snippets/snakeoil.conf; #root /var/www/html; root /home/season/myseason/myfile/dist/static; # Add index.php to the list if you are using PHP index index.html index.htm index.nginx-debian.html; server_name _; location / { # First attempt to serve request as file, then # as directory, then fall back to displaying a 404. try_files $uri $uri/ =404; } location /api { proxy_pass http://gmall-h5-api.atguigu.cn; } # pass PHP scripts to FastCGI server # #location ~ \.php$ { # include snippets/fastcgi-php.conf; # # # With php-fpm (or other unix sockets): # fastcgi_pass unix:/var/run/php/php7.4-fpm.sock; # # With php-cgi (or other tcp sockets): # fastcgi_pass 127.0.0.1:9000; #} # deny access to .htaccess files, if Apache's document root # concurs with nginx's one # #location ~ /\.ht { # deny all; #} } # Virtual Host configuration for example.com # # You can move that to a different file under sites-available/ and symlink that # to sites-enabled/ to enable it. # #server { # listen 80; # listen [::]:80; # # server_name example.com; # # root /var/www/example.com; # index index.html; # # location / { # try_files $uri $uri/ =404; # } #}
sudo service nginx restart
很多时候导航列表无法带出,有时候可以带出。不知道bug在哪?
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。