项目接口文档
部署上线搞了很久没有成功,后面我再试试。这个项目主要使用vue2 vue-router vuex element-ui Axios等一些技术,这个项目做了蛮久了因为中间因为其他事情耽误了一段时间,自己也摆了很久,不过这是一个完整的项目。分别完成了商品首页,商品详情页,购物车,结算模块,商品清单,登录和注册页面,用户可以注册账号并登录,选择商品进行结算,我主要负责完成了axios二次封装,路由模块配置,获取后台数据存放到仓库并渲染到页面上,学会了用token进行身份验证,本地存储用户登录信息,实现登录持久化,并且自己独立封装了分页器组件。一共输出笔记字数1.3w+。
1.初始化项目
1.1初始化项目
1.安装vue-cli
npm install -g @vue/cli
2.创建vue项目
vue create +项目名称
3.项目文件所表示的的意思
4.配置packjson使项目运行时自动打开页面
在sever中加入 --open
"scripts": {
"serve": "vue-cli-service serve --open",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
5.关闭ESlint校验工具
1.创建vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
// 关闭Eslint
lintOnSave:false
})
1.2.项目路由分析
1.路由分析
2.引入less文件
less 版本不要太高
npm install less@4 --save-dev
npm install less-loader@7 --save-dev
lang=“less”
在compontens组件中引入Header组件和Footer 并设置样式 清除默认样式
3.路由组件搭建
分别由 home seach login Register
安装vue-router
npm i vue-router@3.5.2 -S
配置路由模块
// 创建路由模块
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
// 导入组件
import Home from '../page/home/home.vue'
import login from '../page/login/login.vue'
import search from '../page/search/search.vue'
import Register from '../page/search/search.vue'
export default new VueRouter({
routes: [
{ path: '/home', component: Home },
{ path: '/login', component: login },
{ path: '/search', compontent: search },
{ path: '/register', component: Register }
路由重定向:
]
})
在main.js中进行导入路由模块并挂载
设置page文件夹定义路由组件 home login Register find
总结:
路由组件与非路由组件区别:
路由组件一般放到page文件夹中
非路由组件一般放到compontens文件夹中
路由模块书写步骤:
1.创建路由模块
2.在入口文件中导入并挂载
3.router-view标签使用
路由的跳转两种方法:
声明式导航router-link
编程式导航push|replace
编程式导航可以做其他业务逻辑
路由传参:
params参数:属于路径当中的一部分,需要注意,在配置路由的时候,需要占位
query参数: 不属于路径当中的一部分,类似与Ajax中的queryString 不需要占位
登录或者注册页面不显示footer组件
方法1 :
使用v-show 来判断Footer的显示与隐藏
v-show=route.path='/search'
方法2:
使用meta ,在路由模块加入meta :true
4.路由传参方法:
this.$router.push({name:'find',params:{keyword:this.keyword},query:{k:this.keyword.toUpperCase()}})
并且路由name命名
路由模块:
// 创建路由模块
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
// 导入组件
import Home from '../page/home/home.vue'
import login from '../page/login/login.vue'
import Register from '../page/Register/Register.vue'
import find from '../page/find/find.vue'
// 重写push方法
const originpush = VueRouter.prototype.push
const originreplace = VueRouter.prototype.replace
VueRouter.prototype.push = function (location, resolve, reject) {
if (resolve && reject) {
originpush.call(this, location, resolve, reject)
} else {
originpush.call(this, location, () => { }, () => { })
}
}
VueRouter.prototype.replace = function (location, resolve, reject) {
if (resolve && reject) {
originreplace.call(this, location, resolve, reject)
} else {
originreplace.call(this, location, () => { }, () => { })
}
}
export default new VueRouter({
routes: [
// 路由重定向
{
path: '/', redirect: '/home',
},
{
path: '/home', component: Home,
// 控制footer在哪个页面显示或隐藏
meta: { show: true }
},
{
path: '/login', component: login,
meta: { show: false }
},
{
path: '/register', component: Register,
meta: { show: false }
},
{
path: '/find:keyword?', component: find,
meta: { show: true },
name: 'find',
props: ($route) => {
return { keyword: $route.params.keyword, k: $route.query.k }
}
}
]
})
接收参数:
<h1>params参数{{ $route.params.keyword }}</h1>
<h2>query参数{{ $route.query.k }}</h2>
5.面试题 (难点)
*** 面试题 难点
路由传递参数:
1.路由传递参数(对象写法)path是否可以结合params参数一起使用?
path写法不能结合params使用
methods: {
goSearch () {
this.$router.push({name:'find',params:{keyword:''||undefined},query:{k:this.keyword.toUpperCase()}})
}
}
2.如何指定params参数可穿可不穿
在配置路由时在占位符后面加上?就可以
3.params参数可以传递也可以不传递,但是如果传递是空串,如何解决?
路径中为少了find路径
解决方案:
使用undefined
this.$router.push({name:'find',params:{keyword:''||undefined},query:{k:this.keyword.toUpperCase()}})
4.路由组件能不能传递props数据吗?
{
path: '/find:keyword?', component: find,
meta: { show: true },
name:'find',
props:(route)=>{
return {keyword:route.params.keyword,k:$route.query.k}
}
}
路由组件可以传递props,但是只能传递props
5.编程时路由跳转到当前路由(参数不变),多次执行会抛出NavigationDuplicated警告错误?
声明式导航进行路由跳转不会报错,因为vue-router底层已经处理好了
1.1为什么编程式导航进行路由跳转时,就会有这种警告错误
push return返回的是一个Promise对象
- this 是当前组件实例 find组件
- this.router|$route属性
- push:VueRouter类的一个实例
function VueRouter(){}
VueRouter.prototype.push=function(){
函数的上下文为VueRouter类的一个实例
}
let $router=new VueRouter();
方法1:
this.$router.push({ name: 'find', params: { keyword: '' || undefined }, query: { k: this.keyword.toUpperCase() } }, (err) => {
console.log(err);
}, (result) => {
console.log(result);
})
这种写法治标不本
方法2:重写push方法和replace方法
// 重写push方法
const originpush = VueRouter.prototype.push
const originreplace = VueRouter.prototype.replace
VueRouter.prototype.push = function (location, resolve, reject) {
if (resolve && reject) {
originpush.call(this, location, resolve, reject)
} else {
originpush.call(this, location, () => { }, () => { })
}
}
VueRouter.prototype.replace = function (location, resolve, reject) {
if (resolve && reject) {
originreplace.call(this, location, resolve, reject)
} else {
originreplace.call(this, location, () => { }, () => { })
}
}
报错:
2.开发首页模块home
1.axios的二次封装
为什么要对axios二次封装
通常我们的项目会越做越大,页面也会越来越多,随之而来的是接口数量的增加。api统一管理,不管接口有多少,所有的接口都可以非常清晰,容易维护。
举个例子,当axios发生问题存在重大bug时,我们只需要修改封装部分代码即可修改全部接口(当然我们再次封装的请求需要使用现有参数格式)。
npm intall --save axios
// 对axios进行二次封装
const axios =require('axios')
// 利用axios的create方法,创建一个axios实例
const requests =axios.create({
// 基础配置
// 对基础路径配置 以后所有的路径都自带/api
baseURL:'/api',
//请求时间超过5秒
timeout:5000
})
// 请求拦截器
requests.interceptors.request.use(function(config){
// 在发送请求之前做些shenm
return config
})
// 响应拦截器
requests.interceptors.response.use((res)=>{
// 响应成功
return res.data
},(err)=>{
return new Promise.reject(new Error('falie'))
})
// 对外暴露
export default requests
终于找到接口了呜呜呜,拿走不客气~~~ gmall-h5-api.atguigu.cn/api/product…
在request文件下创建index.js文件用来专门写接口
跨域问题!!!
如果我在自己电脑上向别的服务器发送请求,就会出现跨域问题,除非服务器就是自己的电脑。
跨域问题是因为 协议,域名,端口号 其中任意一个不同导致的
比如我主机的地址是:http://192.168.72.254 http://localhost:8080/api/product/getBaseCategoryList 404报错 使用代理
但是服务器的地址是:http://39.98.123.211
域名不相同就会出现跨域问题
解决跨域问题
1.jsonp
2.cors
3.代理
使用代理服务器的方法:
webpack配置代理服务器
在vue.config,js文件中进行如下配置:
// 代理跨域
devServer: {
proxy: {
'/api': {
target: 'http://gmall-h5-api.atguigu.cn',
}
}
}
2.nprogress 进度条
进度条是页面顶部当发起请求时进度条开始移动,结束后消失
npm install nprogress
在api.js 中引入
import nprogress from 'nprogress';
// 引入进度条样式
import 'nprogress/nprogress.css'
在请求拦截器中配置
nprogress.star()
在响应拦截器中配置
nprogress.done()
3.vuex 状态管理库
集中管理项目数据 集中存储组件共用数据
安装:
npm install vuex@3.6.2
创建store文件 index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
import home from '@/store/home'
import search from '@/store/search'
// 暴露
export default new Vuex.Store({
modules:{
home,
search
}
})
在main.js中引入并挂载
组件实例会多一个$store属性
在组件中使用vuex
使用辅助函数获取vuex中的数据:
import {mapState} from 'vuex'
computed:{
...mapState(['count'])
}
创建两个仓库 home search 并暴露出去
import { getlist } from '@/request/index'
const state = {
categorylist: []
}
const mutations = {
CATECORYLIST (state, categorylist) {
state.categorylist = categorylist
}
}
const actions = {
// acions里面不能修改内容
async categorylist ({ commit }) {
let result = await getlist()
if (result.code == 200) {
commit('CATECORYLIST', result.data)
}
}
}
export default {
state,
actions,
mutations
}
在typenav里面使用
mounted () {
this.$store.dispatch('categorylist')
},
computed: {
//右侧需要的是一个函数,当使用计算属性时右侧函数会立即执行一次
//注入一个参数state即为大仓库的数据
...mapState({
categorylist: (state) => {
console.log(state);
return state.home.categorylist
}
})
},
1.1三级分类背景颜色
<div class="item" @mouseenter="changeindex(index)" @mouseleave="moveindex(index)" :class="{cur:currentIndex===index}">
<!-- 如果currentIndex===index就给当前class一个类名 cur -->
<h3>
<a href>{{ c1.categoryName }} </a>
</h3>
methods:{
changeindex(index){
this.currentIndex=index
},
moveindex(index){
this.currentIndex=-1
}
}
事件委派:
当有多个列表元素需要绑定事件时,一个一个去绑定即浪费时间,又不利于性能,这时候就可以用到事件委托,给他们的一个共同父级元素添加一个事件函数去处理他们所有的事件情况
1.2通过js控制二三级分类的显示与隐藏
给二三级菜单动态绑定一个样式 :style {}
<!-- 二级与三级分类 -->
<div class="item-list clearfix" :style="{display:currentIndex===index?'block':'none'}">
1.3防抖与节流
在二三级菜单移动中,如果用户操作过快只有部分的数据能输出,因为浏览器反映不过来,
防抖(debounce):
防抖触发高频率事件时n秒后只会执行一次,如果n秒内再次触发,则会重新计算。
概述:每次触发时都会取消之前的延时调用。
节流(thorttle):
高频事件触发,每次触发事件时设置一个延迟调用方法,并且取消之前的延时调用方法。
概述:每次触发事件时都会判断是否等待执行的延时函数。
区别: 降低回调执行频率,节省计算资源。
函数防抖一定时间连续触发的事件,只在最后执行一次,而函数节流一段时间内只执行一次。
loadsh对防抖和节流进行了封装
防抖案例:
连续触发只会执行一次
<script src="./lodash.js"></script>
let input=document.querySelector('input')
input.oninput=_.debounce(function(){
console.log('ajax请求');
},1000)
节流:
在规定的时间间隔范围内不会重复触发回调,只有大于这个时间间隔才会触发回调,把频繁触发变为少量触发
区别:防抖:用户操作频繁但是只执行一次
节流:用户操作频繁,把频繁变为少量的操作【给浏览器充足的时间解析代码】
<span>0</span>
<button>点击+1</button>
<script>
var count=0
let span = document.querySelector('span')
let button = document.querySelector('button')
button.onclick = _.throttle(function () {
count++
span.innerHTML=count
}, 1000)
1.4三级联动节流
//也可以使用按需引入
import _ from 'lodash'
methods: {
changeindex:_.throttle(function(index){
this.currentIndex = index
},40)
,
moveindex (index) {
this.currentIndex = -1
}
}
1.5三级联动路由跳转分析
三级联动用户可以点击的:一级分类、二级分类、三级分类
Home模块跳转到Search模块、一级会把用户选中的产品(产品名字,产品ID)在路由跳转的时候进行传递
三级联动路由跳转的最好方法:
编程式导航 + 事件委派
使用编程式导航不会卡
事件委派只需要给父盒子一个事件就可以绑定所有的标签 节省性能
@click=“change ”
change()
{
this,$router.push('/find)
}
1.1.1使用自定义属性
:data-categoryName="c1.categoryName"
1.1.2使用event.target
event.target获取当前点击的标签,并且加上自定义 属性
1.1.3获取节点的自定义属性
setdata
1.1.4区分一级分类,二级分类,三级分类a标签
<a :data-categoryName="c1.categoryName" :data-category1Id="c1.categoryId">{{ c1.categoryName }}</a>
<a :data-categoryName="c2.categoryName" :data-category2Id="c2.categoryId">{{ c2.categoryName }}</a>
<a :data-categoryName="c3.categoryName" :data-category3Id="c3.categoryId">{{ c3.categoryName}}</a>
goSearch(event){
let element=event.target;
let {categoryname,category1id,category2id,category3id} =element.dataset
console.log(category1id)
// 进行路由传
if(categoryname){
let location={name:'find'}
let query={categoryName:categoryname}
if(category1id){
query.category1Id=category1id
}else if(category2id){
query.category2Id=category2id
}else{
query.category3Id=category3id
}
location.query=query
this.$router.push(location)
}
}
浅浅的复习一下吧:
1.从后台获取数据并渲染,解决跨域问题 使用代理
2.函数的防抖和节流
3.路由跳转 需要使用编程式导航 不能使用声明式导航,因为这样非常消耗内存
4.三级导航路由跳转 需要采用自定义属性 使用event.target可以判断点击哪一个标签,
并且需要把参数传递给find页面
3.Home模块开发
3.1控制find页面导航栏的隐藏
要求在find页面中find导航栏隐藏 但是在home页面中不隐藏
可以使用v-if
进行判断
if (this.$route.path !== '/home') {
this.show = false
}
3.2过度动画样式
3.3typenav组件优化
typenav性能优化
当我切换到home 或者切换到find组件 每次都会发送请求 从性能角度来说不是很好
解决方案:
我们需要知道的是,在跟app组件中mounted生命周期只会执行一次
所以我们可以把this.$store.dispatch('categorylist')(派发一个action)放到app跟组件中
3.3.1search搜索功能
输入文字进行搜索并且把参数发送给后台 把输入的内容发送给typenav组件
goSearch () {
if (this.$route.query) {
let location = { name: 'find', params: { keyword: this.keyword || undefined } }
location.query = this.$route.query
this.$router.push(location)
}
}
3.4mock模拟数据
印记中文
1.mock.js
生成数据,拦截Ajax请求
安装mock.js
npm install mockjs
2.mockjs使用步骤
- 创建mock文件夹
- 准备json数据
- 把需要的图片放到publick文件夹中
- 创建mockServe.js文件
- 把mock文件在main.js中引用
//通过mockjs插件实现模拟数据
import Mock from 'mockjs'
//webpack默认对外暴露的有 图片 json文件格式 不需要exprort
import banner from './banner.json'
import floor from './floor.json'
//mock数据
Mock.mock('/mock/banner',{code:200,data:banner})
Mock.mock('/mook/floor',{code:200,data:floor})
3.5轮播图
mounted(){
//派发一个action
this.$store.dispatch('getBannerlist')
},
computed:{
...mapState({
bannerList:(state)=>{
return state.home.bannerlist
}
})
}
获取数据
在home仓库中进行配置
import { getlist, reqGetBanner } from '@/request/index'
const state = {
categorylist: [],
bannerlist: []
}
const mutations = {
CATECORYLIST (state, categorylist) {
state.categorylist = categorylist
},
GETBANNER(state,bannerlist){
state.bannerlist=bannerlist
}
}
const actions = {
// acions里面不能修改内容
async categorylist ({ commit }) {
let result = await getlist()
if (result.code == 200) {
commit('CATECORYLIST', result.data)
}
},
async getBannerlist () {
const result = await reqGetBanner()
if (result.code == 200) {
commit('GETBANNER',result.data)
}
}
}
export default {
state,
actions,
mutations
}
3.5.1swiper的基本使用
安装:
- npm install --save swiper@5.4.5
- 引包 from swiper from ‘swiper’
- 在main.js引入swiper样式
// 引入swiper样式
import "swiper/css/swiper.css";
注意点:
我们不能在mounted中放入是wiper实例
因为组件上并没有接收的数据,问题在与我还没有对数据进行修改,就要初始化swiper实例,所以不能在mounted中写swiper实例
3.6watch+nextTick解决轮播图问题
nextTick:在下次更新DOM循环结束之后。执行延迟回调,在修改数据之后立即使用这个方法获取更新后的DOM
$nextTick的作用:保证页面DOM结构一定是有的 很多插件是保证DOM结构已经有了才执行回调。
<div class="swiper" style=" overflow: hidden;">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="item in getbannerlist" :key="item.id">
<img :src="item.imgUrl" alt />
</div>
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>
<!-- 如果需要导航按钮 -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
<!-- 如果需要滚动条 -->
<div class="swiper-scrollbar"></div>
</div>
</div>
为什么需要把js代码放到watch中
!!!!
通过watch监听bannerList属性的属性值变化
如果执行handeler方法,代表组件实例身上的这个属性已经有了 【数组:4个元素】
当前执行这个函数只能保证bannerList数据已经有了,但是没办法保证v-for已经执行结束了
v-for执行完毕,才有结构
nextTick:在下次DOM更新 循环结束之后,执行延迟回调
watch: {
getbannerlist: {
immediate:true,
handler (newValue, oldValue) {
this.$nextTick(() => {
var swiper = new Swiper(document.querySelector('.swiper'), {
// spaceBetween: 30,
//无限循环
loop:true,
//自动播放
autoplay:true,
//分页器
pagination: {
el: ".swiper-pagination",
clickable: true,
},
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
});
})
}
}
}
}
3.6.1底下两个小轮播图
对home组件进行遍历生成两个组件,并且通过父子组件通讯的方法把父组件的值传递给子组件
<!-- v-for可以在自定义标签上使用 -->
<Floor v-for="item in getfloorlist" :key="item.id" :list="item"/>
computed: {
...mapState({
getfloorlist:(state)=>{
return state.home.getFloorlist
}
})
},
mounted(){
this.$store.dispatch('getFloorlist')
},
子组件
<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.bigImg" />
</div>
<!-- 轮播图 -->
<div class="floorBanner" style="overflow: hidden;">
<div class="swiper-container" id="floor1Swiper">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="corouse in list.carouselList" :key="corouse.id">
<!-- // 遍历数据 -->
<img :src="corouse.imgUrl" />
</div>
</div>
props: ["list"],
在子组件我可以把轮播图js代码放到mounted里面
原因是因为我的请求是在父组件中通过Prop传递过来的,我的数据已经有了并渲染成DOM元素
props: ["list"],
mounted () {
var mySwiper = new Swiper(this.$refs.swiper, {
direction: 'vertical', // 垂直切换选项 可以删除
loop: true, // 循环模式选项
autoplay:true //自动播放
// 如果需要分页器
pagination: {
el: '.swiper-pagination',
},
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
// 如果需要滚动条
})
},
4.搜索模块(find)
1.步骤:
- 写静态页面
- 写api获取数据
- 写vuex仓库
- 组件获取仓库数据,动态展示数据
1.引入search模块
写api接口
// 导入request
// 写接口
import request from '@/request/api'
import mockRequest from '@/request/mockajax'
export const getlist = () => {
return request({ url: '/product/getBaseCategoryList', methods: 'get' })
}
export const reqGetBanner = () => {
return mockRequest.get('/banner')
}
export const reqfloorList=()=>{
return mockRequest.get('/floor')
}
// 商品请求 /api/list post请求 带参数
export const reqGetSearchinfo=(params)=>{
return request({
url:'/list',
method:'post',
data:params
})
}
2.建立vuex仓库并获取数据
通过测试可以拿到数据,必须在search页面中打开才能发生请求
2.1vuex中使用getters
在项目使用getters 可以对state里面的数据进行处理,得到我们需要的数值
import { reqGetSearchinfo } from '@/request/index'
const state = {
searList: {}
}
const mutations = {
SEARCH(state,searList){
state.searList=searList
}
}
const actions = {
async getSearchList ({commit},params={}) {
let result=await reqGetSearchinfo(params)
console.log(result.data);
if(result.code==200){
commit('SEARCH',result.data)
}
}
}
// 计算属性
const getters = {
//可以简化state里面的数据,返回我们想要的数值
//如果网络请求速度慢,则需要返回一个空对象
goodsList: state => state.searchList.goodsList||[],
attrsList: state => state.searchList.attrsList||[],
trademarkList: state => state.searchList.trademarkList||[]
}
export default {
state,
mutations,
actions,
getters
}
2.2获取数据并渲染到页面
methods: {
getData () {
//派发一个getSearchList 并且把searchParams传递给服务器
this.$store.dispatch('getSearchList', this.searchParams)
},
mounted () {
this.getData()
},
computed: {
...mapGetters(['goodsList'])
},
遍历整个attrsLIst
<div class="type-wrap" v-for="(attr,index) in attrsList" :key="index">
<div class="fl key">{{ attr.attrName }}</div>
<div class="fl value">
<ul class="type-list">
<!-- //5g -->
<li v-for="(arrtValue,index) in attr.attrValueList" :key="index">
<a>{{ arrtValue }}</a>
</li>
</ul>
</div>
在三级导航进入search组件中 发送请求前就把参数发送到服务器
3.监听路由信息
当我们路由里面的参数发生变化时,必须重新发送请求并且把参数发送给服务器
data () {
return {
searchParams: {
//产品相应的id
category1Id: "",
category2Id: "",
category3Id: "",
//产品的名字
categoryName: "",
//搜索的关键字
keyword: "",
//排序:初始状态应该是综合且降序
order: "1:desc",
//第几页
pageNo: 1,
//每一页展示条数
pageSize: 3,
//平台属性的操作
props: [],
//品牌
trademark: "",
},
}
},
components: {
SearchSelector
},
beforeMount () {
// 在发送请求之前需要把参数整理发送给服务器
Object.assign(this.searchParams, this.$route.query, this.$route.params);
},
mounted () {
this.getData()
},
computed: {
...mapGetters(['goodsList'])
},
methods: {
getData () {
this.$store.dispatch('getSearchList',this.searchParams)
}
},
watch: {
$route (newValue, oldValue) {
Object.assign(this.searchParams, this.$route.query, this.$route.params);
this.getData()
console.log(this.searchParams);
//每一次请求都应该把123级分类清空
this.searchParams.category1Id=''
this.searchParams.category2Id=''
this.searchParams.category3Id=''
}
}
}
2.面包屑
对面包屑进行v-if判断 如果有categoryName就显示
<ul class="fl sui-tag">
<li class="with-x" v-if="searchParams.categoryName">
{{ searchParams.categoryName }}
<i @click="delbread">×</i>
</li>
//第二个面包屑
<li class="with-x" v-if="searchParams.keyword">
{{ searchParams.keyword }}
<i @click="removekeyword">×</i>
</li>
</ul>
delbread () {
this.searchParams.categoryName=undefined
this.searchParams.category1Id = undefined
this.searchParams.category2Id = undefined
this.searchParams.category3Id = undefined
this.getData()
//需要判断是否有params参数 如果路径中出现pararms参数不应该删除 需要带着 params参数在搜索按钮保存
if(this.$route.params){
//地址栏也需要该 清空
this.$router.push({name:'find',params:this.$route.params})
}
//删除关键字
removekeyword () {
//删除后需要继续发请求
this.searchParams.keyword=undefined
//继续发请求
this.getData()
}
}
2.1全局事件总线
当我点击面包屑删除按钮时,我需要把搜索框的内容清空,由于search组件和header组件是单独的,我需要使用全局事件总线来完成数据的传递与接收
简单理解,全局事件总线其实就是一个中间介质,组件间的相互通信借助于这个中间介质,通过这个中间转换介质,从而完成数据的传递与接收,实现组件间的相互通信
2.1.1 在main.js中完成全局事件总线的配置
new Vue({
router,
store,
render: h => h(App),
beforeCreate(){
Vue.prototype.$bus=this
}
}).$mount('#app')
removekeyword () {
//删除后需要继续发请求
this.searchParams.keyword = undefined
//继续发请求
this.getData()
this.$bus.$emit('clear')
}
//通过全局事件总线清除关键字
this.$bus.$on('clear',()=>{
this.keyword=''
})
2.2面包屑处理品牌信息
点击品牌展示品牌数据,并且有面包屑
步骤:
- 使用自定义属性 用来接收子组件传递过来的值
子组件:
methods:{
tradMatk(mark){
//点击按钮 必须在父组件中发送请求,因为父组件中的searchParams参数是带给服务器的参数
//组件通讯子组件传递父组件
this.$emit('tradmark',mark)
}
}
父组件:
//自定义事件接收子组件的值
taradmark (state) {
//接收子组件传递过来的值
console.log(state);
//整理参数
this.searchParams.trademark = `${state.tmId}:${state.tmName}`
//再次发送请求
this.getData()
//将trademark展示给面包屑
// this.searchParams.trademark=''
},
- 品牌的面包屑
<!-- //品牌的面包屑 -->
<li class="with-x" v-if="searchParams.trademark">
{{ searchParams.trademark.split(":")[1] }}
<i @click="removeTrademark">×</i>
</li>
- 删除按钮
//品牌的面包屑关闭按钮
removeTrademark(){
this.searchParams.trademark=undefined
this.getData()
}
全部代码:
import { mapGetters } from 'vuex'
import SearchSelector from './SearchSelector/SearchSelector'
export default {
name: 'Search',
data () {
return {
//传递给服务器的数据
searchParams: {
//产品相应的id
category1Id: "",
category2Id: "",
category3Id: "",
//产品的名字 用户搜索的名字
categoryName: "",
//搜索的关键字
keyword: "",
//排序:初始状态应该是综合且降序
order: "1:desc",
//第几页
pageNo: 1,
//每一页展示条数
pageSize: 3,
//平台属性的操作
props: [],
//品牌
trademark: "",
},
}
},
components: {
SearchSelector
},
beforeMount () {
// 在发送请求之前需要把参数整理发送给服务器
Object.assign(this.searchParams, this.$route.query, this.$route.params);
},
mounted () {
this.getData()
},
computed: {
...mapGetters(['goodsList'])
},
methods: {
getData () {
//派发一个getSearchList 并且把searchParams传递给服务器
this.$store.dispatch('getSearchList', this.searchParams)
},
delbread () {
this.searchParams.categoryName = undefined
this.searchParams.category1Id = undefined
this.searchParams.category2Id = undefined
this.searchParams.category3Id = undefined
this.getData()
//需要判断是否有params参数 如果路径中出现pararms参数不应该删除 需要带着 params参数在搜索按钮保存
if (this.$route.params) {
//地址栏也需要该 清空
this.$router.push({ name: 'find', params: this.$route.params })
}
},
//删除关键字
removekeyword () {
//删除后需要继续发请求
this.searchParams.keyword = undefined
//继续发请求
this.getData()
//清除搜索框文字
this.$bus.$emit('clear')
// 如果有query参数必须带上
if (this.$route.query) {
this.$router.push({ name: 'find', query: this.$route.query })
}
},
//自定义事件接收子组件的值
taradmark (state) {
//接收子组件传递过来的值
console.log(state);
//整理参数
this.searchParams.trademark = `${state.tmId}:${state.tmName}`
//再次发送请求
this.getData()
//将trademark展示给面包屑
// this.searchParams.trademark=''
},
//品牌的面包屑关闭按钮
removeTrademark(){
this.searchParams.trademark=''
this.getData()
}
},
watch: {
$route (newValue, oldValue) {
//合并query和params参数并给searchParams
Object.assign(this.searchParams, this.$route.query, this.$route.params);
//进行一次请求
this.getData()
//每一次请求都应该把123级分类清空
this.searchParams.category1Id = undefined
this.searchParams.category2Id = undefined
this.searchParams.category3Id = undefined
}
}
}
面包屑的属性展示
把这个参数传递给服务器
<!-- 平台售卖属性面包屑 -->
<li class="with-x" v-for="(arrtValue,index) in searchParams.props" :key="index">
{{ arrtValue.split(":")[1] }}
<i @click="removeTrademark">×</i>
</li>
在子组件中使用自定义属性,将子组件的属性ID 属性值 属性名 传递给父组件
<!-- 平台售卖属性 -->
<!-- 比如:手机内存 -->
<div class="type-wrap" v-for="(attr,index) in attrsList" :key="index">
<div class="fl key">{{ attr.attrName }}</div>
<div class="fl value">
<!-- 属性值 -->
<ul class="type-list">
<!-- 5g 4g 3g 2g -->
<li v-for="(arrtValue,index) in attr.attrValueList" :key="index" @click="attrinfo(attr,arrtValue)">
<a href="#">{{ arrtValue }}</a>
</li>
</ul>
</div>
attrinfo(attr,attrValue){
this.$emit('attrinfo',attr,attrValue)
}
}
父组件接收
@attrinfo="attrinfo"
//接收子组件的属性
attrinfo(attr,attrValue){
let result=`${attr.attrId}:${attrValue}:${attr.attrName}`
// 数组去重 祛除里面重复的元素 否则面包屑可以一直增加
if (this.searchParams.props.indexOf(result) == -1) {
this.searchParams.props.push(result)
}
this.getData()
// this.searchParams.props=[]
}
移出售卖的面包屑
需要传递一个索引值index
使用splice删除指定元素 splice(index,1)
// 删除商品售卖属性元素
removerattr(index){
this.searchParams.props.splice(index,1)
this.getData()
}
3.排序操作(课程重点)
先给一个初始值
//排序:初始状态应该是综合且降序
order: "1:desc",
问题1.
谁应该有类名
通过order属性值中的包含1(综合) | 包含2(价格)
<li :class="{active:searchParams.order.indexOf('1')!==-1}" >
<a href="#">综合</a>
</li>
<li :class="{active:searchParams.order.indexOf('2')!==-1}">
<a href="#">价格</a>
</li>
//控制显示与隐藏 取决于order里面是1 还是 2
<li :class="{active:searchParams.order.indexOf('1')!==-1}" >
<a href="#">综合 <span v-show="this.searchParams.order.indexOf('1')!==-1">1</span></a>
</li>
<li :class="{active:searchParams.order.indexOf('2')!==-1}">
<a href="#">价格 <span v-show="this.searchParams.order.indexOf('2')!==-1">1</span></a>
</li>
3.1使用iconfont图标
在public里面引入
使用incon-class的方法引入图标
阿里图标
升序降序
<li :class="{active:searchParams.order.indexOf('1')!==-1}">
<a href="#">
综合
<span v-show="this.searchParams.order.indexOf('1')!==-1" class="iconfont" :class="{'icon-arrdown1':isASC,'icon-arrdown-green-down':isDESC}"></span>
</a>
</li>
<li :class="{active:searchParams.order.indexOf('2')!==-1}">
<a href="#">
价格
<span v-show="this.searchParams.order.indexOf('2')!==-1" class="iconfont" :class="{'icon-arrdown1':isASC,'icon-arrdown-green-down':isDESC}"></span>
</a>
</li>
computed: {
...mapGetters(['goodsList']),
isASC(){
//包含升序
return this.searchParams.order.indexOf('asc')!==-1
},
isDESC(){
//包含降序
return this.searchParams.order.indexOf('desc')!==-1
}
},
点击操作使用户切换
这一部分非常的绕 需要多多思考几遍 并且自己动手敲一遍代码
// 排序操作 进行点击切换
changeorder (flag) {
//数子 1|2
let originFlge = this.searchParams.order.split(':')[0]
//升序或者降序 desc asc
let originSort = this.searchParams.order.split(':')[1]
let newOrder = ''
// 点击的是综合
if (flag == originFlge) {
newOrder = `${flag}:${originSort == 'desc' ? 'asc' : 'desc'}`
}
// 点击的是价格
else {
newOrder = `${flag}:${'desc'}`
//flag:desc
}
this.searchParams.order = newOrder
this.getData()
}
4.分页器静态组件(重要难点)
能手写一个最好
为什么很多项目采取分页器功能,因为数据量非常大 1w+ ,因此拆分为全局分页器功能
分页器展示:
需要知道当前是第几个:pageNo字段代表当前页数
需要知道每一个展示多少条数据:pageSize字段进行代表
需要知道整个分页器一共多少条数据,total字段代表代表一共展示多少条数据
分页器连续页码个数 【5|7】
对于分页器而言,自定义前提需要知道四个前提条件
pageNo:当前第几个
pageSize:代表每一页展示几个数据
total:代表整个分页一共展示多少条数据
continues:代表分页连续页码个数
对于分页器而言,很重要的地方是算出连续页码的起始数据
先传递一些假数据
【--获取另一条数据--】
封装一个全局分页器组件
在main.js中注册
自定义分页器:自己先手动调试一遍
对于分页器而言最重要的地方是 算出连续页面的起始数字和结束数字
当前是第八页计算出前后数字
6 7 8 9 10
props: ['pageNo', 'pageSize', 'total', 'continues'],
computed: {
// 计算属性 计算出总共多少页
totalpage () {
//向上取整
return Math.ceil(this.total / this.pageSize)
},
//计算连续页面起始数字和连续数字
startNumandEndNum () {
const { continues, pageNo, totalpage } = this
//先定义两个变量
let start = 0;
let end = 0;
//如果总页码就4页 连续页码是5页这样不正常
//连续页数大于总页数
if (this.continues > this.totalpage) {
start = 1
end = this.totalpage
} else {
//正常现象 连续页码是5 总页数大于5
//6 7 8 9 10 当前第8页
//5 6 7 8 9 10 11
// 因为连续页码不是固定的 可能是5也可能是7
start = pageNo - parseInt(continues / 2)
end = pageNo + parseInt(continues / 2)
//当前第1页或者第2页
//1 2 3 4 5
if (start < 1) {
start = 1
end = continues
}
if (end > totalpage) {
end = totalpage
start=totalpage-continues+1
}
}
return { start, end }
}
}
分页器动态展示
<template>
<div class="pagination" style="text-align:center">
<button :disabled="pageNo==1" @click="$emit('pageNo',pageNo-1)">上一页</button>
<button v-if="startNumandEnd.start>1" @click="$emit('pageNo',1)" :class="{active:pageNo==1}">1</button>
<button v-if="startNumandEnd.start>1">...</button>
<!-- 中间部分 -->
<button v-if="page>=startNumandEnd.start" v-for="(page,index) in startNumandEnd.end" :key="index" @click="$emit('pageNo',page)" :class="{active:page==pageNo}">{{ page }}</button>
<button v-if="startNumandEnd.end<totalPage">···</button>
<!-- //总页数 -->
<button v-if="startNumandEnd.end<totalPage" @click="$emit('pageNo',totalPage)" :class="{active:pageNo==totalPage}">{{totalPage}}</button>
<button :disabled="pageNo==totalPage" @click="$emit('pageNo',pageNo+1)" >下一页</button>
<!-- //总条数 -->
<button style="margin-left: 30px">共{{ total }}条数据</button>
</div>
</template>
父组件自定义事件
<!-- //分页器 -->
<Pageation :pageNo="searchParams.pageNo" :pageSize="searchParams.pageSize" :total="total" :continues="5" @pageNo="pageNo" />
</div>
</div>
...mapState({
total: (state) => state.search.searList.total
})
// 自定义事件,获取当前第几页
pageNo (page) {
//整理带给服务器的参数
this.searchParams.pageNo=page
this.getData()
}
最终效果:
全部代码:
<template>
<div class="pagination" style="text-align:center">
<button :disabled="pageNo==1" @click="$emit('pageNo',pageNo-1)">上一页</button>
<button v-if="startNumandEnd.start>1" @click="$emit('pageNo',1)" :class="{active:pageNo==1}">1</button>
<button v-if="startNumandEnd.start>1">...</button>
<!-- 中间部分 -->
<button v-if="page>=startNumandEnd.start" v-for="(page,index) in startNumandEnd.end" :key="index" @click="$emit('pageNo',page)" :class="{active:page==pageNo}">{{ page }}</button>
<button v-if="startNumandEnd.end<totalPage">···</button>
<!-- //总页数 -->
<button v-if="startNumandEnd.end<totalPage" @click="$emit('pageNo',totalPage)" :class="{active:pageNo==totalPage}">{{totalPage}}</button>
<button :disabled="pageNo==totalPage" @click="$emit('pageNo',pageNo+1)" >下一页</button>
<!-- //总条数 -->
<button style="margin-left: 30px">共{{ total }}条数据</button>
</div>
</template>
<script>
export default {
name: "Pageation",
props: ['pageNo', 'pageSize', 'total', 'continues'],
computed: {
// 总页数
totalPage () {
return Math.ceil(this.total / this.pageSize)
},
// 连续数字
startNumandEnd () {
const { pageNo, continues } = this
let start = 0;
let end = 0;
if (continues > this.totalPage) {
start = 1;
end = continues
} else {
start = pageNo - parseInt(continues / 2)
end = pageNo + parseInt(continues / 2)
// 当前页不能为1或者0 也就是说start不能<1
if (start < 1) {
start = 1;
end = continues
}
if (end > this.totalPage) {
end = this.totalPage;
start = this.totalPage - continues + 1
}
return {start,end}
}
}
}
}
</script>
<style lang="less" scoped>
.pagination {
button {
margin: 0 5px;
background-color: #f4f4f5;
color: #606266;
outline: none;
border-radius: 2px;
padding: 0 4px;
vertical-align: top;
display: inline-block;
font-size: 13px;
min-width: 35.5px;
height: 28px;
line-height: 28px;
cursor: pointer;
box-sizing: border-box;
text-align: center;
border: 0;
&[disabled] {
color: #c0c4cc;
cursor: not-allowed;
}
&.active {
cursor: not-allowed;
background-color: #409eff;
color: #fff;
}
}
}
</style>
5.商品详情页
- 静态页面
- 发送请求
- vuex
在search页面中 a标签替换声明式导航 /detail 并且带参数
1.需要带一个参数
// 获取商品详情
// /api/item/{ skuId }
export const reqgoodinfo=(skuId)=>request.get(`/item/ ${ skuId }`)
在index.js中书写商品详情页请求
用户派发action的时候需要带一个参数
mounted () {
// 派发action获取产品的详情信息
this.$store.dispatch('getGoodinfo', this.$route.params.id)
},
2.建立detail仓库
import { reqgoodinfo } from '@/request/index'
const state = {
goodInfo: {}
}
const mutations = {
GETGOODINFO (state, goodInfo) {
state.goodInfo = goodInfo
}
}
const actions = {
// 用户派发
async getGoodinfo ({ commit }, skuId) {
let result = await reqgoodinfo(skuId)
if (result.code == 200) {
commit('GETGOODINFO', result.data)
}
}
}
// 简化数据
const getters = {
categoryView (state) {
// 如果服务器的数据没有拿到 会报错 至少返回一个空对象
return state.goodInfo.categoryView || {};
},
spuSaleAttrList (state) {
return state.goodInfo.spuSaleAttrList
},
skuInfo (state) {
return state.goodInfo.skuInfo || {}
},
spuSaleAttrList(state){
return state.goodInfo.spuSaleAttrList||[]
}
}
export default {
state,
mutations,
actions,
getters
}
3.用户在detail组件中获取getters中的数据,并且把数据动态渲染到页面上去
并且把值传递给子组件
商品详情页使用js完成切换效果
<dl v-for="(spuSaleAttr,index) in spuSaleAttrList" :key="spuSaleAttr.id">
<dt class="title">{{ spuSaleAttr.saleAttrName }}</dt>
<dd
changepirce="0"
:class="{active:spuSaleAttrValue.isChecked==1}"
v-for="(spuSaleAttrValue,index) in spuSaleAttr.spuSaleAttrValueList"
:key="spuSaleAttrValue.id"
@click="changeActive(spuSaleAttrValue,spuSaleAttr.spuSaleAttrValueList)"
>{{ spuSaleAttrValue.saleAttrValueName }}</dd>
</dl>
</div>
changeActive (spuSaleAttrValue, arr) {
console.log(arr);
arr.forEach(item => {
item.isChecked = 0
});
spuSaleAttrValue.isChecked=1
}
}
4.底部列表轮播图
nextTick
<div class="swiper-container" ref="cur">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="(slide,index) in skuImageList" :key="skuImageList.id">
<img :src="slide.imgUrl" :class="{active:CurrentIndex==index}" @click="changeIndex(index)" />
</div>
</div>
<div class="swiper-button-next"></div>
<div class="swiper-button-prev"></div>
</div>
name: "ImageList",
data () {
return {
CurrentIndex: 0
}
},
props: ['skuImageList'],
watch: {
skuImageList (newValue, oldValue) {
nextTick(() => {
var mySwiper = new Swiper(this.$refs.cur, {
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
// 显示几个图片
slidesPerView: 3,
//每一次切换图片个数
slidesPerGroup: 3,
})
})
}
},
methods: {
changeIndex (index) {
this.CurrentIndex = index;
//给兄弟组件传递index值
this.$bus.$emit('getIndex',index)
}
}
放大镜效果:
<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 ref="mask" class="mask"></div>
</div>
methods: {
handler (event) {
let mask = this.$refs.mask
let left = event.offsetX - mask.offsetWidth / 2
let top = event.offsetY - mask.offsetHeight / 2
// 约束范围
if (left <= 0) left = 0
if (top <= 0) top = 0
if (left > mask.offsetWidth) left = mask.offsetWidth
if (top > mask.offsetHeight) top = mask.offsetHeight
// 修改元素的left和top
mask.style.left = left + 'px';
mask.style.top = top + 'px';
// 修改大图
let big = this.$refs.big;
big.style.left = -2 * left + 'px';
big.style.top = -2 * top + 'px'
}
}
购买商品个数
加减 用户在表单中输入:要求输入的值小于1时等于1 并且只能是数字
<div class="controls">
<input autocomplete="off" class="itxt" v-model.number="skyNum" @change="changGouwu" />
<a href="javascript:" class="plus" @click="skyNum++">+</a>
<a href="javascript:" class="mins" @click="skyNum<=1?skyNum=1:skyNum--">-</a>
</div>
changGouwu (event) {
let value = event.target.value
if (isNaN(value)||value<1){
this.skyNum=1;
}else{
this.skyNum=parseInt(value)
}
}
5.点击加入购物车跳转:
发起一次请求
请求地址
/api/cart/addToCart/{ skuId }/{ skuNum }
// 购物车点击事件
addShopcar () {
// 1.发请求 --将产品发送到服务器
// 2.服务器存储成功 --进行路由跳转传参
// 3.失败给用户提示
this.$store.dispatch('addorderUpdateShopcat', { skuId: this.$route.params.id, skuNum: this.skyNum })
}
// 购物车点击事件
async addShopcar () {
// 1.发请求 --将产品发送到服务器
// 2.服务器存储成功 --进行路由跳转传参
// 3.失败给用户提示
//需要判断加入购物车是否成功
//使用 try-catch 成功执行try失败执行catch
try {
await this.$store.dispatch('addorderUpdateShopcat', { skuId: this.$route.params.id, skuNum: this.skyNum })
} catch (error) {
alert(error.message)
}
}
1.1跳转购物车:
引入AddCartSuccess组件 并写入路由
//成功了进行路由跳转
this.$router.push({name:'addcartsuccess'})
5.1重点:
然后我需要把这个页面所点击的内容传递给addcartsuccess组件
可以使用路由传参,但是这种方法一刷新就没了
我们使用会话存储
本地存储:持久行--5M
会话存储:并非持久:页面关闭就消失了
都不允许存对象
一些简单的数据通过query形式传递给路由
一些复杂的数据通过会话存储(好处:不持久化 会话结束消失)
try {
await this.$store.dispatch('addorderUpdateShopcat', { skuId: this.$route.params.id, skuNum: this.skyNum })
//会话存储
sessionStorage.setItem('SKUINFO',JSON.stringify(this.skuInfo))
//成功了进行路由跳转
this.$router.push({name:'addcartsuccess',query:{skyNum:this.skyNum}})
//在路由跳转的时候需要把产品的信息传递过去
} catch (error) {
alert(error.message)
}
在购物成功路由获取
computed: {
skuInfo () {
return JSON.parse(sessionStorage.getItem('SKUINFO'))
}
}
5.2在购物成功面板有两个跳转
“查看商品详情”使用声明式导航跳转
将skuinfo.id给to跳转
5.3点击购物车结算去购物车结算页面
引入shopcart组件,并使用router-link进行跳转
<div class="right-gocart">
<!-- <a href="javascript:" >查看商品详情</a> -->
<router-link class="sui-btn btn-xlarge" :to="`/detail/${skuInfo.id}`" >查看商品详情</router-link>
<router-link to="/shopcart">去购物车结算</router-link>
</div>
</div>
5.4.购物车结算页面
/api/cart/cartList 接口请求地址
发起请求的时候获取不到购物车数据,返回一共空数组 因为服务器不知道是谁存的东西
使用uuid库
游客身份生成一次就行
两种身份:
1.游客身份
2.用户
uuid
封装一共uuid的模块
utils/uuid_token.js
import { v4 as uuidv4 } from 'uuid';
export const getUUID=()=>{
let uuid_token=localStorage.getItem('UUIDTOKEN')
if(!uuid_token){
// 生成一个uuid
uuid_token=uuidv4();
localStorage.setItem('UUIDTOKEN',uuid_token)
}
return uuid_token
}
把uuid_token存放到仓库中
//封装游客身份模块uuid--->生成一个随机字符串(不能在变了)
import {getUUID} from '@/utils/uuid_token'
const state = {
goodInfo: {},
//游客临时身份
uuid_token:getUUID()
}
在请求拦截器中进行配置
// 请求拦截器 在发送请求前拦截
requests.interceptors.request.use(function (config) {
if (store.state.detail.uuid_token) {
//请求头添加一共字段(userTempId) 和后台老师商量好了 不能写错
config.headers.userTempId = store.state.detail.uuid_token
}
nprogress.start()
return config;
})
动态展示数据
<ul class="cart-list" v-for="(cart,index) in cartInfoList" :key="cart.id">
<li class="cart-list-con1">
<input type="checkbox" name="chk_list" :checked="cart.isChecked==1" />
</li>
<li class="cart-list-con2">
<img :src="cart.imgUrl" />
<div class="item-msg">{{ cart.skuName }}</div>
</li>
<li class="cart-list-con4">
<span class="price">{{ cart.skuPrice }}</span>
</li>
<li class="cart-list-con5">
<a href="javascript:void(0)" class="mins" @click="handler('jian',-1,cart)">-</a>
<input autocomplete="off" type="text" :value="cart.skuNum" minnum="1" class="itxt" @change="handler('change',$event.target.value*1,cart)" />
<a href="javascript:void(0)" class="plus" @click="handler('add',1,cart)">+</a>
</li>
<li class="cart-list-con6">
<span class="sum">{{ cart.skuPrice*cart.skuNum }}</span>
</li>
<li class="cart-list-con7">
<a href="#none" class="sindelet">删除</a>
<br />
<a href="#none">移到收藏</a>
</li>
</ul>
</div>
</div>
computed: {
...mapGetters(['cartList']),
cartInfoList () {
return this.cartList.cartInfoList
},
//计算购买产品总价
totalPrice () {
let sum = 0;
this.cartInfoList.forEach((item) => {
sum += item.skuPrice * item.skuNum
})
return sum;
},
//判断是否全选
isChecked () {
return this.cartInfoList.every((item) => item.isChecked == 1)
},
}
实现购物商品加减功能
需要使用之前写好的一个请求,每次增加或减少都要请求一次 skuid参数是当前的商品id skuNum是当前商品新增几次
如果是加号skuNum=1 减号skuNum=-1
加减和用户输入使用一共方法,需要传递三个参数
- type 类型
- disNum 如果是加法就是1 减法-1 用户输入就是本身
- cart 是当前的商品
<a href="javascript:void(0)" class="mins" @click="handler('jian',-1,cart)">-</a>
<input autocomplete="off" type="text" :value="cart.skuNum" minnum="1" class="itxt" @change="handler('change',$event.target.value*1,cart)" />
<a href="javascript:void(0)" class="plus" @click="handler('add',1,cart)">+</a>
async handler (type, disNum, cart) {
switch (type) {
//点击加号
case 'add':
disNum = 1
break;
//点击减号
case 'jian':
if (cart.skuNum > 1) {
disNum = -1
} else {
disNum = 0
}
break;
//用户输入
case 'change':
//用户输入的如果是非数字或者小于1
if (isNaN(disNum) || disNum < 1) {
disNum=0;
}else{
//输入正常数字包括小数
disNum=parseInt(disNum)-cart.skuNum;
}
break
}
// 派发请求
// this.$store.dispatch('addorderUpdateShopcat',{skuId:cart.skuId,skuNum:disNum})
try {
await this.$store.dispatch('addorderUpdateShopcat', { skuId: cart.skuId, skuNum: disNum })
this.getData()
} catch (error) {
alert(error.message)
}
}
},
删除购物车商品
写请求
仓库三连环
绑定click删除事件
传递参数 派发action 发起请求
//点击删除按钮
async delCart (cart) {
try {
await this.$store.dispatch('DelCartList', cart)
this.getData()
} catch (error) {
alert(error.message)
}
}
5.5防抖节流
当用户点击过快时,使用节流
import throttle from 'lodash/throttle'
// 节流
handler: throttle(async function (type, disNum, cart) {
switch (type) {
//点击加号
case 'add':
disNum = 1
break;
//点击减号
case 'jian':
if (cart.skuNum > 1) {
disNum = -1
} else {
disNum = 0
}
break;
//用户输入
case 'change':
//用户输入的如果是非数字或者小于1
if (isNaN(disNum) || disNum < 1) {
disNum = 0;
} else {
//输入正常数字包括小数
disNum = parseInt(disNum) - cart.skuNum;
}
break
}
// 派发请求
// this.$store.dispatch('addorderUpdateShopcat',{skuId:cart.skuId,skuNum:disNum})
try {
await this.$store.dispatch('addorderUpdateShopcat', { skuId: cart.skuId, skuNum: disNum })
this.getData()
} catch (error) {
alert(error.message)
}
}, 1000),
删除选中的商品按钮
仓库中context可以得到仓库的所有内容包括,mutatuions actions state getters
方法1:
// 商品全部删除
// 商品全部删除
DelectAllChecked({dispatch,getters}){
//获取购物车中全部的产品
console.log(getters.cartList.cartInfoList);
getters.cartList.cartInfoList.forEach(item => {
// 只能删除勾选的按钮
item.isChecked==1? dispatch('DelCartList',item.skuId):'';
});
}
方法2:
// 商品全部删除
DelectAllChecked ({ dispatch, getters }) {
//获取购物车中全部的产品
let PromiseAll = []
getters.cartList.cartInfoList.forEach(item => {
// 只能删除勾选的按钮
let Promise = item.isChecked == 1 ? dispatch('DelCartList', item.skuId) : '';
PromiseAll.push(Promise)
});
return Promise.all(PromiseAll)
}
全选按钮
以全选按钮为主,点击他为1,不点击是0
//商品全部删除
updateCheckAll ({ dispatch, state }, check) {
let PromiseAll = []
state.cartList[0].cartInfoList.forEach(item => {
let Promise = dispatch('getCheckedupdate', { skuId: item.skuId, isChecked: check })
PromiseAll.push(Promise)
})
return Promise.all(PromiseAll)
}
删除点击事件
// 全选删除按钮
async CheckAllUpadte (event) {
let check = event.target.checked ? '1' : '0'
await this.$store.dispatch('updateCheckAll', check)
this.getData()
}
6.登录注册模块
- 引入login静态组件
- 引入注册组件
1.注册页面:
- 获取后台验证码
/api/user/passport/sendCode/{phone}
获取验证码
get
获取仓库中的state里面的code
data () {
return {
phone: '',
code: ''
}
},
methods: {
async getCode (phone) {
await this.$store.dispatch('getCode', phone)
this.code= this.$store.state.user.code
}
}
user仓库
import { reqGetCode } from '@/request/index'
const state = {
code: ''
}
const mutations = {
GETCODE (state, code) {
state.code = code
}
}
const actions = {
async getCode ({ commit }, phone) {
let result = await reqGetCode(phone)
if (result.code == 200) {
commit('GETCODE', result.data)
}
}
}
const getters = {}
export default {
state,
mutations,
actions,
getters
}
判断账号信息是否填写完整,并注册用户,注册成功后进行路由跳转
data () {
return {
phone: '',
code: '',
password: '',
password1: '',
}
},
methods: {
async getCode (phone) {
await this.$store.dispatch('getCode', phone)
this.code = this.$store.state.user.code
},
userRegister (data) {
const { phone, code, password, password1 } = this
try {
//判断是否输入完整信息
if (phone && code && password && password1 && password == password1) {
//注册账号
this.$store.dispatch('getUserRegister', { phone, password, code })
//进行路由跳转
this.$router.push('/login')
} else {
alert('请输入完整信息')
}
} catch (error) {
alert(error.Mesage)
}
}
}
仓库
async getUserRegister ({commit},user) {
let result = await reqRegisterMessage(user)
if(result.code==200){
return 'ok'
}else{
return Promise.reject(new Error('file'))
}
}
表单验证:
2.登录业务
- 注册--通过数据存储用户信息(名字,密码)
- 登录--登录成功的时候,后台为了区分你这个用户是谁-服务器下发token
- 前台需要持久化存储token
- 将来需要带token找服务器要数据
- vuex存储数据不是永久的,一刷新就没了
点击登录按钮进行跳转
methods: {
async UserLogin () {
try {
const { phone, password } = this
if (phone && password) {
await this.$store.dispatch('UserLogin', { phone, password })
this.$router.push('/home')
}else{
alert('请输入账号或者密码')
}
} catch (error) {
alert(error.message)
}
}
}
}
vuex仓库
const state = {
code: '',
token: '',
userinfo:{}
}
const mutations = {
GETCODE (state, code) {
state.code = code
},
USERLOGIN (state, token) {
state.token = token
},
GETUSERINFO(state,userinfo){
state.userinfo=userinfo
}
}
//登录
async UserLogin ({ commit }, data) {
let result = await reqLogin(data)
if (result.code == 200) {
commit('USERLOGIN', result.data.token)
} else {
return Promise.reject(new Error('账号或密码不正确'))
}
},
1.用户登录携带token
写接口地址 /user/passport/auth/getUserInfo 发起请求
在请求拦截器中进行配置
// 请求拦截器 在发送请求前拦截
requests.interceptors.request.use(function (config) {
if (store.state.detail.uuid_token) {
//请求头添加一共字段(userTempId) 和后台老师商量好了 不能写错
config.headers.userTempId = store.state.detail.uuid_token
}
// 需要携带token给服务器
if(store.state.user.token){
config.headers.token=store.state.user.token
}
nprogress.start()
return config;
})
async UserLogin () {
try {
const { phone, password } = this
if (phone && password) {
await this.$store.dispatch('UserLogin', { phone, password })
alert('登录成功')
// 获取用户信息
await this.$store.dispatch('getUserInfo')
setTimeout(() => {
this.$router.push('/home')
}, 500)
} else {
alert('请输入账号或者密码')
}
} catch (error) {
alert(error.message)
}
}
vuex仓库获取用户信息
//获取用户信息
//如果有token可以获取用户信息,没有则不能
async getUserInfo({commit}){
let result =await reqGetUserInfo();
if(result.code==200){
// 提交用户信息
commit('GETUSERINFO',result.data)
}
}
2.控制显示与隐藏
使用v-if和v-else控制隐藏
<p v-if="!userName">
<span>请</span>
<!-- 声明式导航:router-link务必要有to属性 -->
<router-link to="/login">登录</router-link>
<router-link class="register" to="/register">免费注册</router-link>
</p>
<!-- 登录了 -->
<p v-else>
<a>{{userName}}</a>
<a class="register" @click="logout">退出登录</a>
</p>
computed: {
userName () {
return this.$store.state.user.userinfo.name
}
}
3.用户登录后信息展示
3.1.当用户注册完成,用户登录【用户名+密码】向服务器发起请求
登录成功获取到token,并存放到仓库中(非持久化),路由跳转到home首页
3.2.因此在首页中mounted中派发action(getUserInfo)获取用户信息,一级动态展示header组件内容
3.3 一刷新home首页,获取不到用户信息,原因是因为仓库不是持久化存储 ,token一刷新就没了
解决方案:持久化存储
在user仓库中使用本地存储
token: localStorage.getItem('TOKEN'),
//登录
async UserLogin ({ commit }, data) {
let result = await reqLogin(data)
if (result.code == 200) {
commit('USERLOGIN', result.data.token)
localStorage.setItem('TOKEN',result.data.token)
return 'ok'
} else {
return Promise.reject(new Error('账号或密码不正确'))
}
},
存在问题:
1.多个组件展示用户信息需要在每一个组件的mounted中触发 this.$store.dispatch('getUserInfo')
2.用户登录后不能在回到登录界面
3.退出登录,点击退出登录后用户的token等全部清空
4.退出登录
退出登录要做两件事
1.向服务器发起请求
2.清楚用户信息token
vuex仓库
CLEAR(state){
state.token='',
state.userinfo='',
localStorage.removeItem('TOKEN')
}
async UserLogout ({ commit }) {
let result = await reqLogout()
if (result.code == 200) {
commit('CLEAR')
}else{
return Promise.reject(new Error('faile'))
}
}
//退出登录按钮
logout () {
try {
//如果退出登录成功
this.$store.dispatch('UserLogout')
//跳转到home首页
this.$router.push('/home')
} catch (error) {
alert(error.Message)
}
}
5.导航守卫的理解
之前的登录有两个问题:
问题1:
已经登录就不能再跳转到登录页面
导航守卫
Vue.js是一种轻量级的前端 JavaScript 框架,它提供了一些内置的导航守卫。 Vue.js的导航守卫通常使用 QR 码或二维码来展示导航栏。当用户扫描这个 QR 码或二维码时,导航守卫将调用 Vue.js 的路由和状态管理功能,并将用户重定向到相应的页面或路由。 Vue.js的导航守卫通常也会监听导航栏的点击事件,以便在用户离开当前页面时触发一些操作,例如关闭当前页面或导航到另一个页面。 此外,Vue.js 的导航守卫还可以用于验证用户身份,防止恶意攻击和数据泄露。例如,它可以检查用户是否有足够的权限访问某个页面,或者防止用户尝试访问不允许访问的页面。 总之,Vue.js 的导航守卫是一种非常有用的工具,可以帮助开发人员在前端应用程序中实现更好的安全性和用户体验。 通义千问
全局守卫:只要发生路由的变化,守卫就能监听到
// 全局前置守卫 在路由之前触发
router.beforeEach((to,from,next) => {
})
// 全局前置守卫 在路由之前触发
router.beforeEach((to,from,next) => {
//to: 即将要进入的目标 用一种标准化的方式
//from: 当前导航正要离开的路由 用一种标准化的方式
// next 放行函数 next()
})
导航守卫判断:
从仓库中拿到token就可以判断用户有没有登录
在router.js中引入store
从home页面跳转到search页面需要判断用户信息是否存在
用户登录了但是去的不是login 我们要获取name如果没有就获取用户信息 派发action 再次放行
this.$store.dispatch('getUserInfo')
import store from '@/store'
console.log(store);
let name = store.state.user.userinfo.name
// 全局前置守卫
router.beforeEach((to, from, next) => {
//放行
if (store.state.user.token) {
//如果登录了,并且在login页面 就直接跳转到首页
if (to.path == '/login') {
next('/home')
} else {
//登录了 获取到了用户信息 直接放行
if (name) {
next()
} else {
//登录了 但是没有获取到用户信息 派发一个action获取到用户信息 因为用户刷新的时候用户信息会丢失 所以需要获取一次 然后在放行
try {
store.dispatch('getUserInfo')
next()
} catch (error) {
//token 过期
//清除token 派发退出登录action
alert(error.Message)
}
}
}
} else {
//如果没有登录 就直接放行
next()
}
})
统一登录的账号
账号:13700000000 密码:111111
7.商品结算组件
- 引入静态组件 Trade
- 在路由模块引入
- 使用router-link链接
- 写一个trade的仓库
1.获取交易页数据
1.1获取用户地址的接口
/api/user/userAddress/auth/findUserAddressList
获取用户地址信息
get
1.2获取订单交易信息,用户登录了才能获取
获取商品交易订单
/api/order/auth/trade
GET
detailArrayList 里面有用户的购物车商品
用户地址显示
data () {
return {
userAddress: '',
consignee:'',
phoneNum:''
}
},
mounted () {
this.$store.dispatch('getUserAddress'),
this.$store.dispatch('getUserMessage')
},
computed: {
...mapState({
address: (state) => {
return state.trade.address
}
})
},
methods: {
//干掉所有人,只剩我自己
changDefault (item, address) {
address.forEach(item => {
item.isDefault = 0
})
item.isDefault = 1
this.userAddress = item.userAddress
this.consignee=item.consignee
this.phoneNum=item.phoneNum
},
获取数据并渲染 然后点击对应的地址
8.订单与支付页面
- 完成交易页面
- 提交订单 先搞定支付组件
pay静态组件 点击提交订单跳转到支付页面
提交订单接口
/api/order/auth/submitOrder?tradeNo={tradeNo}
1.1统一引入api
import * as API from '@/request/index'
Vue.component(TypeNav.name, TypeNav)
Vue.component(Pageation.name,Pageation)
Vue.config.productionTip = false
// 引入swiper样式
import "swiper/css/swiper.css";
new Vue({
router,
store,
render: h => h(App),
beforeCreate(){
Vue.prototype.$bus=this,
Vue.prototype.$API=API
}
}).$mount('#app')
微信支付
引入element ui组件
按需引入
使用element-ui中的弹框
使用一些配置
open () {
this.$alert('<strong>这是 <i>HTML</i> 片段</strong>', 'HTML 片段', {
dangerouslyUseHTMLString: true,
showCancelButton:true,
center:true,
confirmButtonText: '已支付成功',
cancelButtonText:'支付遇见问题',
showClose:false
});
}
二维码生成
npm
qrcode
npm i qrcode
let url=await QRCode.toDataURL(' 你好 这个是使用npm里面的一个包生成 由于接口没有数据 我页没办法做 只好写点东西代替')
this.$alert(`<img src=${url}></img>`, '微信支付', {
dangerouslyUseHTMLString: true,
showCancelButton:true,
center:true,
confirmButtonText: '已支付成功',
cancelButtonText:'支付遇见问题',
showClose:false
});
}
支付成功路由跳转 ,支付失败提示弹框
async open () {
let url = await QRCode.toDataURL('这个是使用npm里面的一个包生成的,由于接口没有数据,我也没有办法做 只好写点东西代替')
this.$alert(`<img src=${url}></img>`, '微信支付', {
dangerouslyUseHTMLString: true,
showCancelButton: true,
center: true,
confirmButtonText: '已支付成功',
cancelButtonText: '支付遇见问题',
showClose: false
});
//支付成功 进行路由跳转
//写一个定时器 每一秒发一次请求 检测用户是否支付成功
// 发请求获取用户支付状态
if (!this.timer) {
this.timer = setInterval(async () => {
let result = await this.$API.reqpayment(21)
if(result.code==200){
//如果支付成功清除定时器
clearInterval(timer)
timer=null
//保存返回的code
this.code=result.code
//关闭弹出框
this.$mesbox.close()
//支付成功跳转到下一个组件
this.$router.push('/paysuccess')
}
},1000)
}
}
使用element ui 弹框中的beforeClose属性
可以知道你点击了哪个按钮 done关闭弹框
async open () {
let url = await QRCode.toDataURL('这个是使用npm里面的一个包生成的,由于接口没有数据,我也没有办法做 只好写点东西代替')
this.$alert(`<img src=${url}></img>`, '微信支付', {
dangerouslyUseHTMLString: true,
showCancelButton: true,
center: true,
confirmButtonText: '已支付成功',
cancelButtonText: '支付遇见问题',
showClose: false,
//关闭弹出框
beforeClose: (type, instance, done) => {
if (type == 'cancel') {
alert("请询问管理员")
//关闭弹窗
done()
}else{
//判断是否真的支付成功
//因为接口没有使用 所以我们取消这个判断 点击支付后直接跳转
// if(this.code==200){
//关闭定时器
this.timer=null
//关闭弹窗
done();
//路由跳转
this.$router.push('/paysuccess')
}
}
});
//支付成功 进行路由跳转
//写一个定时器 每一秒发一次请求 检测用户是否支付成功
// 发请求获取用户支付状态
let result = await this.$API.reqpayment(21)
if (result.code == 200) {
//如果支付成功清除定时器
clearInterval(timer)
timer = null
//保存返回的code
this.code = result.code
//关闭弹出框
this.$mesbox.close()
//支付成功跳转到下一个组件
this.$router.push('/paysuccess')
}
}
9.产看订单 (个人中心)
导入center组件
二级路由
//产看订单 个人中心
{
path: '/center', component: Center,
children: [
{ path: '/center', redirect: '/center/myorder' },
{ path: 'grouporder', component: GroupOrder },
{ path: 'myorder', component: MyOrder }
]
},
获取订单列表接口
/api/order/auth/{page}/{limit}
10.未登录导航守卫设置
面试:
封装过分页器和日历
没有登录不能交易页,也不能去支付相关的pay paysuccess , 不能去个人中心
router.beforeEach((to, from, next) => {
//如果登录了获取token
if (store.state.user.token) {
// 如果在login页面跳转到home页面
if (to.path == '/login') {
next('/home')
} else {
//登录了如果在其他页面 需要判断页面中有没有userinfo 如果没有需要进行派发 不然用户一刷新token就没了 就需要从新登录
if (name) {
next()
} else {
try {
//没有userinfo进行派发获取用户数据
store.dispatch('getUserInfo')
next()
} catch (error) {
// token过期
// 派发退出登录的接口
store.dispatch('UserLogout')
}
}
}
} else {
//如果没登陆 有一些页面不能去 没有登录不能交易页,也不能去支付相关的pay paysuccess , 不能去个人中心
let toPath=to.path
if(toPath.indexOf('/trade')!=-1 || to.path.indexOf('/pay')!=-1 ||to.path.indexOf('/paysuccess')!=-1 ||to.path.indexOf('/center/myorder')!=-1){
//把未登录的时候而没有去成的地址 存储在地址栏中
next('/login?redirect='+toPath)
}else{
next()
}
}
login点击
async UserLogin () {
try {
const { phone, password } = this
if (phone && password) {
await this.$store.dispatch('UserLogin', { phone, password })
// 获取用户信息
await this.$store.dispatch('getUserInfo')
// this.$router.push('/home')
// let path = this.$route.query.redirect || '/home'
// this.$router.push(path)
//判读路径中是否有query 如果有跳转到query 没就跳转到home首页
if(this.$route.query.redirect){
this.$router.push(`${this.$route.query.redirect}`)
}else{
this.$router.push('/home')
}
} else {
alert('请输入账号或者密码')
}
} catch (error) {
alert(error.message)
}
}
11.用户独享守卫
方法1:使用路由独享守卫
因为必须从购物车到交易页 ,而不能从其他页面到交易页所以我们使用路由独享守卫
同样到支付页面pay 需要经过trade
方法2:组件内守卫:
export default {
name: 'PaySuccess',
beforeRouteEnter (to, from, next) {
// 在渲染该组件的对应路由被验证前调用
// 不能获取组件实例 `this` !
// 因为当守卫执行时,组件实例还没被创建!
if (from.path == '/pay') {
next()
} else {
next(false)
}
},
}
路由独享守卫:
//购物车最后结算
{
path: '/trade', component: Trade,
meta: { show: true },
//必须从购物车去交易页
beforeEnter: (to, from, next) => {
if (from.path == '/shopcart') {
next()
} else {
next(false)
}
}
},
//支付页面
{
path: '/pay', component: Pay,
meta: { show: true },
beforeEnter: (to, from, next) => {
if (from.path == '/trade') {
next()
} else {
next(false)
}
}
},
vue-router官方文档
10.懒加载
1.图片懒加载
vue-lazyload插件
npm i vue-lazyload
全局自定义指令
2.路由懒加载
vue-router官方文档
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效。
// 路由重定向
{
path: '/', redirect: '/home',
},
//购物车结算
{
path: '/shopcart', component: ()=>import('@/page/ShopCart/index.vue')
},
//购物
{
path: '/addcartsuccess', component: ()=>import('@/page/AddCartSuccess/index.vue'),
name: 'addcartsuccess'
},
不再直接从路由模块中引入组件 使用懒加载会更高效
11.打包上线
npm run build
map文件
项目打包后,代码都是经过加密的,如果运行时候报错,输出错误信息无法准确的得知是哪里的代码报错,所以就有了map文件可以准确的输出哪一行代码报错
项目上线可以不需要map文件了,进行配置删除
在vue.config.js中加入 productionSourceMap:false
购买服务器
部署上线
niginx配置
1.进入etc目录
2.进入nginx目录
3.如果想安装niginx
使用 yum install nginx
4.安装完nginx服务器以后,在nginx.config 中进行编辑
5.vim nginx.config
6.编辑的时候需要添加
location / {
root /root/jch/www/shangpinhui/dist;
index index.html;
try_files uri/ /index.html;
}
数据来源 保证访问公网ip就能拿到数据