赞
踩
后台接口: http://gmall-h5-api.atguigu.cn
百度网盘:链接: https://pan.baidu.com/s/15Ad4sJp_clhCN29RYa6dcw?pwd=5hx3 提取码: 5hx3
npm i vuex@3
import { ... } from "@/api";
const state = {};
const actions = {};
const mutations = {};
const getters={};
export default {
state,
actions,
mutations,
getters,
};
1、使用了 get / post /delete 三种请求数据的方法;
2、对不带参数的请求 ,比如: { url: "/product/getBaseCategoryList", method: "get" };
3、带参数的请求:{ url: /item/${skuid}
, method: “get” }`,用模板字符串
4、暴露方式为:分别暴露(引入:按需引入)
import request from "./request"; import mockAjax from "./mockAjax"; // http://gmall-h5-api.atguigu.cn/api/product/getBaseCategoryList // axios请求结果默认为promise export const reqCategoryList = () => request({ url: "/product/getBaseCategoryList", method: "get" }); //mock请求 //banner组件 export const reqGetbannerList = () => mockAjax.get("/banner"); // floor组件 export const reqFloorList = () => mockAjax.get("/floor"); // 获取商品详情接口 export const reqDetailList = (skuid) => request({ url: `/item/${skuid}`, method: "get" }); //添加到购物车 export const reqCartList = (skuid, skuNum) => request({ url: `/cart/addToCart/${skuid}/${skuNum}`, method: "post", }); //删除购物车数据 ///api/cart/deleteCart/{skuId} DELETE export const reqDeleteCartList = (skuId) => request({ url: `/cart/deleteCart/${skuId}`, method: "delete", });
axios中文文档
一个axios的二次 封装
// 二次封装 import axios from "axios"; // 进度条 import nProgress from "nprogress"; import "nprogress/nprogress.css"; import store from "@/store"; const request = axios.create({ // `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。 // 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URL baseURL: "/api", // `timeout` 指定请求超时的毫秒数。 // 如果请求时间超过 `timeout` 的值,则请求会被中断 timeout: 1000, }); // 添加请求拦截器 request.interceptors.request.use( (config) => { // console.log(store); //获取游客用户的id if (store.state.detail.uuid) { config.headers.userTempId = store.state.detail.uuid; } // 在发送请求之前做些什么 nProgress.start(); //进度条开始 return config; }, (error) => { // 对请求错误做些什么 return Promise.reject(error); } ); // 添加响应拦截器 request.interceptors.response.use( (response) => { // 2xx 范围内的状态码都会触发该函数。 // 对响应数据做点什么 nProgress.done(); //进度条结束 return response.data; }, (error) => { // 超出 2xx 范围的状态码都会触发该函数。 // 对响应错误做点什么 return Promise.reject(new Error("fail")); } ); export default request;
以购物车的后台数据为例:
1、在api/index.js中写请求 reqShopCartList
//购物车
export const reqShopCartList = () =>
request({
url: "/cart/cartList",
method: "get",
});
2、在shopcart.js 里写 请求 和 处理 的方法
//导入请求的函数 import { reqShopCartList } from "@/api"; //查看接口文档--返回的数据类型为 对象类型 , //ShopcartList用于存放接收的数据 const state = { ShopcartList: {}, }; //请求数据 const actions = { //获取购物车列表 async getShopcartList({ commit }) { let result = await reqShopCartList(); // console.log(result); if (result.code == 200) { commit("SHOPCARTLIST", result.data); } }, }; //保存数据 const mutations = { SHOPCARTLIST(state, ShopcartList) { state.ShopcartList = ShopcartList; }, }; //简化数据 映射 const getters = { ShopcartList(state) { return state.ShopcartList[0] || []; }, };
3、派发请求:谁用谁发
3.1、 cartInfoList的处理,简化了数据请求,同时可能因为网络延迟有一段时间内出现 cartInfoList为undefined 那么控制台就会报错,为了避免这种情况可以预置为空数组;
3.2、发送请求:在shopcart.vue中,调用dispatch函数发送getShopcartList请求
import { mapGetters } from "vuex";
computed: {
...mapGetters(["ShopcartList"]),
cartInfoList() {
return this.ShopcartList.cartInfoList || [];
},
methods:{
getData() {
this.$store.dispatch("getShopcartList");
}
},
3.3、除了需要接收数据的,还有另一种,只需要知道是否连接成功。大同小异,只是这里请求不需要保存数据,只需要判断响应是的结果
在action里:
// 修改购物车状态
async updateIscheckedById({ commit }, { skuId, isChecked }) {
let result = await reqIsChecked(skuId, isChecked);
if (result.code == 200) {
console.log(result);
return "ok";
} else {
return Promise.reject(new Error("fail"));
}
},
};
在组件里
//更新购物车中 商品的选中状态
async updateChecked(goods, event) {
try {
let isChecked = event.target.checked ? 1 : 0
await this.$store.dispatch('updateIscheckedById', { skuId: goods.skuId, isChecked: isChecked })
this.getData()
} catch (error) {
alert(error.message)
}
}
throttle(防抖)和debounce(节流)
节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效(只执行一次)
防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时(执行最后一次)
1、import throttle from 'lodash/throttle';
2、使用方法:xxx:throttle(function(参数){…},时间)
// 避免用户点击过于频繁 防抖
changeIndex: throttle(function (index) {
this.currentIndex = index;
}, 50),
三级菜单里的知识点:
1、如何判断当前点击的是1/2/3级菜单?只有点击a标签才进行路由的跳转,如何判断点击的是a标签?
2、合并query后params参数;使路径包含 搜索框的搜索词keyword、1或2或3级菜单中最后的菜单等级 type和商品分名称 categoryName
3、事件委派:事件委派,把全部的子节点(h3 dl dm em )的事件委派给父节点。
3.1、动态绑定属性值
通过 :data-categoryName 来判断当前是不是a 标签,通过 :data-type1、:data-type2、:data-type3来判断是第几层菜单。
<!-- 路由跳转 --> <div class="all-sort-list2" @click="goSearch"> <div class="item" v-for="(c1,index) in categoryList" :key="c1.categoryId"> <!-- 一级菜单 --> <h3 @mouseenter="changeIndex(index)" :class="{current:currentIndex==index}" > <a :data-categoryName="c1.categoryName" :data-type1="c1.categoryId">{{c1.categoryName}}</a> </h3> <!-- 二级三级菜单 --> <div class="item-list clearfix" :style="{dispaly: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-type2="c2.categoryId">{{c2.categoryName}}</a> </dt> <dd > <em v-for="(c3,index) in c2.categoryChild" :key="c3.categoryId"> <a :data-categoryName="c3.categoryName" :data-type3="c3.categoryId">{{c3.categoryName}}</a> </em> </dd> </dl> </div> </div> </div> </div>
//路由跳转:编程式导航+事件委派 //问题:事件委派,把全部的子节点(h3 dl dm em )的事件委派给父节点 //如何确定只用但点击a标签才进行路由跳转 goSearch(event) { //1-给每一级的a标签添加data-categoryname属性 let ele = event.target; //获取当前触发的事件的节点,需要带有data-categoryname这样的节点,可以确定一定是 a标签 let { categoryname, type1, type2, type3 } = ele.dataset; //注:如果a标签里面带有href="" 不会显示传递的参数 if (categoryname) { //区分1 2 3 级标签 let location = { name: 'search' }; let query = { categoryName: categoryname } // 添加data-type(1 2 3 )来区分 if (type1) { query.type1 = type1 } else if (type2) { query.type2 = type2 } else { query.type3 = type3 } //将路由参数合并 query参数 //路由跳转categoryName,type合并到location里 //console.log(query); // console.log(location); location.query = query;//合并location和query if (this.$route.params) { location.params = this.$route.params } //路由跳转 this.$router.push(location) } },
goSearch() {
// this.$router.push("./search");
// 如果有query參數也帶過去
let location = {
name: "search",
params: { keyword: this.keyword || undefined },
};
console.log(this.keyword);
if (this.$route.query) {
location.query = this.$route.query;
this.$router.push(location);
}
难点在于如何分页器的要考虑的情况比较多;
连续页码数展示一般设置为5或者7;当前页码的开始页码、结束页码如何判断;以及省略号的显示与隐藏;上一页下一页分别不能在第一页和最后一页点击;实际页码数小于连续页码数(即小于5页);更新到对应页面(点击上一页、点击下一页,点击页码中的数字)
1、使用 disabled :disabled="pageNo == 1" :disabled="pageNo == totalPage"
当前页码为1 或者 totalPage时
2、计算连续页码:当前页码 pageNo
2.1 总共页数小于连续页码数: start = 1; end = totalPage;
2.2 正常情况:总页数大于 连续页码数(5)
开始页码为: start = pageNo - parseInt(pagerCount / 2);
结束页码为:end = pageNo + parseInt(pagerCount / 2);
2.3 约束头部、尾部
如果 start < 1 (至于为什么小于1 ,请再看看计算方式,完全是有可能出现的情况)
start = 1;
end = pagerCount;
如果end > totalPage
end = totalPage;
start = totalPage - pagerCount + 1;
3、省略号的显示与隐藏
3.1 start 大于 2 进行展示,v-if="startAndEnd.start > 2"
3.2 end 小于 totalPage - 1 进行展示 :
3.3 不展示的情况:
假设当前页码 23 展示的连续页码 21 22 23 24 25 但结束页码的后一页为最后一页,此时就没必要展示省略号了,同样的 如果当前页码为4 展示的连续页码 2 3 4 5 6 第一页与第二页页没必要展示省略号
<div class="pagination"> <!-- <h1>{{ startAndEnd }}</h1> --> <!-- 点击上一页,pageNo-1,如果当前页码==1,禁止上一页事件 --> <!-- click事件是将用户的页码数读取并传回search组件使其页active --> <button @click="$emit('currentPage', pageNo - 1)" :disabled="pageNo == 1">上一页</button> <!-- 起始页大于1 展示 --> <button v-if="startAndEnd.start > 1" @click="$emit('currentPage', 1)">1</button> <!-- 起始页大于2 展示 --> <button v-if="startAndEnd.start > 2">.....</button> <!-- 中间连续页码的地方:v-for、数组、对象、数字、字符串 --> <button v-for="(page, index) in startAndEnd.end" :key="index" v-show="page >= startAndEnd.start" @click="$emit('currentPage', page)" :class="{ active: pageNo == page }">{{ page }}</button> <!-- ...展示与否 与totalPage的差大于1 --> <button v-if="startAndEnd.end < totalPage - 1">......</button> <button v-if="startAndEnd.end < totalPage" @click="$emit('currentPage', totalPage)">{{ totalPage }}</button> <button @click="$emit('currentPage', pageNo + 1)" :disabled="pageNo == totalPage">下一页</button> <button style="margin-left: 30px">共 {{ total }} 条</button> </div>
export default { name: "PageNation", props: ["total", "pageSize", "pageNo", "pagerCount"], computed: { //分页器一共多少页【总条数/每页展示条数】 totalPage() { //向上取整数 return Math.ceil(this.total / this.pageSize); }, //底下的代码是整个分页器最重要的地方[算出连续五个数字、开头、结尾] startAndEnd() { //算出连续页码:开始与结束这两个数字 let start = 0, end = 0; const { totalPage, pagerCount, pageNo } = this; //特殊情况:总共页数小于连续页码数 if (totalPage < pagerCount) { start = 1; end = totalPage; } else { //正常情况:分页器总页数大于连续页码数 start = pageNo - parseInt(pagerCount / 2); end = pageNo + parseInt(pagerCount / 2); //约束start|end在合理范围之内 //约束头部 if (start < 1) { start = 1; end = pagerCount; } //约束尾部 if (end > totalPage) { end = totalPage; start = totalPage - pagerCount + 1; } } console.log(start, end); return { start, end }; }, }, };
1、安装uuid
2、创建utils文件夹,创建uuid_token.js文件,生成id
import { v4 as uuidv4 } from "uuid";
export const getUID = () => {
let uuid = localStorage.getItem("UUIDTOKEN");
//本地没有临时id
if (!uuid) {
// 生成
uuid = uuidv4();
// 本地存储一次
localStorage.setItem("UUIDTOKEN", uuid);
}
return uuid;
};
3、将id写到请求头里,这样任何路由下都会携带该id
/ 添加请求拦截器 request.interceptors.request.use( (config) => { // console.log(store); //获取游客用户的id if (store.state.detail.uuid) { config.headers.userTempId = store.state.detail.uuid; } // 在发送请求之前做些什么 nProgress.start(); //进度条开始 return config; }, (error) => { // 对请求错误做些什么 return Promise.reject(error); } );
1、购物车商品变化: 商品数量的改动 通过 - input + 进行改变,这三种行为最终值 只触发一个派发请求(将购物车 变化的的数据 上传给服务器)
2、判断用户进行哪种操作 type ,点击了 - 传给后后台的数据 -1;点击了 + 传给后台的数据 1 ,点击了input 通过 $event.target.value * 1 获取input框的数据
传给后台的数据 event.target.value*1 - goods.skuNum
3、频繁点击会出现 商品数量小于1 的情况 ,为避免这种不合理的情况 要进行防抖
<li class="cart-list-con5">
<a href="javascript:void(0)" class="mins" @click="handler('minus', -1, goods)"
:diasabled="goods.skuNum == 1">-</a>
<input autocomplete="off" type="text" value="1" minnum="1" class="itxt" v-model="goods.skuNum"
@click="handler('change', $event.target.value * 1, goods)" />
<a href="javascript:void(0)" class="plus" @click="handler('add', 1, goods)">+</a>
</li>
//商品数量的改动 通过 - input + 行为进行改变,这三种行为最终值触发一个派发请求(将购物车 变化的的数据 上传给服务器) // type 点击加号1 点击减号-1 input框的值 要进行类型判断 // cart 商品的id handler: throttle(async function (type, disNum, goods) { switch (type) { case "add": disNum = 1; break; case "minus": //当前商品数量为1事,不能再减 disNum = goods.skuNum > 1 ? -1 : 0; break; case "change": if (isNaN(disNum) || disNum < 1) { disNum = 0; } else { disNum = parseInt(disNum) - goods.skuNum; } break; } //派发请求 try { await this.$store.dispatch("addOrupdateShopcart", { skuid: goods.skuId, skuNum: disNum, }); this.getData(); } catch (error) { console.log(error.message); } }, 300),
在action里进行dispatch,promise.all 返回值为成功时,组件的回调才会返回成功。
关于promise.all
promise.all()该方法用于将多个Promise实例,包装成一个新的Promise实例。
var p = Promise.all([p1,p2,p3]);
(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
// 删除商品
async deleteShopcartList({ commit }, skuId) {
let result = await reqDeleteCartList(skuId);
if (result.code == 200) {
return "ok";
} else {
return Promise.reject(new Error("fail"));
}
},
//删除选中的商品
async deleteAllCart() {
try {
//等待全部勾选商品删除以后
await this.$store.dispatch('deleteAllCart');
//再次获取购物车的数据
this.getData();
} catch (error) {
alert('删除失败');
}
},
//删除选中的商品
deleteAllCart({ dispatch, getters }) {
let arr = [];
//获取仓库里面购物车的数据
state.ShopcartList[0].cartInfoList.forEach((item) => {
//商品的勾选状态是勾选的,发请求一个一个删除
if (item.isChecked == 1) {
let ps = dispatch("deleteShopcartList", item.skuId);
arr.push(ps);
}
});
return Promise.all(arr);
},
在做注册、登录业务的时候,先不处理表单的验证功能,在项目最后一天,在把表单如何验证,如果是那哪些插件解决【最后去处理】
正则
手机号:11
验证码:4-6
登录密码|确认密码:首字母大写、包含英文、数字、特殊字符等等。
1、需要确认用户已经填写【手机号、验证码、密码==确认密码】之后将【手机号、登录密码】传递给服务器
name: "Register", data() { return { phone: "", passcode: "", password: "", password1: "", agree: true, }; }, methods: { //获取验证码 async getCode() { try { //简单的判断 const { phone } = this; phone && (await this.$store.dispatch("getPasscode", phone)); this.passcode = this.$store.state.user.passcode; } catch (error) { alert(error.message) } }, //用户的注册信息 async userRegister() { try { const { phone, passcode, password, password1 } = this; phone && passcode && password == password1 && await this.$store.dispatch("getUserRegister", { phone, passcode, password, }); this.$router.push('/login'); } catch (error) { alert(error.message) } }, },
2、注册完成之后跳转到登录页面,通过向服务器发请求确认用户传递的手机号、密码;根据api文档,需要通过token对用户身份进行校验(token为唯一标识符),但vue不提供长期的数据保存业务,只要刷新页面,token就会被清除,所以要对获取到的用户信息进行本地保存。
action:
// 登录 async getUserLogin({ commit }, data) { let result = await reqLogin(data); if (result.code == 200) { commit("SETUSERTOKEN", result.data.token); //本地保存token localStorage.setItem("TOKEN", result.data.token); return "ok"; } else { console.log(result); return Promise.reject(new Error(result.message)); } }, //保持用户信息到本地 async getUserInfo({ commit }) { let result = await reqUserInfo(); //将用户信息存储到store中 if (result.code === 200) { //vuex存储用户信息 commit("SETUSERINFO", result.data); return "ok"; } else { return Promise.reject(new Error(result.message)); } },
mutation:
//存储验证码
GETCODE(state, passcode) {
state.passcode = passcode;
},
// 存储token
SETUSERTOKEN(state, token) {
state.token = token;
},
//存储用户信息
SETUSERINFO(state, data) {
state.userInfo = data;
},
3、同时,判断用户是否登录,以及登录之后路由可以跳转到哪?进行判断,用到了全局路由守卫,在route文件夹的index.js文件中
//设置全局导航前置守卫 router.beforeEach(async (to, from, next) => { let token = store.state.user.token; let name = store.state.user.userInfo.name; //1、有token代表登录,全部页面放行 if (token) { //1.1登陆了,不允许前往登录页 if (to.path === "/login") { next("/home"); } else { //1.2、因为store中的token是通过localStorage获取的,token有存放在本地 // 当页面刷新时,token不会消失,但是store中的其他数据会清空, // 所以不仅要判断token,还要判断用户信息 //1.2.1、判断仓库中是否有用户信息,有放行,没有派发actions获取信息 if (name) next(); else { //1.2.2、如果没有用户信息,则派发actions获取用户信息 try { await store.dispatch("getUserInfo"); next(); } catch (error) { //1.2.3、获取用户信息失败,原因:token过期 //清除前后端token,跳转到登陆页面 await store.dispatch("logout"); next("/login"); } } } } else { //2、未登录,首页或者登录页可以正常访问 //未登录不能去交易相关、支付相关、个人中心 let topath = to.path; if ( topath.indexOf("/trade") != -1 || topath.indexOf("/pay") != -1 || topath.indexOf("/center") != -1 ) { //登录之后回到之前的路径 next("/login?redirect=" + topath); } else { //其他 alert('请先登录'); next('/login'); } } });
退出登录之后用户身份为游客,要清除登录时保存的用户信息
//logout async userLogout() { let result = await reqLogout(); if (result.code == 200) { commit("CLEAR"); return "ok"; } else { return Promise.reject(new Error(result.message)); } }, //退出登录 CLEAR(state) { //清除仓库相关用户信息 state.token = ""; state.userInfo = {}; //本地存储令牌清空 localStorage.removeItem("TOKEN"); },
安装插件 vee-validate @2版本
import Vue from "vue"; import VeeValidate from "vee-validate"; import zh_CN from "vee-validate/dist/locale/zh_CN"; // 引入中文 message Vue.use(VeeValidate); // 第二步:提示信息 VeeValidate.Validator.localize("zh_CN", { messages: { ...zh_CN.messages, is: (field) => `${field}必须与密码相同`, // 修改内置规则的 message,让确认密码和密码相同 }, attributes: { // 给校验的 field 属性名映射中文名称 phone: "手机号", passcode: "验证码", password: "密码", password1: "确认密码", agree: "协议", }, }); //自定义校验规则 //定义协议必须打勾同意 VeeValidate.Validator.extend("agree", { validate: (value) => { return value; }, getMessage: (field) => field + "必须同意", });
<div class="content"> <label>手机号:</label> <input placeholder="请输入你的手机号" v-model="phone" name="phone" v-validate="{ required: true, regex: /^1\d{10}$/ }" :class="{ invalid: errors.has('phone') }" /> <span class="error-msg">{{ errors.first("phone") }}</span> </div> <div class="content"> <label>验证码:</label> <input type="text" placeholder="请输入验证码" v-model="passcode" name="passcode" v-validate="{ required: true, regex: /^\d{6}$/ }" :class="{ invalid: errors.has('passcode') }" /> <button @click="getCode">获取验证码</button> <span class="error-msg">{{ errors.first("passcode") }}</span> </div> <div class="content"> <label>登录密码:</label> <input type="password" placeholder="请输入你的登录密码" v-model="password" name="password" v-validate="{ required: true, regex: /^[0-9A-Za-z]{8,20}$/ }" :class="{ invalid: errors.has('password') }" /> <span class="error-msg">{{ errors.first("password") }}</span> </div> <div class="content"> <label>确认密码:</label> <input type="password" placeholder="请输入确认密码" v-model="password1" name="password1" v-validate="{ required: true, is: password }" :class="{ invalid: errors.has('password1') }" /> <span class="error-msg">{{ errors.first("password1") }}</span> </div> <div class="controls"> <input type="checkbox" :checked="agree" name="isCheck" v-validate="{ required: true, agree: true }" :class="{ invalid: errors.has('agree') }" /> <span>同意协议并注册《尚品汇用户协议》</span> <span class="error-msg">{{ errors.first("agree") }}</span> </div>
一个合理的用户名正则表达式(菜鸟教程)
阮一峰 ECMAScript 6 (ES6) 标准入门教程 第三版 正则的扩展
一个合理的用户名正则表达式
用户名可以包含以下几种字符:
1、26 个大小写英文字母表示为 a-zA-Z。
2、数字表示为 0-9。
3、下划线表示为 _。
4、中划线表示为 -。
用户名由若干个字母、数字、下划线和中划线组成,所以需要用到 + 表示 1 次或多次出现。
根据以上条件得出用户名的表达式可以为:
[a-zA-Z0-9_-]+
1、什么是路由懒加载?
整个网页默认是刚打开就去加载所有页面,路由懒加载就是只加载你当前点击的那个模块。
按需去加载路由对应的资源,提高首屏加载速度(tip:首页不用设置懒加载,而且一个页面加载过后再次访问不会重复加载)。
实现原理:将路由相关的组件,不再直接导入了,而是改写成异步组件的写法,只有当函数被调用的时候,才去加载对应的组件内容。
{ path: "/communication", component: () => import("@/pages/Communication/Communication"), children: [ { path: "event", component: () => import("@/pages/Communication/EventTest/EventTest"), meta: { show: false }, }, { path: "model", component: () => import("@/pages/Communication/ModelTest/ModelTest"), meta: { show: false, }, }, { path: "sync", component: () => import("@/pages/Communication/SyncTest/SyncTest"), meta: { show: false, }, }, { path: "attrs-listeners", component: () => import("@/pages/Communication/AttrsListenersTest/AttrsListenersTest"), meta: { show: false, }, }, { path: "children-parent", component: () => import("@/pages/Communication/ChildrenParentTest/ChildrenParentTest"), meta: { show: false, }, }, { path: "scope-slot", component: () => import("@/pages/Communication/ScopeSlotTest/ScopeSlotTest"), meta: { show: false, }, }, ], },
2、图片懒加载:发现一篇https://blog.csdn.net/qq_44947815/article/details/125286969
3、下载lazyload插件,并导入mainjs
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。