014之前可参考官方笔记
https://blog.csdn.net/weixin_43424325/article/details/121684101
015-axios二次封装
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')
018-vuex模块式开发
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,
};
019-026 三级联动数据
019-动态展示三级联动数据
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>
020-完成三级联动动态背景
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>
021-通过JS控制二三级
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>
025-三级联动节流
插件官网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>
026-三级联动路由跳转
方法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);
}
}
},
029-Search模块中商品分类与过渡动画
商品分类
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;
}
030-typeNav商品分类列
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>
031-合并参数
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>
032-mockjs模拟数据 & 033-尚硅谷-尚品汇-获取Banner轮播图数据
官网链接
第一步:安装依赖包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>
034~036-Banner实现轮播图
【24、swiper插件实现轮播图】
官方做法请见https://blog.csdn.net/weixin_43424325/article/details/121684101中的【24、swiper插件实现轮播图】。
ElementUI轮播图用10行代码就能解决
链接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>
最终效果:
037~038-动态展示Floor组件(ElementUI做轮播图)
1.获取floor组件mock数据
2.分析数据结构
3.动态展示Floor组件(ElementUI做轮播图)
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>
039-将轮播图模块提取为公共组件
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`"/>
040~042-search模块中动态展现数据
API、vuex三连、mapstate、动态展现
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
044-Search模块中子组件(SearchSelector)动态展示
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 数据结构
045-监听路由的变化再发请求获取数据
watch监听路由的变化再发请求获取数据
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只能带一个
三个ID只能带一个,所以需要重置。分类名字与关键字不用清理:因为每一次路由发生变化的时候,都会给他赋予新的数据
//分类名字与关键字不用清理:因为每一次路由发生变化的时候,都会给他赋予新的数据
this.searchParams.category1Id = undefined;
this.searchParams.category2Id = undefined;
this.searchParams.category3Id = undefined;
//分类Season清掉
this.searchParams.categoryName = undefined;
this.searchParams.keyword = undefined;
046~049-面包屑
面包屑categoryName
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>
面包屑keyword
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
第四种 插槽(适用于父子组件通信)
面包屑trademark
点击子组件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>
面包屑props
点击子组件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>
050~051-排序
排序的逻辑比较简单,只是改变一下请求参数中的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>
052~058-分页器
先查看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}
}
ElementUI的分页器
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)
059-滚动条
使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。 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细节(*****)
访问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||{}
060~068 产品详情Detail
路由的坑
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的插件(可单独使用)
单纯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)
},
},
069-073 加入购物车
API接口说明
api/index.js
//加入购物车 /api/cart/addToCart/{ skuId }/{ skuNum }
export const reqAddOrUpdateShopCart = (skuId,skuNum)=>requests({url:`/cart/addToCart/${skuId}/${skuNum}`,method:"post"});
直接发请求并路由跳转(没必要vuex储存数据)
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传递参数,但是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>
073-075 购物车之一:获取购物车列表
API接口
如果没有uuidToken,返回数据为空
但是如果想要获取详细信息,还需要一个用户的uuidToken,用来验证用户身份。但是该请求函数没有参数,所以我们只能把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函数使用)
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>
076-077 购物车之二:购物车的商品数量变动
复用加入购物车的API
重点是,API商品数量是变动值(+100),不就是最终数量(105)。
api/index.js
//加入购物车 /api/cart/addToCart/{ skuId }/{ skuNum }
export const reqAddOrUpdateShopCart = (skuId,skuNum)=>requests({url:`/cart/addToCart/${skuId}/${skuNum}`,method:"post"});
用handler函数处理数量变动(直接发请求,不用vuex)
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')
}
}
修改产品数据时,实时发请求。
加入节流throttle
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)
}
078 购物车之三:删除购物车的商品
删除购物车的商品的API
重点是:delete请求
api/index.js
//删除购物车单个商品 /api/cart/deleteCart/{skuId}
export const reqDeleteCartById = (skuId)=>requests({url:`/cart/deleteCart/${skuId}`,method:"DELETE"});
用函数处理删除购物车的商品(有bug)
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>
因网速原因,删除可能不及时。
用函数处理删除购物车的商品(没有bug)
链接
办法就是去掉[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,
};
079 购物车之四:修改购物车的商品状态
修改购物车的商品的API
重点是:其实不太懂为什么商品状态还有分选中不选中?
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>
081 购物车之五:删除全部选中商品
删除购物车的商品的API【复用删除单个商品的API】
删除全部选中商品(有bug)
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>
点击【删除选中的商品】
因网速原因,删除可能不及时。
bug:多于2个可正常删除后,剩下最后一栏无法删除会出现bug。
![![在这里插入图片描述](https://img-blog.csdnimg.cn/9653ab556b324e6bb8b327f8b83456b8.png)
删除全部选中商品(没有bug)
办法就是使用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,
};
082 购物车之六:全部产品的勾选状态修改
全部产品的勾选状态修改的API【复用勾选单个商品的API】
全部产品的勾选状态修改(有bug)
<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()
},
全部产品的勾选状态修改(没有bug)
方法就是写在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
},
ES6 const新用法
const {comment,index,deleteComment} = this
上面的这句话是一个简写,最终的含义相当于:
const comment = this.comment
const index = this.index
const deleteComment = this.deleteComment
083-089 登录和注册
083-085 注册
静态页面
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:获取验证码、注册用户
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}}
);
函数处理:获取验证码、注册用户(关键点在于参数的格式{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,并跳转到登录页面
086-088 登录
API接口:登录
api/index.js
//用户登录 /api/user/passport/login
export const reqUserLogin = ({phone,password})=>requests(
{url:`user/passport/login`,
method:"post",
data:{phone,password}}
);
函数处理:用户登录(登录成功后跳转home页)
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接口:获取用户登录信息
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,用户验证成功
bug:跳出了home组件就会失去登录信息
解法1:将读取用户信息放在公共组件
App.vue在mounted时读取用户信息
mounted(){
this.$store.dispatch('getUserInfo');
},
解法2:路由守卫
089 退出登录
API:退出登录
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>