文章目录
- 最好使用视频上的账号密码,13700000000 密:111111
- 最新服务端接口地址:http://gmall-h5-api.atguigu.cn
- 脚手架使用
- 一、项目路由分析
- 二、Header、Footer非路由组件完成
- 三、TypeNav三级联动组件完成
- 四、Home首页拆分静态组件完成
- 五、动态渲染三级联动部分
- 六、mockjs模拟数据 | Home首页完成
- 七、Search模块
- 八、分页器
- 九、Detail商品详情组件
- 十、添加到购物车成功 路由组件
- 十一、购物车组件
- 十二、登录注册组件
- 十三、Trade交易组件
- 十四、支付页面
- 十五、微信支付业务
- 十六、Center个人中心组件
- (17)用户登录后的导航守卫(路由独享守卫与组件内守卫)
- (18)未登录的导航守卫
- (19)图片懒加载
- (20)vee-validate插件 表单验证
- (21)路由懒加载
- (22)处理map文件
- (23)服务器
最好使用视频上的账号密码,13700000000 密:111111
最新服务端接口地址:http://gmall-h5-api.atguigu.cn
因为该账号数据库存有地址信息 。
但是注意,因为很多同学同时操作,所以可能出现:购物车商品被其他同学增加或减少,订单不能重复提交等问题。此时稍等一会,或多试几次即可。
学习尚硅谷的商城前台尚品汇项目的笔记:
脚手架使用
1、创建项目
vue create 项目名称
2、脚手架默认目录:
- node_modules:放置项目依赖的地方。
- public:一般放置一些共用的静态资源,打包上线的时候,public文件夹里面资源原封不动打包到dist文件夹里面。
- src:程序员源代码文件夹:
- assets:经常放置一些静态资源(公用的图片(即很多组件都用此图)),assets文件夹里面资源webpack会进行打包为一个模块(js文件夹里面)
- components:一般放置非路由组件(如共用的组件)
- App.vue:唯一的根组件
- main.js:入口文件【程序最先执行的文件】
- babel.config.js:babel配置文件
- package.json:项目描述、项目依赖、项目运行
- README.md:项目说明文件
注意:
(放public中的图片,组件引用:images/1.png
组件的less中引用:url(/images/9.png);
使用"/"作为根目录。 因为最后webpack打包,public中的图片原封不动打包到了images中,所以用绝对路径)
放assets中的图片,组件的html中引用:@/assets/2.png
组件的less中引用:~@/assets/7.png
因为assets文件夹里面资源webpack会进行打包为一个模块(js文件夹里面),所以用相对路径)
3、脚手架下载的项目稍微配置一下
-
1)浏览器自动打开
在 package.json 文件中
"scripts": { "serve": "vue-cli-service serve --open", "build": "vue-cli-service build", "lint": "vue-cli-service lint" },
-
2)关闭 eslint 校验工具,以防写代码时没错也报错。
在根目录创建 vue.config.js 文件:需要对外暴露
module.exports = { lintOnSave: false, }
-
3)src文件夹的别名的设置
因为项目大的时候src(源代码文件夹):里面目录会很多,找文件不方便,设置src文件夹的别名的好处,找文件会方便一些。
就不用用那么多…/…/了。直接@代替。
(js用
@
代替src路径,css用~@
代替src路径)创建 jsconfig.json 文件
{ "compilerOptions": { "baseUrl": "./", "paths": { "@/*": [ "src/*" ] } }, "exclude": [ "node_modules", "dist" ] }
一、项目路由分析
路由组件:
Home首页、Search搜索、login登录、Refister注册
非路由组件:
Header头部、Footer底部 (有Home首页、Search搜索组件。无login登录、Refister注册组件)
二、Header、Footer非路由组件完成
在开发项目的时候:
1: 书写静态页面(HTML + CSS)
2: 拆分组件
3: 获取服务器的数据动态展示
4: 完成相应的动态业务逻辑
需要用到less ,安装低版本,高版本容易出错。
yarn add less less-loader@5
(组件中,需要加 <style scoped lang="less">
1、使用非路由组件步骤:
- 创建
- 引入
- 注册
- 用标签使用
1、创建在 components下
2、引入到(需要的组件中)App.vue中,并注册、使用。
<template>
<div id="app">
<Header></Header>
<Footer></Footer>
</div>
</template>
<script>
import Header from './components/Header'
import Footer from './components/Footer'
export default {
name: 'App',
components: {
Header,
Footer
}
}
</script>
2、使用路由组件步骤:
-
yarn add vue-router
3版本 -
在src 创建 pages | views文件夹:放置路由组件。
-
配置路由:
在src 创建 router文件夹,建 index.js
index.js:
// 配置路由
import Vue from 'vue';
import VueRouter from 'vue-router';
Vue.use(VueRouter);
// 引入路由组件
import Home from '@/pages/Home'
import Search from '@/pages/Search'
import Login from '@/pages/Login'
import Register from '@/pages/Register'
export default new VueRouter({
routes: [
{
path: "/home",
component: Home
},
{
path: "/Search",
component: Search
},
{
path: "/Login",
component: Login
},
{
path: "/Register",
component: Register
},
]
})
还要去main.js引入和注册路由
main.js
import Vue from 'vue'
import App from './App.vue'
// 引入路由
import router from '@/router';
Vue.config.productionTip = false
new Vue({
render: h => h(App),
router
}).$mount('#app')
路由组件出口,路由组件展示
App.vue
<template>
<div id="app">
<Header></Header>
<!-- 路由组件出口的地方、路由组件展示 -->
<router-view></router-view>
<Footer></Footer>
</div>
</template>
1) 路由的跳转有两种形式:
注册完路由,不管路由路由组件、还是非路由组件身上都有$route、$router属性。
$route:一般获取当前组件的路由信息 [路径、query、 params等等]
$router:一般进行编程式导航进行路由跳转 [push | replace]
1、声明式导航:router-link,可以进行路由的跳转
<router-link to="/login">登录</router-link>
2、编程式导航:利用组件实例的 $router.push | replace,可以进行路由跳转
// 搜索按钮的回调函数,点击按钮跳转至search路由
goSearch() {
this.$router.push("/search");
},
编程式导航:声明式导航能做的,编程式导航都能
但是编程式导航除了可以进行路由跳转,还可以做一些其他的业务逻辑。
2) 路由元信息:
将任意信息附加到路由上,如过渡名称、谁可以访问路由等。这些事情可以通过$route 的 meta属性 来实现,并且它可以在路由地址和导航守卫上都被访问到。
Footer组件的显示与隐藏
显示或隐藏组件: v-if | v-show
Footer组件 在Home、Search显示 在登录注册隐藏。
router中的index.js:
{
path: "/home",
component: Home,
meta: {show:true}
},
{
path: "/search",
component: Search,
meta: {show:true}
},
{
path: "/login",
component: Login,
meta: {show:false}
},
{
path: "/register",
component: Register,
meta: {show:false}
},
]
<!-- meta.show在路由配置中定义 -->
<Footer v-show="$route.meta.show"></Footer>
3) 路由传递参数
路由传参,参数有几种写法?
params参数: 属于路径当中的一部分,需要注意,在配置路由的时候,需要占位
query参数: 不属于路径当中的一部分,类似于ajax中的queryString /home?k=v&kv=,不需要占位
Header/index.vue
<input
type="text"
id="autocomplete"
class="input-error input-xxlarge"
v-model="keyword"
/> <!-- keyword数据绑定 -->
data() {
return {
keyword: "",
};
},
methods: {
// 搜索按钮的回调函数,点击按钮跳转至search路由
goSearch() {
// 路由传递参数:
// 第一种:字符串形式
// this.$router.push(
// "/search/" + this.keyword + "?k=" + this.keyword.toUpperCase() // toUpperCase()转为大写字母
// 第二种:模板字符串
// this.$router.push(
// `/search/${this.keyword}?k=${this.keyword.toUpperCase()}`
// );
// 第三种:对象(常用)
this.$router.push({
name: "search",
params: { keyword: this.keyword },
query: { k: this.keyword.toUpperCase() },
});
},
search/index.vue
<div>
<h1>params参数---{{ $route.params.keyword }}</h1>
<h1>query参数---{{ $route.query.k }}</h1>
</div>
router/index.js
{
// params参数在配置路由的时候,需要占位
path: "/search/:keyword",
component: Search,
meta: {show:true},
// 对象形式路由传递参数
name: "search",
},
路由传递参数面试题:
-
路由传递参数(对象写法) path是否可以结合 params参数一起使用? 即:
this.$router.push({ path: '/search', params: { keyword: this.keyword }, query: { k: this.keyword.toUpperCase() }, });
答:报错,不能。
-
如何指定 params参数 可传可不传? 即:
this.$router.push({ name: "search", query: { k: this.keyword.toUpperCase() }, });
答:配置路由时,path上加个 ? 号,代表可传参数也可不传;若不加 ? ,则URL会出现问题。
{
path: "/search/:keyword?",
component: Search,
meta: {show:true},
// 对象形式路由传递参数
name: "search",
},
-
params参数 可以传递也可以不传递,但是如果传递是空串,如何解决? 即:
this.$router.push({name:"search",params:{keyWord:''},query:{k:this.keyWord}})
答:可以使用 undefined 来解决params参数可以传递也可不传递(空的字符串)
this.$router.push({
name: "search",
params: { keyword: '' || undefined },
query: { k: this.keyword.toUpperCase() },
});
},
- 路由组件能不能传递 props数据?
答:可以。三种写法:
{
path: "/search/:keyword",
component: Search,
meta: { show: true },
// 对象形式路由传递参数
name: "search",
// 路由组件能不能传递 props数据?
// 1、布尔值写法,但是这种方法只能传递params参数
// props: true,
// 2、对象写法:额外给路由组件传递一些props
// props: { a: 1, b: 2 },
// 函数写法(常用):可以params参数、query参数,通过props传递给路由组件
props: ($route) => {
return {keyword: $route.params.keyword, k: $route.query.k};
}
},
<div>
<h1>params参数---{{ $route.params.keyword }}</h1>
<h1>query参数---{{ $route.query.k }}</h1>
<h1>props数据---{{ keyword }}</h1>
<!-- <h1>props数据---{{ a }}--{{ b }}</h1> -->
</div>
</template>
<script>
export default {
name: '',
props: ['keyword', 'a', 'b'],
}
4) 重写push与repalce方法
编程式路由跳转 到当前路由(参数不变),多次执行跳转到当前路由,会抛出NavigationDuplicated的警告错误?
– 路由跳转有两种形式:声明式导航、编程式导航
– 声明式导航 没有这类问题的,因为vue-router底层已经处理好了。“vue-router”: “^3.5.3” 最新的vue-router引入promise。
为什么 编程式导航 这时会有问题?
this.$router.push({
name: "search",
params: { keyword: this.keyword },
query: { k: this.keyword.toUpperCase() },
},
()=>{},()=>{});
这种写法:治标不治本,将来在别的组件当中push | replace,编程式导航还是有类似错误。
this:当前组件实例( search)
this.$router
:VueRouter的实例。当在入口文件注册路由的时候,给组件实例添加了$router | $route属性。
即: let $router = new VueRouter();
VueRouter是一个构造函数。
push:是VueRouter的一个方法
即: VueRouter.prototype.push = function() {}
$router 借用原型对象的方法:
$router.push(xxx)
router/index.js
// 保存原来的push函数
let originPush = VueRouter.prototype.push;
// 保存原来的replace函数
let originReplace = VueRouter.prototype.replace;
// 重写push函数,为解决相同路径跳转报错
// 第一个参数: 告诉原来push方法,你往哪里跳转(传递哪些参数)
// 第二个参数: 成功回调 第三个参数: 失败的回调
VueRouter.prototype.push = function(location, resolve, reject) {
if(resolve && reject) {
originPush.call(this, location, resolve, reject);
} else {
originPush.call(this, location, () => { }, () => { });
}
}
// 重写replace函数,为解决相同路径跳转报错
VueRouter.prototype.replace = function(location, resolve, reject) {
if(resolve && reject) {
originReplace.call(this, location, resolve, reject);
} else {
originReplace.call(this, location, () => { }, () => { });
}
}
三、TypeNav三级联动组件完成
因三级联动组件存在于Home、Search、Detail组件中,所以注册为全局组件。
main.js
// 三级联动组件--注册为全局组件(在各组件中使用就不需要引入了)
import TypeNav from '@/components/TypeNav'
// 第一个参数:全局组件的名字 第二个参数:哪一个组件
Vue.component(TypeNav.name, TypeNav)
components/TypeNav/index.vue
<template>
<div>
主页
<TypeNav></TypeNav>
</div>
</template>
<script>
</script>
<style lang="less" scoped>
</style>
四、Home首页拆分静态组件完成
Home/index.vue
<template>
<div>
<TypeNav/>
<ListContainer/>
<Recommend/>
<Rank/>
<Like/>
<Floor/>
<Floor/>
<Brand/>
</div>
</template>
<script>
import ListContainer from '../Home/ListContainer';
import Recommend from '../Home/Recommend';
import Rank from '../Home/Rank';
import Like from '../Home/Like';
import Floor from '../Home/Floor';
import Brand from '../Home/Brand';
export default {
name: '',
components: {
ListContainer,
Recommend,
Rank,
Like,
Floor,
Brand,
}
}
</script>
<style lang="less" scoped>
</style>
五、动态渲染三级联动部分
1、postman测试接口
填入服务器地址和请求地址,即
http://gmall-h5-api.atguigu.cn/api/product/getBaseCategoryList
200ok 说明成功。接口没问题。
2、axios 二次封装
为什么二次封装?
为了请求拦截器、响应拦截器。
请求拦截器:在发请求之前可以处理一些业务;
响应拦截器:当服务器数据返回以后,可以处理一些事情。
安装axios
yarn add axios
在项目中经常有 api文件夹,一般都是放 axios的
api/request.js
// 对于axios进行二次封装
import axios from "axios"
// 利用axios对象的方法create,去创建一个axios实例
// 这里的request 就是 axios,在这里配置一下
const request = axios.create({
// 配置对象
// 基础路径,发请求的时候,路径当中会默认有/api,不用自己写了
baseURL: "/api",
// 请求超时5s
timeout: 5000,
})
// 请求拦截器:在发请求之前,请求拦截器可以检测到,在请求发出之前做一些事情;
requests.interceptors.request.use((config) => {
// config:配置对象,其有一个重要属性:header请求头
})
// 响应拦截器:当服务器数据返回以后,可以处理一些事情。
requests.interceptors.response.use(((res) => {
// 服务器响应成功的回调函数
return res.data;
}, (error) => {
// 服务器响应失败的回调函数
return Promise.reject(new Error('faile'));
}))
// 对外暴露
export default requests;
3、API接口统一管理
若项目很小,可以在组件的生命周期函数中发请求
但项目大,组件多,若有更改,将麻烦。所以API接口统一管理。
什么是跨域?
同源就是指,域名、协议、端口均为相同。
跨域,是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对JavaScript实施的安全限制。
vue.config.js
// webpack中的代理跨域
devServer: {
proxy: {
'/api': {
// 服务器地址
target: 'http://gmall-h5-api.atguigu.cn',
},
},
},
api/index.js
import requests from "./request";
// 三级联动接口
export const reqCategoryList = () =>
// 发请求:axios发请求返回结果是Promise对象
requests({ url: "/product/getBaseCategoryList", method: "get" })
4、nprogress进度条的使用
安装: yarn add nprogress
在响应拦截器使用
api/request.js
// 引入进度条
import nprogress from 'nprogress'
// 引入进度条样式
import "nprogress/nprogress.css"
// 请求拦截器:
requests.interceptors.request.use((config) => {
// config:配置对象,其有一个重要属性:header请求头
// 进度条开始动
nprogress.start();
return config;
})
// 响应拦截器:
requests.interceptors.response.use((res) => {
// 服务器响应成功的回调函数
// 进度条结束
nprogress.done();
return res.data;
}, (err) => {
// 服务器响应失败的回调函数
return Promise.reject(new Error('faile'));
})
5、vuex 模块式开发
vuex 是官方提供的插件, 状态管理库,集中式管理项目中组件共用的数据 。
切记,并不是全部项目都需要 Vuex,如果项目很小,完全不需要Vuex,如果项目很大,组件很多、数据很多,数据维护很费劲,用Vuex
安装vuex yarn add vuex
main.js
// 引入仓库
import store from './store'
store/home/index.js
// home 模块的小仓库
// state:仓库存储数据的地方
const state = {}
// mutations:修改state的唯一手段
const mutations = {}
// actions:处理action,书写自己的业务逻辑、也可以处理异步
const actions = {}
// getters:计算属性,用于简化仓库数据,让组件获取仓库的数据更方便
const getters = {}
// 对外暴露
export default {
state,
mutations,
actions,
getters,
}
store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
// 引入小仓库
import home from './home'
import search from './search'
export default new Vuex.Store({
modules: {
home,
search,
}
})
6、动态展示三级联动数据
api/index.js
import requests from "./request";
// 三级联动接口
export const reqCategoryList = () =>
// 发请求:axios发请求返回结果是Promise对象
requests({ url: "/product/getBaseCategoryList", method: "get" })
store/home/index.js
// home 模块的小仓库
import { reqCategoryList } from "@/api"
// state:仓库存储数据的地方
const state = {
categoryList: [],
}
// mutations:修改state(数据)的唯一手段
const mutations = {
CATEGORYLIST(state, categoryList) {
state.categoryList = categoryList
}
}
// actions:处理action,书写自己的业务逻辑、也可以处理异步
const actions = {
// {commit}是因为其中要用到commit,原本是categoryList(context,value){}, 用commit时需要context.commit,{commit}可以省略context. 若不需要value则可不写。
async categoryList({commit}) {
// 向服务器发请求
let result = await reqCategoryList();
// console.log(result)
// result.code == 200代表请求成功
if (result.code == 200) {
// 修改数据
commit("CATEGORYLIST", result.data)
}
},
}
// 对外暴露
export default {
state,
mutations,
actions,
getters,
}
components/TypeNav/index.vue
<script>
import { mapState } from "vuex";
export default {
name: "TypeNav",
// 组件挂载完毕,可以向服务器发请求
mounted() {
// 通知Vuex发请求,获取数据,存储于仓库当中
this.$store.dispatch("categoryList");
},
computed: {
...mapState({
// 从仓库拿数据
categoryList: (state) => state.home.categoryList,
}),
},
};
</script>
<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.categroyId"
>
<dl class="fore">
<dt>
<a href="#">{{ c2.categoryName }}</a>
</dt>
<dd>
<em
v-for="(c3, index) in c2.categoryChild"
:key="c3.categroyId"
>
<a href="#">{{ c3.categoryName }}</a>
</em>
</dd>
</dl>
</div>
</div>
</div>
</div>
7、动态一级菜单背景颜色
鼠标移出全部商品分类时,一级菜单背景消失
<div @mouseleave="leaveIndex">
<h2 class="all">全部商品分类</h2>
实现鼠标在当前标题,当前标题背景颜色设为蓝色,排他高亮显示
<div
class="item"
v-for="(c1, index) in categoryList"
:key="c1.categoryId"
>
<!-- 当鼠标进入当前的一级标题,则改变currentIndex为当前的index -->
<h3
@mouseenter="changeIndex(index)"
:class="{ cur: currentIndex == index }"
> <!-- 若currentIndex==index ,则加类名cur,实现鼠标在当前标题,当前标题背景颜色设为蓝色,排他高亮显示 -->
<a href="#">{{ c1.categoryName }}</a>
</h3>
data() {
return {
// 存储用户移上哪一个一级分类
currentIndex: -1,
};
},
methods: {
// 鼠标进入则修改响应式数据currentIndex属性
changeIndex(index) {
// index:鼠标移上某个一级分类元素的索引值
this.currentIndex = index;
},
// 鼠标移出
leaveIndex() {
this.currentIndex = -1;
},
},
8、JS控制二三级分类显示与隐藏
之前是使用 css的 display: block | none;
现用 js 实现
<div
class="item-list clearfix"
:style="{ display: currentIndex == index ? 'block' : 'none' }"
>
9、函数防抖与函数节流
methods: {
// 鼠标进入则修改响应式数据currentIndex属性
changeIndex(index) {
// index:鼠标移上某个一级分类元素的索引值
this.currentIndex = index;
console.log(this.currentIndex);
// 当鼠标移动过快,可能只有部分h3触发了鼠标进入事件,会出现,1 2 3 9 的情况。
},
用户行为过快,导致浏览器反应不过来。
如果当前回调函数中有一些大量业务,有可能出现卡顿现象。
节流: 在规定的间隔时间范围内不会重复触发回调,只有大于这个时间间隔才会触发回调,把频繁触发变为少量触发。
防抖: 前面的所有的触发都被取消,最后一次执行在规定的时间之后才会触发,也就是说如果连续快速的触发只会执行一次。
防抖案例:
<p>输入搜索内容:<input type="text"></p>
<script>
let input = document.querySelector('input');
// 文本发生变化立即执行,则用户输入时,将多次执行
// input.oninput = function () {
// console.log('ajax请求')
// }
// 但是常规是,用户输入完成后才执行
// 文本输入完成后1s后执行,则将只执行1次
// 用到了 lodash插件,其中封装有函数的防抖与节流业务
// lodash函数对外暴露 _函数 使用 debounced(防抖动)函数
input.oninput = _.debounce(function () {
console.log('ajax请求')
}, 1000)
</script>
节流案例:
<div>
<h1>我是计数器:<span>0</span></h1>
<button>点我+1</button>
</div>
<script>
let span = document.querySelector('span')
let button = document.querySelector('button')
let count = 0
// 用到了 lodash插件,其中封装有函数的防抖与节流业务
// lodash函数对外暴露 _函数 使用 throttle(节流)函数
// 计数器在两秒以内,即使点击很多次,数字也只加一次1
button.onclick = _.throttle(function () {
count++
span.innerHTML = count
}, 2000)
10、三级联动节流
之前下载过的有些依赖会依赖lodash,所以不用下载lodash了。
components/TypeNav/index.vue
// 引入lodash插件中的throttle节流函数
import throttle from "lodash/throttle";
// 鼠标进入则修改响应式数据currentIndex属性
// changeIndex(index) {
// // index:鼠标移上某个一级分类元素的索引值
// this.currentIndex = index;
// },
// 为防止用户行为过快,使用节流 lodash插件中的throttle节流函数
changeIndex: throttle(function (index) {
this.currentIndex = index;
},50),
11、三级联动路由跳转与传递参数
用户点击一级分类、二级分类、三级分类时,Home模块则跳转到Search模块,会把用户选中的产品(产品的名字、产品的ID)在路由跳转的时候,进行传递。
路由跳转:
声明式导航: router-link
编程式导航: push | replace
若使用声明式导航:router-link,可以实现路由的跳转与传递参数。
但是会出现卡顿现象。router-link 是一个组件,当服务器的数据返回之后,循环出很多的router-link组件【创建组件实例的】 1000+。创建组件实例的时候,一瞬间创建1000+很耗内存,因此出现了卡顿现象。
所以用编程式导航: push | replace
利用编程式导航+事件委派
components/TypeNav/index.vue
若每个a标签都绑定点击事件,则会降低性能,所以事件委派给父元素
<div class="sort">
<div class="all-sort-list2" @click="goSearch">
为了传递对应的路由跳转参数,给每级的 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) {
// 获取当前触发事件的元素,(h3、a、。。)
let element = event.target;
// 带有 data-categoryName属性的是a标签
// 节点有一个属性dataset属性,可以获取节点的自定义属性
// event.target.dataset=当前节点的自定义属性名。event.target.dataset.属性名=当前节点的自定义属性值 以下是es6语法 相当于 let xxx = element.dataset.xxx
// 获取自定义属性值
let { categoryname, category1id, category2id, category3id } =
element.dataset;
if (categoryname) {
// 路由跳转的参数
let location = { name: "search" };
let query = { categoryName: categoryname };
if (category1id) {
query.category1Id = category1id;
} else if (category2id) {
query.category2Id = category2id;
} else {
query.category2Id = category2id;
}
location.query = query
// 路由跳转
this.$router.push(location)
}
},
12、TypeNav组件一级商品分类的显示与隐藏,和过渡动画
在Search页也有三级联动即TypeNav组件。
在Search页 一级分类是隐藏的,滑过才会显示。
用响应式数据 控制一级分类的显示与隐藏。
TypeNav/index.vue
<div class="sort" v-show="show">
data() {
...
// 控制一级分类的显示与隐藏
show: ture,
};
mounted() {
...
// 如果当前不是Home路由,则将typeNav组件中一级分类隐藏
if (this.$route.path != '/home') {
this.show = false
}
<div @mouseleave="leaveShow" @mouseenter="enterShow">
<h2 class="all">全部商品分类</h2>
// 在Search路由组件时,鼠标移入,一级分类展示
enterShow() {
this.show = true
}
leaveShow() {
// h3无背景颜色
this.currentIndex = -1;
// 在非Home路由组件,鼠标离开,一级目录隐藏
if (this.$route.path != "/home") {
this.show = false;
}
},
过渡动画: 前提 组件或元素务必要有 v-if | v-show指令 才可以进行过渡动画。
加<transition>
标签
<h2 class="all">全部商品分类</h2>
<!-- 过渡动画 -->
<transition name="sort">
<div class="sort" v-show="show">
// 过渡动画的样式
// 过渡动画开始状态(进入)
.sort-enter {
height: 0;
}
// 过渡动画结束状态(进入)
.sort-enter-to {
height: 461px;
}
// 定义动画时间、速率
.sort-enter-active {
transition: all .5s linear;
}
// 离开没有加动画。
13、TypeNav商品分类列表的优化
在Home组件和Search组件中都用到了TypeNav组件,总共发了2次ajax请求,但是请求的数据是一样的,可以优化为只发1次请求。
将TypeNav组件中的派发action,放入根组件App.vue 即可实现只发1次请求。
将数据存入store仓库中,其他组件也可以用,所以可以在App.vue派发action。
App.vue
mounted() {
// 派发一个action 获取商品分类的三级列表数据
this.$store.dispatch("categoryList");
}
14、合并参数
如果不合并参数,则只能传递query参数或params参数。
但是我们想既传递选中的菜单的参数,又想传递搜索框中的参数,就要合并参数,如下:
TypeNav/index.vue
location.query = query;
// 合并参数,即若有params参数,则和query参数一起传递
if (this.$route.params) {
location.params = this.$route.params;
}
// 路由跳转
this.$router.push(location);
}
Header/index.vue
// 搜索按钮的回调函数,点击按钮跳转至search路由
goSearch() {
let location = {
name: "search",
params: { keyword: this.keyword || undefined },
};
// 合并参数,如果有query参数,则和params参数一起传递
if (this.$route.query) {
location.query = this.$route.query;
}
this.$router.push(location);
},
六、mockjs模拟数据 | Home首页完成
服务器中Home首页模块除了商品三级分类数据,其他数据没有,我们需要 模拟数据以完成Home首页。
使用Mock.js插件,生成随机数据,拦截 Ajax 请求。
-
前后端分离:让前端攻城师独立于后端进行开发。
-
增加单元测试的真实性:通过随机数据,模拟各种场景。
-
开发无侵入:不需要修改既有代码,就可以拦截 Ajax 请求,返回模拟的响应数据。
-
用法简单:符合直觉的接口。
-
数据类型丰富:支持生成随机的文本、数字、布尔值、日期、邮箱、链接、图片、颜色等。
-
方便扩展:支持支持扩展更多数据类型,支持自定义函数和正则。
1、安装:(mockjs没有 .) yarn add mockjs
2、创建 src/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"
}
]
注意:json文件中不要有空格,否则程序报错。
3、把mock数据需要的图片放到 public文件夹中。
4、创建 mock/mockServe.js 通过mockjs插件实现模拟数据
// 先引入mockjs模块
import Mock from 'mockjs';
// 把JSON数据格式引入进来
// [JSON数据格式根本没有对外暴露,但是可以引入],因为 webpack默认对外暴露的:图片、JSON数据格式
import banner from './banner.json';
import floor from './floor.json';
// 模拟首页大轮播图的数据
// mock数据:第一个参数:请求地址 第二个参数:请求数据(200代表请求成功,data代表数据)
Mock.mock("/mock/banner", {
code: 200,
data: banner
});
Mock.mock("/mock/floor", {
code: 200,
data: floor
});
5、mockServe.js文件在入口文件中引入(至少需要执行一次,才能模拟数据)
// 引入mockServe.js ----mock数据
import '@/mock/mockServe'
1、获取Banner轮播图的数据
创建 api/mockAjax.js
将之前真实的访问服务器的 ajax.js 复制并修改路径,
baseURL: "/mock", // 将api修改为mock
api/index.js
import requests from "./ajax";
import mockRequests from "./mockAjax";
// 三级联动接口
export const reqGetCategoryList = () =>
// 发请求:axios发请求返回结果是Promise对象
requests({ url: "/product/getBaseCategoryList", method: "get" })
// 获取首页轮播图的接口
export const reqGetBannerList = () => mockRequests({ url: "/banner", method: "get" })
store/home/index.js
// home 模块的小仓库
import { reqGetCategoryList,reqGetBannerList } from "@/api"
// state:仓库存储数据的地方
const state = {
// 存储三级菜单数据
categoryList: [],
// 存储轮播图数据
bannerList: [],
}
// mutations:修改state(数据)的唯一手段
const mutations = {
GETCATEGORYLIST(state, categoryList) {
state.categoryList = categoryList
},
GETBANNERLIST(state, bannerList) {
state.bannerList = bannerList
}
}
// actions:处理action,书写自己的业务逻辑、也可以处理异步
const actions = {
// 获取三级菜单数据
async categoryList({commit}) {
// 向服务器发请求
let result = await reqGetCategoryList();
// console.log(result)
// result.code == 200代表请求成功
if (result.code == 200) {
// 修改数据
commit("GETCATEGORYLIST", result.data)
}
},
// 获取首页轮播图数据
async getBannerList({commit}) {
let result = await reqGetBannerList();
if (result.code == 200) {
// 修改数据
commit("GETBANNERLIST", result.data)
}
}
}
// getters:计算属性,用于简化仓库数据,让组件获取仓库的数据更方便
const getters = {}
// 对外暴露
export default {
state,
mutations,
actions,
getters,
}
pages/Home/ListContainer/index.vue
import {mapState} from 'vuex'
export default {
name: 'ListContainer',
mounted() {
// 派发action: 通过Vuex发起ajax请求,将数据仓储在仓库当中
this.$store.dispatch('getBannerList')
},
computed: {
...mapState({
bannerList: state => state.home.bannerList
})
}
2、使用swiper轮播图插件
安装 yarn add swiper@5
(6版本似乎有问题)
pages/Home/ListContainer/index.vue
// 引入swiper插件
import Swiper from 'swiper'
可能很多组件都用swiper插件,所以在入口文件main.js引入1次即可。
main.js
// 引入swiper样式
import "swiper/css/swiper.css"
轮播图页面结构要有:
pages/Home/ListContainer/index.vue
<div class="swiper-container" id="mySwiper">
<div class="swiper-wrapper">
<div
class="swiper-slide"
v-for="(carousel, index) in bannerList"
:key="carousel.id"
>
<img :src="carousel.imgUrl" />
</div>
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>
<!-- 如果需要导航按钮 -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
</div>
</div>
mounted() {
// 派发action: 通过Vuex发起ajax请求,将数据仓储在仓库当中
this.$store.dispatch("getBannerList");
// 发起ajax请求是异步操作,数据还没有修改完成,导致组件结构不完整,就new Swiper,会出错,所以应加定时器,一段时间后再new Swiper
// 放在updated()中即数据更新后触发,但如果其他数据更新,则会造成多次new Swiper,此方法不行。
// 放在mounted()中并加定时器也不是最好的办法,下节会有好办法。
setTimeout(() => {
// 使用Swiper插件,初始化swiper
var mySwiper = new Swiper(".swiper-container", {
autoplay: true, // 自动切换
loop: true, // 循环模式选项
// 如果需要分页器
pagination: {
el: ".swiper-pagination",
// 点击圆点切换图片
clickable: true,
},
// 如果需要前进后退按钮
navigation: {
prevEl: ".swiper-button-prev",
nextEl: ".swiper-button-next",
},
});
}, 1000);
},
computed: {
...mapState({
bannerList: (state) => state.home.bannerList,
}),
},
3、轮播图通过 watch+nectTick 解决问题
Vue.nextTick( [callback, context] )
-
参数:
{Function} [callback]
{Object} [context]
-
用法:
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
// watch监听数据的变化
watch: {
bannerList: {
handler(newValue, oldValue) {
// nextTick:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
this.$nextTick(() => {
// 当执行这个回调时,服务器数据已经回来,v-for循环执行完毕了,轮播图结构渲染完成
// 使用Swiper插件
var mySwiper = new Swiper(".swiper-container", {
autoplay: true, // 自动切换
loop: true, // 循环模式选项
// 如果需要分页器
pagination: {
el: ".swiper-pagination",
// 点击圆点切换图片
clickable: true,
},
// 如果需要前进后退按钮
navigation: {
prevEl: ".swiper-button-prev",
nextEl: ".swiper-button-next",
},
});
});
4、获取Floor组件mock数据
有些步骤略,(和前面组件方式一样,这里就不写了)
需要注意的是:Home组件中有两个Floor组件,(并且mock数据也有两条)需要在Home组件中写v-for,并且,派发action,获取Floor组件数据应写在Home组件中。
Home/index.vue
<!-- 因为Home获取了数据,所以Home组件需要传值(数据)给其子组件Floor,让子组件获取数据并渲染。(即 :list="floor") -->
<Floor v-for="(floor, index) in floorList" :key="floor.id" :list="floor" />
import {mapState} from 'vuex'
export default {
name: "",
components: {
ListContainer,
Recommend,
Rank,
Like,
Floor,
Brand,
},
mounted() {
// 派发action,获取floor组件的数据
this.$store.dispatch("getFloorList");
},
computed: {
...mapState ({
floorList: state => state.home.floorList
})
}
Home/Floor/index.vue
export default {
name: 'Floor',
// 接收父组件传来的数据
props: ['list'],
},
mounted() {
// 使用Swiper插件
// 此处不用watch+nectTick,因为获取此数据未在本组件发起请求,而是在父组件发的,并父组件将数据传给了本组件
var mySwiper = new Swiper(".swiper-container", {
autoplay: true, // 自动切换
loop: true, // 循环模式选项
// 如果需要分页器
pagination: {
el: ".swiper-pagination",
// 点击圆点切换图片
clickable: true,
},
// 如果需要前进后退按钮
navigation: {
prevEl: ".swiper-button-prev",
nextEl: ".swiper-button-next",
},
});
},
.......
<h3 class="fl">{{ list.name }}</h3>
<div class="fr">
<ul class="nav-tabs clearfix">
<li
class="active"
v-for="(navList, index) in list.navList"
:key="index"
>
<a href="#tab1" data-toggle="tab">{{ navList.text }}</a>
</li>
.......
组件通信的方式有哪些?
- props:用于父子组件通信
- 自定义事件:@on @emit 实现子给父传数据通信
- 全局事件总线:$bus 全能
- pubsub-js:vue当中几乎不用 全能
- 插槽
- vuex
5、制作共用组件Carsouel轮播图组件
// watch监听数据的变化
watch: {
list: {
// 立即监听:不管数据有无变化,都会监听,即执行此回调函数
immediate: true,
此写法就可以拆分成共用组件了,不管数据有无变化,都会监听,都可以用同样的代码。
main.js 引入共用组件Carousel
import Carsousel from "@/components/Carousel"
Vue.component(Carsousel.name, Carsousel)
创建components/Carousel/index.vue
<!-- 轮播图组件 -->
<template>
<div class="swiper-container" ref="cur">
<div class="swiper-wrapper">
<div
class="swiper-slide"
v-for="(carousel, index) in list"
:key="carousel.id"
>
<img :src="carousel.imgUrl" />
</div>
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>
<!-- 如果需要导航按钮 -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
</div>
</template>
<script>
// 引入swiper插件
import Swiper from "swiper";
export default {
name: "Carousel",
// 接收父组件传来的数据
props: ["list"],
mounted() {
// console.log(this.list);
},
// watch监听数据的变化
watch: {
list: {
// 立即监听:不管数据有无变化,都会监听,即执行此回调函数
immediate: true,
handler() {
// nextTick:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
this.$nextTick(() => {
// 当执行这个回调时,服务器数据已经回来,v-for循环执行完毕了,轮播图结构渲染完成
// 使用Swiper插件
// this.$refs.cur是dom元素(轮播图大容器),swiper规定此处可以是"选择器" 或 dom元素
var mySwiper = new Swiper(this.$refs.cur, {
autoplay: true, // 自动切换
loop: true, // 循环模式选项
// 如果需要分页器
pagination: {
el: ".swiper-pagination",
// 点击圆点切换图片
clickable: true,
},
// 如果需要前进后退按钮
navigation: {
prevEl: ".swiper-button-prev",
nextEl: ".swiper-button-next",
},
});
});
},
},
},
};
</script>
使用Carousel组件并给Carousel组件传数据:
ListContainer/index.vue
<!--banner轮播-->
<Carousel :list="bannerList" />
Floor/index.vue
<!-- 轮播图组件 -->
<Carousel :list="list.carouselList" />
七、Search模块
1、Search模块vuex操作
api/index.js
// 获取搜索模块数据
// reqGetSearchInfo函数在获取服务器数据时,至少传递一个参数(空对象)
export const reqGetSearchInfo = (params) => requests({
url: "/list",
method: "post",
data: params
})
2、动态展示产品列表
store/search/index.js
// getters:计算属性,用于简化仓库数据,让组件获取仓库的数据更方便
const getters = {
goodsList(state) {
return state.searchList.goodsList
}
}
Search/index.vue
<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>
import { mapGetters } from "vuex";
......
computed: {
...mapGetters(["goodsList"]),
},
3、根据不同的参数 获取对应数据 进行展示
从首页三级菜单或搜索框中传参,至服务器返回对应商品数据。
Search/index.vue
data() {
return {
// 带给服务器的参数
searchParams: {
// 一级分类的id
category1Id: "",
// 二级分类的id
category2Id: "",
// 三级分类的id
category3Id: "",
// 分类名字
categoryName: "",
// 关键字
keyword: "",
// 排序
order: "",
// 分页器用的:代表当前是第几页
pageNo: 1,
// 代表每一页展示的数据个数
pageSize: 3,
// 平台售卖属性操作带的参数
props: [],
// 品牌
trademark: "",
},
};
},
// 当组件挂载完毕之前执行一次(mounted之前)
beforeMount() {
// 整合要传递的参数
// 复杂的写法:
// this.searchParams.category1Id = this.$route.query.category1Id
// this.searchParams.category2Id = this.$route.query.category2Id
// this.searchParams.keyword = this.$route.params.keyword
// 简单的写法:用Object.assign:es6新增语法,可以合并对象
// Object.assign(target, ...sources) sources是源对象。target是目标对象。返回值是目标对象。如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖。
Object.assign(this.searchParams, this.$route.query, this.$route.params);
},
// 当组件挂载完毕后执行一次
mounted() {
this.getData();
},
methods: {
// 向服务器发请求获取search模块数据(根据参数不同返回不同的数据进行展示)
getData() {
this.$store.dispatch("getSearchList", this.searchParams);
},
},
4、子组件SearchSelector售卖属性的动态开发
Search/SearchSelector/index.vue
<ul class="logo-list">
<li v-for="(trademark,index) in trademarkList" :key="trademark.tmId">{{trademark.tmName}}</li>
</ul>
......
import { mapGetters } from 'vuex'
export default {
name: 'SearchSelector',
computed: {
...mapGetters(['trademarkList','attrsList'])
}
5、监听路由变化再次发请求获取数据
点击三级分类后,跳至搜索页面,再在搜索框搜索或点三级分类,根据相应参数请求对应数据。可以监听路由变化,就可以再次发请求了。
Search/index.vue
// 当组件挂载完毕之前执行一次(mounted之前)
beforeMount() {
// 传递参数
// 复杂的写法:
// this.searchParams.category1Id = this.$route.query.category1Id
// this.searchParams.category2Id = this.$route.query.category2Id
// this.searchParams.keyword = this.$route.params.keyword
// 简单的写法:用Object.assign:es6新增语法,可以合并对象
// Object.assign(target, ...sources) target目标对象。sources源对象。返回值是目标对象。
Object.assign(this.searchParams, this.$route.query, this.$route.params);
},
// 当组件挂载完毕后执行一次
mounted() {
this.getData();
},
methods: {
// 向服务器发请求获取search模块数据(根据参数不同返回不同的数据进行展示)
getData() {
this.$store.dispatch("getSearchList", this.searchParams);
},
},
computed: {
...mapGetters(["goodsList"]),
},
watch: {
// 监听$route,如果路由信息变化,则会向服务器发请求获取当前参数对应的数据
$route(newValue, oldValue) {
Object.assign(this.searchParams, this.$route.query, this.$route.params);
// 向服务器发请求
this.getData();
// 每次发请求后,应把三级分类的id置空,以便接收下次
this.searchParams.category1Id = ''
this.searchParams.category2Id = ''
this.searchParams.category3Id = ''
},
},
};
6、面包屑
Search/index.vue
......
<ul class="fl sui-tag">
<!-- 三级分类的面包屑 -->
<li class="with-x" v-if="searchParams.categoryName">
{{ searchParams.categoryName
}}<i @click="removeCategoryName">x</i>
</li>
<!-- 关键字(搜索框输入的关键字)的面包屑 -->
<li class="with-x" v-if="searchParams.keyword">
{{ searchParams.keyword }}<i @click="removeKeyword">x</i>
</li>
<!-- 品牌的面包屑 -->
<li class="with-x" v-if="searchParams.trademark">
<!-- split 方法用于把一个字符串分割成字符串数组。 -->
{{ searchParams.trademark.split(":")[1] }}<i @click="removeTrademark">x</i>
</li>
</ul>
</div>
<!--selector-->
<!-- 自定义事件传参 -->
<SearchSelector @trademarkInfo="trademarkInfo" />
// 点击三级分类的面包屑的x后删除分类名字
removeCategoryName() {
// 因为带给服务器的参数都可以是空,当参数值是空时还是会把参数带给服务器会降低性能,设置为undefined时该参数不会带给服务器
this.searchParams.categoryName = undefined;
this.searchParams.category1Id = undefined;
this.searchParams.category2Id = undefined;
this.searchParams.category3Id = undefined;
// 需要展示剩余参数对应的数据,向服务器发请求
this.getData();
// 地址栏也需要修改,即地址栏去掉query参数(三级分类)
if (this.$route.params) {
this.$router.push({
name: "search",
params: this.$route.params,
});
}
},
// 点击关键字(搜索框)的面包屑的x后删除关键字对应的面包屑
removeKeyword() {
this.searchParams.keyword = undefined;
// 需要展示剩余参数对应的数据,向服务器发请求
this.getData();
// 通知兄弟组件Header清除关键字
this.$bus.$emit("clear");
// 地址栏也需要修改,即地址栏去掉params参数,即关键字
if (this.$route.query) {
this.$router.push({ name: "search", query: this.$route.query });
}
},
// 自定义事件回调
// 当点击品牌,展示该品牌数据
trademarkInfo(trademark) {
// 接收子组件SearchSelector传来的品牌信息数据
this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}`
// 再次发请求
this.getData()
},
// 点击品牌的面包屑的x后删除品牌对应的面包屑
removeTrademark() {
this.searchParams.trademark = undefined
this.getData()
}
}
Header/index.vue
mounted() {
// 通过全局事件总线,当面包屑所在组件触发clear事件,则Header组件清除关键字
this.$bus.$on("clear", () => {
this.keyword = "";
});
},
Search/SearchSelector.vue
<li
v-for="(trademark, index) in trademarkList"
:key="trademark.tmId"
@click="tradeMatkHanler(trademark)"
>
{{ trademark.tmName }}
</li>
......
methods: {
// 品牌
tradeMatkHanler(trademark) {
// 点击品牌后,需整理参数,向服务器发请求获取相应数据进行展示
// 因为父组件中数据searchParams参数是带给服务器的参数,所以子组件应把点击的品牌信息,给父组件传过去
// 用自定义事件传参
this.$emit("trademarkInfo", trademark);
},
search路由配置时一定要设置,path上加个 ? 号,代表可传params参数也可不传;若不加 ? ,则URL会出现问题。
router/routes.js
{
name: "search",
// 配置路由时,path上加个 ? 号,代表可传params参数也可不传;若不加 ? ,则URL会出现问题。
path: "/search/:keyword?",
component: () => import('@/pages/Search'),
meta: {
show: true
},
7、平台售卖属性的面包屑
Search/index.vue
<!-- 平台售卖属性的面包屑 -->
<li
class="with-x"
v-for="(attrValue, index) in searchParams.props"
:key="index"
>
<!-- split 方法用于把一个字符串分割成字符串数组。 -->
{{ attrValue.split(":")[1] }}<i @click="removeAttr(index)">x</i>
</li>
</ul>
</div>
<!--selector-->
<!-- 自定义事件传参 -->
<SearchSelector @trademarkInfo="trademarkInfo" @attrInfo="attrInfo" />
// 自定义事件回调
// 当点击平台售卖属性,展示该属性数据
attrInfo(attr, attrValue) {
// 接收子组件SearchSelector传来的平台售卖属性数据
// 根据API文档参数格式:["属性ID:属性值:属性名"]
let props = `${attr.attrId}:${attrValue}:${attr.attrName}`;
// 数组去重
if (this.searchParams.props.indexOf(props) == -1) {
// 若数组没有重复,则追加到props数组
this.searchParams.props.push(props);
}
// 再次发请求
this.getData();
},
// 点击平台售卖属性的面包屑的x后删除对应的面包屑
removeAttr(index) {
this.searchParams.props.splice(index,1);
this.getData();
},
Search/SearchSelector.vue
<!-- 平台售卖属性 -->
<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">
<!-- 属性值 -->
<li
v-for="(attrValue, index) in attr.attrValueList"
:key="attrValue"
@click="attrInfo(attr, attrValue)"
>
......
// 售卖属性值的点击事件
attrInfo(attr, attrValue) {
this.$emit("attrInfo", attr, attrValue)
},
8、排序操作
- 综合排序和价格排序
Search/index.vue
searchParams: {
......
// 排序:初始状态为综合:降序
order: "1:desc",
......
},
};
<ul class="sui-nav">
<!-- 若order中有1则加上类名active isOne在计算属性中-->
<li :class="{active:isOne}">
<a>综合</a>
</li>
<li :class="{active:isTwo}">
<a>价格</a>
</li>
</ul>
......
computed: {
isOne() {
return this.searchParams.order.indexOf("1") != -1;
},
isTwo() {
return this.searchParams.order.indexOf("2") != -1;
},
- 升序降序
用阿里图标,生成链接,//at.alicdn.com/t/font_xxxxxx.css,前面加https:
将此链接加入index.html,然后vue文件用对应类名即可引用 如iconfont icon-down
<li :class="{ active: isOne }">
<a
>综合<span
v-show="isOne"
class="iconfont"
:class="{ 'icon-up': isAsc, 'icon-down': isDesc }"
></span
></a>
</li>
<li :class="{ active: isTwo }">
<a
>价格<span
v-show="isTwo"
class="iconfont"
:class="{ 'icon-up': isAsc, 'icon-down': isDesc }"
></span
></a>
</li>
......
......
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;
},
-
点击
点击价格 价格排序,再点击价格,降序变升序,再点价格,升序变降序
<!-- 排序结构 --> <ul class="sui-nav"> <!-- 若order中有1则加上类名active isOne在计算属性中--> <!-- changeOrder(1)传1代表点的是综合 --> <li :class="{ active: isOne }" @click="changeOrder(1)"> <a >综合<span v-show="isOne" class="iconfont" :class="{ 'icon-up': isAsc, 'icon-down': isDesc }" ></span ></a> </li> <li :class="{ active: isTwo }" @click="changeOrder(2)"> <a >价格<span v-show="isTwo" class="iconfont" :class="{ 'icon-up': isAsc, 'icon-down': isDesc }" ></span ></a> ...... changeOrder(flag) { // flag是形参,传来1|2代表点击的是综合|价格 // 获取起始的排序 let originOrder = this.searchParams.order; // 获取起始的综合|价格 let originFlag = this.searchParams.order.split(":")[0]; // 获取起始的升序|降序 let originSort = this.searchParams.order.split(":")[1]; // 准备一个新的order属性值 let newOrder = ''; if (flag == originFlag) { // 如果flag相同,则将升序变为降序,降序变为升序 newOrder = `${originFlag}:${originSort=="desc"?"asc":"desc"}`; } else { newOrder = `${flag}:${'desc'}`; } // 将新order给searchParams this.searchParams.order = newOrder; // 发请求获取当前排序设置的商品数据 this.getData(); },
八、分页器
是个全局组件,很多组件都用到分页器。
略,时间紧迫
九、Detail商品详情组件
现将静态组件放 pages中,
routes.js
import Detail from '@/pages/Detail'
export default [
......
{
path: "/home",
component: Home,
meta: { show: true }
},
{
path: "/detail/:skuid",
component: Detail,
meta: { show: true }
},
Search/index.vue
<!-- 商品列表 -->
<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">
<!-- 向detail组件传参,展示对应商品详情 -->
<router-link :to="`/detail/${good.id}`">
<img :src="good.defaultImg" />
</router-link>
</div>
1、滚动行为
使用前端路由,当切换到新路由时,想要页面滚动到顶部,或保持原先的滚动位置,就像重新加载页面那样。vue-router
能做到,让你自定义路由切换时页面如何滚动。
当前项目问题:从搜索路由切换到详情路由,页面处于底部,想要页面处于顶部,就需要一些配置。
router/index.js
export default new VueRouter({
routes,
// 滚动行为
// 当前项目问题:从搜索路由切换到详情路由,页面处于底部,想要页面处于顶部,就需要一些配置。
scrollBehavior (to, from, savedPosition) {
// 从搜索路由切换到详情路由,滚动条在最上方,即页面在顶部
return { y: 0 }
}
})
2、产品详情获取
api/index.js
// 获取Detail组件商品详情数据
export const reqGoodsInfo = (skuId) => requests({
url: `/item/${skuId}`,
method: "get"
})
store/detail.js
// detail 模块的小仓库
import {
reqGoodsInfo
} from "@/api"
// state:仓库存储数据的地方
const state = {
goodInfo: {},
}
// mutations:修改state的唯一手段
const mutations = {
GETGOODINFO(state, goodInfo) {
state.goodInfo = goodInfo
}
}
// actions:处理action,书写自己的业务逻辑、也可以处理异步
const actions = {
// 获取detail模块数据
async getGoodInfo({
commit
}, skuId) {
// 向服务器发请求
let result = await reqGoodsInfo(skuId)
if (result.code == 200) {
commit("GETGOODINFO", result.data)
}
}
}
// getters:计算属性,用于简化仓库数据,让组件获取仓库的数据更方便
const getters = {
}
// 对外暴露
export default {
state,
mutations,
actions,
getters,
}
store/index.js
import detail from './detail.js'
export default new Vuex.Store({
modules: {
.....
detail
}
})
3、产品详情展示动态数据
store/detail.js
// getters:计算属性,用于简化仓库数据,让组件获取仓库的数据更方便
const getters = {
categoryView(state) {
// 需要加上 || {},否则若没请求到数据,则会报错
return state.goodInfo.categoryView || {};
},
skuInfo(state) {
return state.goodInfo.skuInfo || {};
}
}
Detail/index.vue
import { mapGetters } from "vuex";
......
computed: {
...mapGetters(["categoryView", "skuInfo"]),
},
展示略
4、产品售卖属性值的排他操作(即切换高亮)
实现:选择某属性值,其高亮,其余属性值不高亮。
Detail/index.vue
<!-- 选择商品售卖属性 -->
<div class="choose">
<div class="chooseArea">
<div class="choosed"></div>
<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>
......
methods: {
// 点击产品售卖属性切换高亮
changeActive(saleAttrValue, arr) {
// 先将所有售卖属性值取消高亮
arr.forEach(item => {
item.isChecked = 0
})
// 将点击的售卖属性值设为高亮
saleAttrValue.isChecked = 1
}
}
5、放大镜
Detail/index.vue
<!--放大镜效果-->
<!-- 给子组件传值 -->
<!-- skuImageList为计算属性 -->
<Zoom :skuImageList="skuImageList" />
<!-- 小图列表 -->
<ImageList :skuImageList="skuImageList" />
两个组件,一个Zoom组件,一张图。一个ImageList组件,是小图商品图片列表。
ImageList组件中有轮播效果,且每次展示3张图(swiper配置即可实现)。左右点击图片切换。
- 实现点击哪个图片,哪个图片有边框
Detail/ImageList/ImageList.vue
......
<div
class="swiper-slide"
v-for="(slide, index) in skuImageList"
:key="slide.id"
>
<!-- 点击哪个图片,哪个图片有边框 -->
<img :src="slide.imgUrl" :class="{active:currentIndex==index}" @click="changecurrentIndex(index)" />
</div>
......
data() {
return {
currentIndex: "",
}
},
methods: {
changecurrentIndex(index) {
this.currentIndex = index;
}
}
- 实现点击列表某小图显示对应大图
Detail/ImageList/ImageList.vue
methods: {
changecurrentIndex(index) {
// 当点击触发,修改响应式数据currentIndex为当前index
this.currentIndex = index;
// 通知兄弟组件Zoom,传当前索引值
this.$bus.$emit('getIndex', this.currentIndex)
}
}
Detail/Zoom/Zoom.vue
name: "Zoom",
data() {
return {
currentIndex: 0,
}
},
props: ["skuImageList"],
computed: {
imgObj() {
return this.skuImageList[this.currentIndex] || {}
}
},
mounted() {
// 全局事件总线,获取兄弟组件传递过来的索引值
this.$bus.$on('getIndex', (index) => {
this.currentIndex = index;
})
}
-
放大镜效果
Detail/Zoom/Zoom.vue
<template> <div class="spec-preview"> <!-- 原图 --> <img :src="imgObj.imgUrl" /> <!-- 遮罩在此移动 --> <div class="event" @mousemove="handler"></div> <!-- 放大的图 --> <div class="big"> <img :src="imgObj.imgUrl" ref="big" /> </div> <!-- 遮罩,即那个移动的小方块 --> <div class="mask" ref="mask"></div> </div> </template>
methods: { // 鼠标移动触发 handler(event) { let mask = this.$refs.mask; let big = this.$refs.big; // 获取mask应距盒子左边的距离 = 鼠标距盒子左边的距离 - mask宽度的一半 let left = event.offsetX - mask.offsetWidth / 2; // 获取mask应距盒子顶部的距离 let top = event.offsetY - mask.offsetHeight / 2; // 约束范围,防止mask移出盒子 if (left <= 0) left = 0; if (left >= mask.offsetWidth) left = mask.offsetWidth; if (top <= 0) top = 0; if (top >= mask.offsetHeight) top = mask.offsetHeight; // 修改mask的left和top属性值 mask.style.left = left + "px"; mask.style.top = top + "px"; // 对应修改大图的位置 (大图是原图的2倍) big.style.left = -2 * left + "px"; big.style.top = -2 * top + "px"; }, }, };
6、购买产品个数的操作
Detail/index.vue
<!-- change文本框发生变化触发 -->
<input
autocomplete="off"
class="itxt"
v-model="skuNum"
@change="changeSkuNum"
/>
<a href="javascript:" class="plus" @click="skuNum++">+</a>
<a
href="javascript:"
class="mins"
@click="skuNum > 1 ? skuNum-- : (skuNum = 1)"
>-</a
>
// 表单元素修改产品个数
changeSkuNum(event) {
// 用户输入进来的文本 event.target.value 获取当前文本框的值(由事件触发时)
let value = event.target.value;
// 如果用户输入的是非数字,或<1
if (isNaN(value) || value < 1) {
this.skuNum = 1;
} else {
// 为整数,不能是小数
this.skuNum = parseInt(value);
}
},
7、添加到购物车
请求地址
/api/cart/addToCart/{ skuId }/{ skuNum }
请求方式
POST
参数类型
参数名称 | 类型 | 是否必选 | 描述 |
---|---|---|---|
skuID | string | Y | 商品ID |
skuNum | string | Y | 商品数量、正数代表增加、负数代表减少 |
api/index.js
// 添加到购物车
export const reqAddOrUpdateShopCart = (skuId,skuNum) =>
requests({
url: `/cart/addToCart/${ skuId }/${ skuNum }`,
method: "post",
})
store/detail.js
// 添加到购物车
async addOrUpdateShopCart({
commit
}, {skuId, skuNum}) {
// 向服务器发请求,服务器写入数据成功,无数据返回,仓库无需存储数据
let result = await reqAddOrUpdateShopCart(skuId, skuNum)
// 200代表服务器写入数据成功
if (result.code == 200) {
return "ok"
} else {
return Promise.reject(new Error('faile'))
}
}
Detail/index.vue
<div class="add">
<a @click="addshopcar">加入购物车</a>
// 添加到购物车
async addshopcar() {
// 把购买商品的信息通过请求的方式通知服务器,服务器进行相应的存储
// try...catch语句标记要尝试的语句块,并指定一个出现异常时抛出的响应。
try {
await this.$store.dispatch("addOrUpdateShopCart", {
skuId: this.$route.params.skuid,
skuNum: this.skuNum,
});
// 若成功,路由跳转,并将产品信息带给 添加到购物车成功路由组件
console.log('略,看下节')
} catch (error) {
alert(error.message);
}
},
十、添加到购物车成功 路由组件
创建pages/AddCartSuccess/index.vue
router/routes.js
import AddCartSuccess from '@/pages/AddCartSuccess'
......
{
path: "/detail/:skuid",
component: Detail,
meta: { show: true }
},
{
name: "addcartsuccess",
path: "/addcartsuccess",
component: AddCartSuccess,
meta: { show: true }
}
1、路由传递参数(结合会话存储)
将Detail组件中的产品信息带给 添加到购物车成功AddCartSuccess路由组件。
(因Detail组件已有数据,无需再在AddCartSuccess组件中重新发请求获取数据。所以用到会话存储。)
一些简单的数据,通过query给路由组件传递,复杂的数据(如对象)通过会话存储。
H5新增的浏览器存储功能:本地存储localStorage和会话存储sessionStorage。
浏览器存储有哪些方法呢?主要有cookie、localStorage、sessionStorage。
cookie属于文档对象模型DOM树根节点document,而 sessionStorage 和 localStorage 属于浏览器对象模型BOM的对象window。
cookie: h5之前,存储主要用cookies,缺点是在请求头上带着数据,导致流量增加。大小限制4k
。过期时间,当过了到期日期时,浏览器会自动删除该cookie,如果想删除一个cookie,只需要把它过期时间设置成过去的时间即可。如果不设置过期时间,则表示这个cookie生命周期为浏览器会话期间,只要关闭浏览器窗口,cookie就消失了。
其中 sessionStorage 和 localStorage 是 HTML5 Web Storage API 提供的
- sessionStorage:会话存储。为每一个给定的源(given origin)维持一个独立的存储区域,该存储区域在页面会话期间可用(即只要浏览器处于打开状态,包括页面重新加载和恢复),但是浏览器关闭,数据消失。
- localStorage:本地存储。在浏览器关闭,然后重新打开后数据仍然存在。以键值对(Key-Value)的方式存储,永久存储,永不失效,除非手动删除。IE8+支持,每个域名限制
5M
。打开同域的新页面也能访问得到。
sessionStorage、localStorage 可以存储数组、数字、对象等可以被序列化为字符串的内容。
Detail/index.vue
// 若成功,路由跳转,并将产品信息带给 添加到购物车成功路由组件
// 会话存储,一些简单的数据,通过query给路由组件传递,复杂的数据(如对象)通过会话存储。
// 因为sessionStorage只存储字符串,所以JSON.stringify()把js对象转换为字符串
sessionStorage.setItem("SKUINFO", JSON.stringify(this.skuInfo));
this.$router.push({
name: "addcartsuccess",
query: { skuNum: this.skuNum },
});
pages/AddCartSuccess/index.vue
<img :src="skuInfo.skuDefaultImg" />
</div>
<div class="right-info">
<p class="title">{{ skuInfo.skuName }}</p>
<p class="attr">
{{ skuInfo.skuDesc }} 数量:{{ $route.query.skuNum }}
</p>
computed: {
skuInfo() {
// JSON.parse()将数据转换为js对象。
return JSON.parse(sessionStorage.getItem("SKUINFO"));
},
},
十一、购物车组件
router/routes.js
import ShopCart from '@/pages/ShopCart'
......
{
name: "shopcart",
path: "/shopcart",
component: ShopCart,
meta: { show: true }
},
pages/AddCartSuccess/index.vue
<div class="right-gocart">
<router-link :to="`/detail/:${skuInfo.id}`" class="sui-btn btn-xlarge">查看商品详情</router-link>
<router-link to="/shopcart">去购物车结算</router-link>
</div>
1、uuid游客身份获取购物车数据
无需安装 uuid,因为有的包依赖uuid,所以uuid已存在。
localStorage:本地存储,存 uuid
创建src/utils文件夹,放功能模块
创建src/utils/uuid_token.js
import {
v4 as uuidv4
} from 'uuid'
// 生成一个随机字符串,且每次执行不能发生变化,游客身份持久
export const getUUID = () => {
// 先从本地存储获取uuid游客身份
let uuid_token = localStorage.getItem('UUIDTOKEN');
// 如果本地存储没有uuid
if (!uuid_token) {
// 生成临时游客身份,即生成一个随机字符串
uuid_token = uuidv4();
// 本地存储游客身份
localStorage.setItem('UUIDTOKEN', uuid_token);
}
// 返回游客身份uuid
return uuid_token;
}
store/detail.js
// 引入封装游客身份模块uuid(会封装一个随机字符串)
import {getUUID} from '@/utils/uuid_token';
// state:仓库存储数据的地方
const state = {
goodInfo: {},
// 游客临时身份
uuid_token: getUUID(),
}
在请求拦截器中(项目发请求之前)请求头添加一个字段:游客身份uuid
api/ajax.js
// 引入store
import store from "@/store"
......
// 请求拦截器:在发请求之前,请求拦截器可以检测到,在请求发出之前做一些事情;
requests.interceptors.request.use((config) => {
// config:配置对象,其有一个重要属性:header请求头
// 请求头添加一个字段:userTempId:游客身份uuid,此字段已和后端协商好了。
if (store.state.detail.uuid_token) {
config.headers.userTempId = store.state.detail.uuid_token;
}
// 进度条开始动
nprogress.start();
return config;
}
2、购物车动态展示相应游客的数据
向服务器发请求,获取对应游客身份的购物车列表数据,并展示数据。
store/shopcart.js
// shopcart 模块的小仓库
import {
reqCartList,
} from "@/api"
// state:仓库存储数据的地方
const state = {
cartList: {},
}
// mutations:修改state的唯一手段
const mutations = {
GETCARTLIST(state, cartList) {
state.cartList = cartList
},
}
// actions:处理action,书写自己的业务逻辑、也可以处理异步
const actions = {
// 获取产品信息
async getCartList({
commit
}) {
// 向服务器发请求
let result = await reqCartList()
if (result.code == 200) {
commit("GETCARTLIST", result.data)
}
},
}
// getters:计算属性,用于简化仓库数据,让组件获取仓库的数据更方便
const getters = {
cartList(state) {
// 需要加上 || {},否则若没请求到数据,则会报错
return state.cartList[0] || {};
},
}
// 对外暴露
export default {
state,
mutations,
actions,
getters,
}
pages/ShopCart/index.vue
获取购物车数据:
import { mapGetters } from 'vuex';
export default {
name: "ShopCart",
mounted() {
this.getData();
},
methods: {
getData() {
this.$store.dispatch('getCartList')
}
},
computed: {
...mapGetters(['cartList']),
// 购物车数据
cartInfoList() {
return this.cartList.cartInfoList || []
}
}
展示购物车数据:
<ul
class="cart-list"
v-for="(cart, index) in cartInfoList"
:key="cart.id"
>
<li class="cart-list-con1">
<input
type="checkbox"
name="chk_list"
id=""
value=""
:checked="cart.isChecked == 1"
/>
计算总价:
<em>总价(不含运费) :</em>
<i class="summoney">{{ totalPrice }}</i>
computed: {
...mapGetters(["cartList"]),
// 购物车数据
cartInfoList() {
return this.cartList.cartInfoList || [];
},
// 计算购买产品的总价
totalPrice() {
let sum = 0;
this.cartInfoList.forEach((item) => {
sum += item.skuNum * item.skuPrice;
});
return sum;
},
全选操作:
<div class="select-all">
<input class="chooseAll" type="checkbox" :checked="isAllCheck" />
<span>全选</span>
computed: {
// 判断底部复选框是否勾选(全部商品都选中,才勾选)
isAllCheck() {
// 遍历数组,若全部元素isChecked==1,则返回true,若有1个不等于1,则返回false
return this.cartInfoList.every((item) => item.isChecked == 1);
},
3、修改购物车产品数量
和Detail组件中的添加到购物车接口一样,
对已有物品进行数量改动接口:/api/cart/addToCart/{ skuId }/{ skuNum }
请求方式: POST
参数类型
参数名称 | 类型 | 是否必选 | 描述 |
---|---|---|---|
skuID | string | Y | 商品ID |
skuNum | string | Y | 商品数量 正数代表增加、负数代表减少 |
不同的是:Detail组件商品传入数据库是要买的商品数量。
而ShopCart购物车组件,要增加或减少数据库中已有的商品数量。
pages/ShopCart/index.vue
<a
href="javascript:void(0)"
class="mins"
@click="handler('minus', -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
>
import throttle from "lodash/throttle";
......
// 修改某一个产品的个数后,需要发请求存入数据库
// 参数:type修改操作类型,disNum给服务器的变化量,cart对应产品
// 但是,当用户行为过快,快速点击减-,浏览器反应不过来,会导致数量成负数。所以我们用到了节流。使用lodash插件。
handler: throttle(async function (type, disNum, cart) {
// 判断
switch (type) {
case "add":
disNum = 1;
break;
case "minus":
// 判断产品个数大于1,才能将-1传给服务器
disNum = cart.skuNum > 1 ? -1 : 0;
break;
case "change":
// 如果用户输入的是非数字,或<1
if (isNaN(disNum) || disNum < 1) {
this.disNum = 0;
} else {
// 为整数,不能是小数
this.disNum = parseInt(disNum) - cart.skuNum;
}
break;
}
try {
// 派发action
await this.$store.dispatch("addOrUpdateShopCart", {
skuId: cart.skuId,
skuNum: disNum,
});
// 请求成功,再一次获取服务器最新数据
this.getData();
} catch (error) {}
}, 500),
4、删除购物车某个产品
请求地址 /api/cart/deleteCart/{skuId}
请求方式 DELETE
参数类型
参数名称 | 类型 | 是否必选 | 描述 |
---|---|---|---|
skuId | string | Y | 商品id |
api/index.js
// 删除购物车某个产品
export const reqDeleteCartById = (skuId) => requests({
url: `/cart/deleteCart/${skuId}`,
method: "delete"
})
store/shopcart.js
import {
reqCartList,
reqDeleteCartById,
} from "@/api"
pages/ShopCart/index.vue
<li class="cart-list-con7">
<a class="sindelet" @click="deleteCartById(cart)">删除</a>
// 删除购物车某个产品
async deleteCartById(cart) {
try {
// 发送请求删除某个产品
await this.$store.dispatch("deleteCartListBySkuId", cart.skuId);
// 再次展示新的数据
this.getData();
} catch (error) {
alert(error.message);
}
},
5、修改某个产品选中状态
请求地址: /api/cart/checkCart/{skuID}/{isChecked}
请求方式:GET
参数类型:
参数名称 | 类型 | 是否必选 | 描述 |
---|---|---|---|
skuID | string | Y | 商品ID |
isChecked | string | Y | 商品选中状态 0代表取消选中,1代表选中 |
api/index.js
// 修改某个产品选中状态
export const reqUpdateCheckedById = (skuId,isChecked) => requests({
url: `/cart/checkCart/${skuId}/${isChecked}`,
method: "get"
})
store/shopcart.js
import {
reqCartList,
reqDeleteCartById,
reqUpdateCheckedById,
} from "@/api"
// 修改某个产品选中状态
async updateCheckedById({
commit
}, {
skuId,
isChecked
}) {
let result = await reqUpdateCheckedById(skuId, isChecked);
if (result.code == 200) {
return 'ok'
} else {
return Promise.reject(new Error('faile'));
}
}
pages/ShopCart/index.vue
<input
type="checkbox"
name="chk_list"
:checked="cart.isChecked == 1"
@change="updateChecked(cart, $event)"
/>
// 修改某个产品选中状态
async updateChecked(cart, event) {
try {
let isChecked = event.target.checked ? "1" : "0";
await this.$store.dispatch("updateCheckedById", {
skuId: cart.skuId,
isChecked,
});
// 再次展示数据
this.getData();
} catch (error) {
alert(error.message);
}
},
6、删除全部选中的商品
pages/ShopCart/index.vue
<input class="chooseAll" type="checkbox" :checked="isAllCheck" />
<span>全选</span>
// 删除全部选中的商品
async deleteAllCheckedCart() {
try {
// 派发action
await this.$store.dispatch("deleteAllCheckedCart");
// 再次获取购物车列表数据
this.getData();
} catch (error) {
alert(error.message);
}
},
store/shopcart.js
之前已写过 删除购物车某个产品 的action,删除全部选中商品的action 可以逐个派发删除购物车某个产品 的action。
// 删除购物车某个产品
async deleteCartListBySkuId({
commit
}, skuId) {
// 向服务器发请求
let result = await reqDeleteCartById(skuId);
if (result.code == 200) {
return 'ok'
} else {
return Promise.reject(new Error('faile'));
}
},
// 删除全部选中的商品
deleteAllCheckedCart({
// context:是第一个参数,包括commit【提交mutations修改state】、getters【计算属性】、dispatch【派发action】、state【当前仓库数据】等
dispatch,
getters
}) {
// 获取购物车中全部的产品(是一个数组)
var allCart = getters.cartList.cartInfoList
let PromiseAll = [];
// 遍历,将选中的产品逐个删除
allCart.forEach(item => {
// 若当前产品被选中了,则删除
// 派发了之前的 删除购物车某个产品这个action
let promise = item.isChecked == 1 ? dispatch("deleteCartListBySkuId", item.skuId) : '';
// 将每次返回的Promise添加到数组中
PromiseAll.push(promise);
});
// 只有所有的promise都返回成功,返回结果才成功,若有一个失败,则返回为失败。
return Promise.all(PromiseAll);
}
7、修改全部产品的勾选状态
当选中全选按钮,则每个商品前的按钮都要选中。
pages/ShopCart/index.vue
<!-- cartInfoList.length > 0 商品列表不为空,才可以选中全选按钮 -->
<input
class="chooseAll"
type="checkbox"
:checked="isAllCheck && cartInfoList.length > 0"
@change="updateAllCartChecked"
/>
<span>全选</span>
// 修改全部产品的勾选状态
async updateAllCartChecked(event) {
try {
let isChecked = event.target.checked ? "1" : "0";
// 派发action
await this.$store.dispatch("updateAllCartChecked", isChecked);
// 更新数据
this.getData();
} catch (error) {
alert(error.message);
}
},
store/shopcart.js
之前已写过 修改某个产品选中状态 的action,修改全部产品勾选状态的action 可以逐个派发修改某个产品选中状态 的action。
// 修改全部产品的勾选状态
updateAllCartChecked({
dispatch,
getters,
}, isChecked) {
// 获取购物车中全部的产品(是一个数组)
var allCart = getters.cartList.cartInfoList
let PromiseAll = [];
// 遍历,逐个修改商品的选中状态
allCart.forEach(item => {
// 派发了之前的 修改某个产品选中状态这个action
let promise = dispatch("updateCheckedById", {skuId:item.skuId,isChecked});
// 将每次返回的Promise添加到数组中
PromiseAll.push(promise);
});
// 只有所有的promise都返回成功,返回结果才成功,若有一个失败,则返回为失败。
return Promise.all(PromiseAll);
}
十二、登录注册组件
路由之前已配置好。
router/routes.js
},
{
path: "/login",
component: Login,
meta: { show: false }
},
{
path: "/register",
component: Register,
meta: { show: false }
}
components/Header/index.vue
<router-link to="/login">登录</router-link>
<router-link to="/register" class="register">免费注册</router-link>
1、注册业务
获取注册验证码,返回验证码。但是正常情况,后端将验证码发到用户手机,前端不会接收验证码(但是发验证码有条数限制,超出会付费,所以此接口验证码就不发给用户手机)。
获取注册验证码接口:
请求地址: /api/user/passport/sendCode/{phone}
请求方式:GET
api/index.js
// 获取注册验证码
export const reqGetCode = (phone) => requests({
url: `/user/passport/sendCode/${phone}`,
method: "get"
})
store/index.js
import user from './user.js'
......
export default new Vuex.Store({
modules: {
......
user,
}
})
store/user.js
// 登录和注册的模块
import {
reqGetCode,
} from "@/api"
// state:仓库存储数据的地方
const state = {
code: '',
}
// mutations:修改state的唯一手段
const mutations = {
GETCODE(state, code) {
state.code = code
},
}
// actions:处理action,书写自己的业务逻辑、也可以处理异步
const actions = {
// 获取注册验证码,返回验证码,但是正常情况,后端将验证码发到用户手机,前端不会接收验证码(但是发验证码有条数限制,超出会付费,所以此接口验证码就不发给用户手机)
async getCode({
commit
},phone) {
// 向服务器发请求
let result = await reqGetCode(phone)
if (result.code == 200) {
commit("GETCODE", result.data)
}
},
}
pages/Register/index.vue
<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>
data() {
return {
// 手机号
phone: "",
// 注册验证码
code: "",
};
},
methods: {
// 获取注册验证码
async getCode() {
try {
// 写法是es6的写法,其实就相当于:const phone = this.phone
const { phone } = this;
// 判断phone是否存在,若存在则派发action
phone && (await this.$store.dispatch("getCode", phone));
// 将验证码的输入框值变为获取到的验证码值
this.code = this.$store.state.user.code;
} catch (error) {
alert(error.message)
}
},
-
注册用户接口:
点击完成注册按钮,进行用户注册。后端将用户账号和密码存入数据库。
请求地址: /api/user/passport/register
请求方式: POST
参数类型
参数名称 | 类型 | 是否必选 | 描述 |
---|---|---|---|
phone | string | Y | 注册手机号 |
password | string | Y | 密码 |
code | string | Y | 验证码 |
api/index.js
// 注册用户
export const reqUserRegister = (data) =>
requests({
url: '/user/passport/register',
method: "post",
data
})
store/user.js
import {
reqUserRegister,
} from "@/api"
......
// 注册用户
async userRegister({commit},user) {
// 向服务器发请求,后端将用户账号和密码存入数据库。
let result = await reqUserRegister(user)
if (result.code == 200) {
return 'ok'
} else {
return Promise.reject(new Error('faile'));
}
}
pages/Register/index.vue
<div class="btn" @click="userRegister">
<button>完成注册</button>
</div>
// 点击完成注册按钮
async userRegister() {
try {
// const {xxx} = this es6语法,相当于const xxx = this.xxx
const { phone, code, password, password1 } = this;
// 当phone等数据存在再派发action
(phone && code && password == password1) && await this.$store.dispatch('userRegister', { phone, code, password });
// 注册成功,跳转至登录页面
this.$router.push('/login');
} catch(error) {
alert(error.message);
}
},
2、登录业务(token)
将用户名和密码发给服务器,判断数据库中是否有,有则登录成功。
请求地址: /api/user/passport/login
请求方式:POST
参数类型
参数名称 | 类型 | 是否必选 | 描述 |
---|---|---|---|
phone | string | Y | 用户名 |
password | string | Y | 密码 |
api/index.js
// 用户登录
export const reqUserLogin = (data) =>
requests({
url: '/user/passport/login',
method: "post",
data
})
token 在计算机身份认证中是令牌的意思,在词法分析中是标记的意思。一般作为邀请、登录系统使用。
登录成功后,服务器返回的数据中,有token: "d20386b3c2554014931a5d124733185f"
客户端需要持久存储 token,客户端每次向服务端请求资源的时候需要带着服务端签发的 token。
注意:在vuex中不能持久存token,因为在登录页面跳到首页,若在首页刷新后,并没有派发用户登录action,vuex中将无token。所以用localStorage本地存储。
store/user.js
// 登录和注册的模块
import {
......
reqUserLogin,
} from "@/api"
......
const state = {
......
token: localStorage.getItem('token'),
}
// mutations:修改state的唯一手段
const mutations = {
......
USERLOGIN(state, token) {
state.token = token;
}
}
const actions = {
.....
// 用户登录
async userLogin({
commit
}, user) {
// 向服务器发请求
let result = await reqUserLogin(user);
// 服务器下发token,用户唯一标识符。
if (result.code == 200) {
// 客户端需要持久存储 token,以后客户端每次向服务端请求资源的时候需要带着服务端签发的 token。
commit("USERLOGIN", result.data.token);
// 持久化存储token,存储到本地
localStorage.setItem("TOKEN", result.data.token);
return 'ok'
} else {
return Promise.reject(new Error('faile'));
}
}
pages/Login/index.vue
<!-- .prevent阻止原生dom的默认行为 -->
<button class="btn" @click.prevent="userLogin">登 录</button>
</form>
data() {
return {
phone: "",
password: "",
};
},
methods: {
// 用户登录
async userLogin() {
try {
const { phone, password } = this;
phone && password && (await this.$store.dispatch("userLogin", { phone, password }));
// 登录成功,跳转到Home路由组件
this.$router.push("/home");
} catch (error) {
alert(error.message);
}
},
3、用户登录后携带token获取用户信息
在请求头携带token
api/ajax.js
// 请求拦截器:在发请求之前,请求拦截器可以检测到,在请求发出之前做一些事情;
requests.interceptors.request.use((config) => {
// config:配置对象,其有一个重要属性:header请求头
// 请求头添加一个字段:userTempId:游客身份uuid,此字段已和后端协商好了。
// if (store.state.detail.uuid_token) {
// 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;
})
api/index.js
// 获取用户信息
export const reqUserInfo = () => requests({
url: '/user/passport/auth/getUserInfo',
method: "get"
})
utils/token.js
// 本地存储token
export const setToken = (token) => {
localStorage.setItem("TOKEN", token);
}
// 获取本地token
export const getToken = () => {
return localStorage.getItem("TOKEN");
}
// 清除本地存储token
export const removeToken = () => {
localStorage.removeItem("TOKEN");
}
store/user.js
import {
......
reqUserInfo,
} from "@/api"
const state = {
code: '',
token: getToken(),
userInfo: {},
}
// mutations:修改state的唯一手段
const mutations = {
......
USERINFO(state, userInfo) {
state.userInfo = userInfo;
}
}
const actions = {
......
// 获取用户信息
async userInfo({
commit
}) {
// 向服务器发请求
let result = await reqUserInfo();
if (result.code == 200) {
commit("USERINFO", result.data);
return 'ok';
}
}
从登录页面登录后进入首页Home,Home组件需要发请求获取用户信息存到仓库。
pages/Home/index.vue
mounted() {
......
this.$store.dispatch("getUserInfo");
},
顶部Header组件登录注册按钮变为用户名和退出登录。
components/Header/index.vue
<!-- 无用户名将显示登录注册按钮 -->
<p v-if="!userName">
<span>请</span>
<router-link to="/login">登录</router-link>
<router-link to="/register" class="register">免费注册</router-link>
</p>
<!-- 有用户将显示用户名 -->
<p v-else>
<a>{{ userName }}</a>
<a class="register">退出登录</a>
</p>
computed: {
// 用户的名字
userName() {
return this.$store.state.user.userInfo.name;
},
但是此业务存在问题:若很多组件如Search等都需要获取用户信息,则需要写很多获取用户信息相关代码,比较繁琐。下面几节再优化。
4、退出登录
api/index.js
// 退出登录
export const reqLogout = () => requests({
url: '/user/passport/logout',
method: "get"
})
store/user.js
const mutations = {
// 清除本地和仓库的token和用户信息
CLEAR(state) {
state.token = '';
state.userInfo = {};
removeToken();
}
const actions = {
// 退出登录
async userLogout({commit}){
// 向服务器发请求,通知服务器清除token
let result = await reqLogout();
if (result.code == 200) {
// 提交给mutations清除本地和仓库的token和用户信息
commit("CLEAR");
return 'ok';
} else {
return Promise.reject(new Error('faile'));
}
},
components/Header/index.vue
// 退出登录
// 通知服务器清除token,并清除本地和仓库的token和用户信息 并返回首页
async logout() {
try {
await this.$store.dispatch("userLogout");
this.$router.push("/home");
} catch (error) {
alert(error.message);
}
},
5、导航守卫,优化登录业务
用户已经登录,就不能回到 /login页面等。
路由守卫知识笔记在在notion笔记—vue知识补充–路由中,此处就不解释了。
router/routes.js
let router = new VueRouter({
......
})
// 全局路由守卫,前置守卫(初始化时被调用,每次路由切换之前调用)
router.beforeEach(async (to, from, next) => {
// to:即将要进入的目标路由 from:当前导航正要离开的路由 next放行函数
let token = store.state.user.token;
// 用户信息
let name = store.state.user.userInfo.name;
// 当token存在,即用户登录了
if (token) {
// 当地址栏输入/login 用户登陆了,就不能再去登录页面,只能去首页
if (to.path == '/login') {
next('/home')
} else {
// 不是去/login,放行
if (name) {
// 如果用户信息存在,则放行
next();
} else {
// 用户信息不存在,派发action获取用户信息后再放行
try {
await store.dispatch("getUserInfo")
next();
} catch (error) {
// 如果不能获取用户信息,说明token失效,此时需要清除token,重新登录
await store.dispatch("userLogout")
next('/login');
}
}
}
} else {
// 未登录则放行,此处还有其他逻辑 后期再处理
next();
}
})
export default router;
十三、Trade交易组件
router/routes.js
import Trade from '@/pages/Trade'
export default [
......
{
name: "trade",
path: "/trade",
component: Trade,
meta: { show: true }
},
点击购物车的结算按钮跳至交易组件
shopCart/index.vue
<router-link to="/trade" class="sum-btn">结算</router-link>
1、获取用户地址信息和商品清单
api/index.js
// 获取用户地址信息
export const reqAddressInfo = () => requests({
url: '/user/userAddress/auth/findUserAddressList',
method: "get"
})
// 获取交易页信息(商品清单)
export const reqOrderInfo = () => requests({
url: '/order/auth/trade',
method: "get"
})
(最好使用视频上的账号密码,因为该账号数据库存有地址信息 。 13700000000 密:111111)
store/trade.js
import {
reqAddressInfo,
reqOrderInfo,
} from "@/api"
const state = {
address: "",
orderInfo: "",
}
const mutations = {
GETUSERADDRESS(state, address) {
state.address = address;
},
GETORDERINFO(state, orderInfo) {
state.orderInfo = orderInfo;
}
}
const actions = {
// 获取用户地址信息
async getUserAddress({
commit
}) {
// 向服务器发请求
let result = await reqAddressInfo()
if (result.code == 200) {
commit("GETUSERADDRESS", result.data)
}
},
// 获取交易页信息(商品清单)
async getOrderInfo({
commit
}) {
// 向服务器发请求
let result = await reqOrderInfo()
if (result.code == 200) {
commit("GETORDERINFO", result.data)
}
},
pages/Trade/index.vue
mounted() {
// 获取用户地址信息
this.$store.dispatch("getUserAddress");
// 获取交易页信息(商品清单)
this.$store.dispatch("getOrderInfo");
},
2、展示用户地址数据
pages/Trade/index.vue
展示用户地址数据,并实现选择地址,排他切换效果。
<h5 class="receive">收件人信息</h5>
<div
class="address clearFix"
v-for="(address, index) in addressInfo"
:key="address.id"
>
<span class="username" :class="{ selected: address.isDefault == 1 }">{{
address.consignee
}}</span>
<!-- 选择地址,排他切换 -->
<p @click="changeDefault(address, addressInfo)">
<span class="s1">{{ address.fullAddress }}</span>
<span class="s2">{{ address.phoneNum }}</span>
<span class="s3" v-show="address.isDefault == 1">默认地址</span>
</p>
</div>
computed: {
...mapState({
// 用户地址信息
addressInfo: (state) => state.trade.address,
}),
},
methods: {
// 地址选中,排他切换
changeDefault(address, addressInfo) {
addressInfo.forEach((item) => (item.isDefault = 0));
address.isDefault = 1;
},
},
交易页下方还需要展示交易最终选中的地址:
// 交易最终选中的地址
userDefaultAddress() {
// find()查找并返回数组中第一个符合条件的元素
return this.addressInfo.find((item) => item.isDefault == 1);
},
<div class="trade">
<div class="price">应付金额: <span>¥5399.00</span></div>
<div class="receiveInfo">
寄送至:
<span>{{ userDefaultAddress.fullAddress }}</span>
收货人:<span>{{ userDefaultAddress.consignee }}</span>
<span>{{ userDefaultAddress.phoneNum }}</span>
</div>
</div>
注意,后端数据改了,需要设置默认地址,否则出错。
mounted() {
// 获取用户地址信息
this.init();
},
computed: {
...mapState({
// 用户地址信息
addressInfo: (state) => state.trade.address,
orderInfo: (state) => state.trade.orderInfo,
}),
// 交易最终选中的地址
userDefaultAddress() {
return this.addressInfo.find((item) => item.isDefault == 1);
}
},
methods: {
// 获取用户地址信息
async init() {
await this.$store.dispatch("getUserAddress");
// 获取交易页信息(商品清单)
await this.$store.dispatch("getOrderInfo");
// 默认选中第一个地址(后端改了,改成了全是0,所以此处要设置默认地址)
this.addressInfo[0].isDefault = 1;
},
3、交易信息展示
<h5>商品清单</h5>
<ul class="list clearFix" v-for="(order,index) in orderInfo.detailArrayList" :key="order.skuId">
<li>
<img :src="order.imgUrl" alt="" width="70px" />
</li>
<li>
<p>
{{order.skuName}}
</p>
<h4>7天无理由退货</h4>
</li>
<li>
<h3>{{order.orderPrice}}</h3>
</li>
<li>X{{order.skuNum}}</li>
......
<h5>买家留言:</h5>
<textarea
placeholder="建议留言前先与商家沟通确认"
class="remarks-cont"
v-model="msg"
></textarea>
......
<li>
<b><i>{{orderInfo.totalNum}}</i>件商品,总商品金额</b>
<span>¥{{orderInfo.totalAmount}}.00</span>
</li>
......
<div class="price">应付金额: <span>¥{{orderInfo.totalAmount}}.00</span></div>
data() {
return {
// 买家留言
msg: "",
}
4、提交订单
当在交易页面提交订单按钮时,跳至支付页面,在跳前应该发请求获取支付页信息。
请求地址: /api/order/auth/submitOrder?tradeNo={tradeNo}
请求方式: POST
参数类型:
参数名称 | 类型 | 是否必选 | 描述 |
---|---|---|---|
traderNo | string | Y | 交易编号(拼接在路径中) |
consignee | string | Y | 收件人姓名 |
consigneeTel | string | Y | 收件人电话 |
deliveryAddress | string | Y | 收件地址 |
paymentWay | string | Y | 支付方式(ONLINE代表在线) |
orderComment | string | Y | 订单备注 |
orderDetailList | Array | Y | 存储多个商品对象的数组 |
api/index.js
// 提交订单
export const reqSubmitOrder = (tradeNo, data) => requests({
url: `/order/auth/submitOrder?tradeNo=${tradeNo}`,
method: "post",
data
})
从这里开始,不使用vuex了,因为需要学会无vuex时,将如何传递数据。(项目不大时,最好不用vuex)
可是每个组件发请求时,要引入api文件中某请求函数比较麻烦,所以用到Vue原型:
main.js
// 引入 统一接口api中,全部请求函数
// 好处是只需引一次,所有组件不用引就可使用。
import * as API from '@api'
new Vue({
render: h => h(App),
beforeCreate() {
// 全局事件总线$bus的配置
Vue.prototype.$bus = this;
// 统一接口api中,全部请求函数。好处是只配置一次,所有组件顺着原型即可使用。
Vue.prototype.$API = API;
},
router,
store,
}).$mount("#app")
提交订单,发请求,传递参数,接收订单号,传给支付页面
pages/Trade/index.vue
<a class="subBtn" @click="submitOrder">提交订单</a>
data() {
return {
// 买家留言
msg: "",
// 订单号
orderId: "",
};
},
......
// 提交订单,路由跳转至支付页面,跳之前向服务器发请求
submitOrder() {
// 参数:交易编码
let { tradeNo } = this.orderInfo;
// 参数:向服务器传用户选择的各种数据
let data = {
consignee: this.userDefaultAddress.consignee, // 最终收件人名字
consigneeTel: this.userDefaultAddress.phoneNum, // 最终收件人手机
deliveryAddress: this.userDefaultAddress.fullAddress, // 最终收件人地址
paymentWay: "ONLINE", // 支付方式
orderComment: this.img, // 买家留言
orderDetailList: this.orderInfo.detailArrayList, // 商品清单
};
// 向服务器发请求
let result = await this.$API.reqSubmitOrder(tradeNo, data);
if (result.code == 200) {
// 如果请求成功,则存订单号以供跳转至支付页面使用
this.orderId = result.data;
this.$router.push("/pay?orderId=" + this.orderId);
} else {
alert(result.data);
}
},
(注意:跳转至支付页面可能会被其他同学影响,报错,因为使用的是相同账号,所以多试几次,就可以了)
因为很多同学同时操作,所以可能出现:购物车商品被其他同学增加或减少,订单不能重复提交等问题。此时稍等一会,或多试几次(自己修改数量以防重复)即可。
十四、支付页面
router/routes.js
import Pay from '@/pages/Pay'
......
{
name: "pay",
path: "/pay",
component: Pay,
meta: { show: true }
},
1、展示支付信息
发请求获取支付信息,
请求地址: /api/payment/weixin/createNative/{orderId}
请求方式:GET
参数类型:
参数名称 | 类型 | 是否必选 | 描述 |
---|---|---|---|
orderId | string | Y | 支付订单ID(通过提交订单得到) |
api/index.js
// 获取支付信息
export const reqPayInfo = (orderId) => requests({
url: `payment/weixin/createNative/${orderId}`,
method: "get"
})
pages/Pay/index.vue
data() {
return {
// 支付信息
payInfo: "",
}
},
computed: {
orderId() {
return this.$route.query.orderId;
},
},
// 不允许给生命周期函数加async|await,但是发请求需要用到async|await,所以this.getPayInfo();这样写,getPayInfo()写在methods中
mounted() {
this.getPayInfo();
},
methods: {
// 获取支付信息
async getPayInfo() {
let result = await this.$API.reqPayInfo(this.orderId);
if(result.code == 200) {
this.payInfo = result.data;
}
},
</em>之内完成支付,超时订单会自动取消。订单号:<em>{{
orderId
}}</em></span
>
<span class="fr"
><em class="lead">应付金额:</em
><em class="orange money">¥{{ payInfo.totalFee }}.00</em></span
>
2、支付页面中使用ElementUI(按需引入)
组件库:
React和Vue都可用:antd(PC端适用); antd-mobile(移动端适用)
Vue可用:ElementUI(PC端适用); vant(移动端适用)
按需引入:
安装ElementUI yarn add element-ui
借助 babel-plugin-component,我们可以只引入需要的组件,以达到减小项目体积的目的。
首先,安装 babel-plugin-component:
npm install babel-plugin-component -D
或
yarn add babel-plugin-component --dev
然后,将 .babelrc (即babel.config.js)修改为:
module.exports = {
presets: ['@vue/cli-plugin-babel/preset'],
plugins: [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
},
],
],
};
注意:配置文件发生变化,需要重启项目。
接下来,如果你只希望引入部分组件,比如 Button 和 MessageBox,那么需要在 main.js 中写入以下内容:
import Vue from 'vue';
import { Button, MessageBox } from 'element-ui';
import App from './App.vue';
// 注册组件
Vue.component(Button.name, Button);
// 或写为 Vue.use(Button)
// 注册组件的另一种写法,挂在原型上
Vue.prototype.$msgbox = MessageBox;
Vue.prototype.$alert = MessageBox.alert;
(最好在vscode上安装个vue-help插件,elementUI提示插件,打el后面会有提示)
使用:
支付页面使用 elementUI 的 MessageBox 弹框组件 和Button组件
实现点击 立即支付按钮,弹出支付框。
MessageBox 弹框组件中,选使用 HTML 片段
message
属性支持传入 HTML 片段。
将dangerouslyUseHTMLString
属性设置为 true,message
就会被当作 HTML 片段处理。
<el-button type="text" @click="open">点击打开 Message Box</el-button>
......
<script>
methods: {
open() {
// this.$alert(message, title),message参数是MessageBox 消息正文内容,title是MessageBox 标题
this.$alert('<strong>这是 <i>HTML</i> 片段</strong>', 'HTML 片段', {
dangerouslyUseHTMLString: true
});
}
}
}
</script>
pages/Pay/index.vue
<span><a class="btn" @click="open">立即支付</a></span>
// elementUI 的 MessageBox 弹框组件,点击立即支付出现支付弹窗
open() {
this.$alert('<strong>这是 <i>HTML</i> 片段</strong>', 'HTML 片段', {
// message属性支持传入 HTML 片段
dangerouslyUseHTMLString: true,
// 中间布局
center: true,
// 是否显示取消按钮 (默认显示确认按钮,不显示取消按钮)
showCancelButton: true,
// 取消按钮的文本内容
cancelButtonText: "支付遇见问题",
// 确定按钮的文本内容
confirmButtonText: "已经支付成功",
// MessageBox 是否显示右上角关闭按钮
showClose: false,
// 还有其他内容,下节再写
});
}
十五、微信支付业务
1、显示支付二维码
服务器返回payInfo支付信息数据中有 codeUrl: “weixin://wxpay/bizpayurl?pr=ruk7767zz” 二维码生成插件根据此相应生成二维码。
使用二维码生成插件:qrcode
(去npm官网搜qrcode,有教程)
用教程中 ES6/ES7写法:
安装: yarn add qrcode
在Pay支付页面,点击立即支付出现支付弹窗,内有支付二维码
pages/Pay/index.vue
// 引入二维码生成插件
import QRCode from "qrcode";
......
// elementUI 的 MessageBox 弹框组件,点击立即支付出现支付弹窗
async open() {
// 使用qrcode插件生成二维码
// QRCode.toDataURL()返回的是Promise
// QRCode.toDataURL() 生成二维码图片的url
let url = await QRCode.toDataURL(this.payInfo.codeUrl);
// this.$alert(message, title),message参数是MessageBox 消息正文内容,title是MessageBox 标题
// 第一个参数是字符串,但有js,所以要用模板字符串
this.$alert(`<img src=${url} />`, "请你微信支付", {
// message属性支持传入 HTML 片段
dangerouslyUseHTMLString: true,
// 中间布局
center: true,
// 是否显示取消按钮 (默认显示确认按钮,不显示取消按钮)
showCancelButton: true,
// 取消按钮的文本内容
cancelButtonText: "支付遇见问题",
// 确定按钮的文本内容
confirmButtonText: "已经支付成功",
// MessageBox 是否显示右上角关闭按钮
showClose: false,
});
},
2、若支付成功 跳至支付成功页面
请求地址:/api/payment/weixin/queryPayStatus/{orderId}
请求方式:GET
参数类型:
参数名称 | 类型 | 是否必选 | 描述 |
---|---|---|---|
orderId | string | Y | 支付订单ID |
api/index.js
// 获取订单支付状态
export const reqPayStatus = (orderId) => requests({
url: `/payment/weixin/queryPayStatus/${orderId}`,
method: "get"
})
当微信支付弹窗弹出,就要持续发获取订单支付状态的请求(持续是因为并不知道用户什么时候支付,所以需要持续发请求,直到能获取到支付状态),若接收到支付成功,则路由跳转至支付成功页面。若收到支付失败,则向用户显示提示信息。
方法:加定时器,1s发1次获取订单支付状态的请求。
pages/Pay/index.vue
// 点击立即支付出现支付弹窗,微信支付业务等
async open() {
// 使用qrcode插件生成二维码
// QRCode.toDataURL()返回的是Promise
// QRCode.toDataURL() 生成二维码图片的url
let url = await QRCode.toDataURL(this.payInfo.codeUrl);
// elementUI 的 MessageBox 弹框组件,点击立即支付出现支付弹窗
// this.$alert(message, title),message参数是MessageBox 消息正文内容,title是MessageBox 标题
// 第一个参数是字符串,但有js,所以要用模板字符串
this.$alert(`<img src=${url} />`, "请你微信支付", {
// message属性支持传入 HTML 片段
dangerouslyUseHTMLString: true,
// 中间布局
center: true,
// 是否显示取消按钮 (默认显示确认按钮,不显示取消按钮)
showCancelButton: true,
// 取消按钮的文本内容
cancelButtonText: "支付遇见问题",
// 确定按钮的文本内容
confirmButtonText: "已经支付成功",
// MessageBox 是否显示右上角关闭按钮
showClose: false,
// MessageBox 关闭前的回调,会暂停实例(即弹框)的关闭
beforeClose: (type, instance, done) => {
// type:区分取消|确定按钮;instance:当前组件实例;done:关闭弹出框的方法
// 如果用户点击取消按钮
if (type == "cancel") {
alert("请联系管理员");
// 清除定时器(即停止请求支付状态)
clearInterval(this.timer);
this.timer = null;
// 关闭弹出框
done();
} else {
// 用户点击了支付成功按钮
// 判断是否支付了
// if (this.code == 200) { 将此步关闭,是为了不付钱就能跳到支付成功页面,方便调试
// 清除定时器(即停止请求支付状态)
clearInterval(this.timer);
this.timer = null;
// 关闭弹出框
done();
// 跳至支付成功页面
this.$router.push("/paysuccess");
// }
}
},
});
// 持续发获取订单支付状态的请求(持续是因为并不知道用户什么时候支付,所以需要持续发请求,直到能获取到支付状态)
// 加定时器,1s发1次获取订单支付状态的请求。
// 若无定时器,则开启新定时器
if (!this.timer) {
this.timer = setInterval(async () => {
// 发请求获取用户支付状态
let result = await this.$API.reqPayStatus(this.orderId);
// 如果支付成功
if (result.code == 200) {
// 清除定时器,即停止发请求
clearInterval(this.timer);
// 保存支付成功返回的code
this.code = result.code;
// 关闭弹出框
this.$msgbox.close();
// 跳至支付成功页面
this.$router.push("/paysuccess");
}
}, 1000);
}
},
router/routes.js
import PaySuccess from '@/pages/PaySuccess'
......
{
name: "paysuccess",
path: "/paysuccess",
component: PaySuccess,
meta: { show: true }
}
pages/PaySuccess/index.vue
<!-- 跳至个人中心 -->
<router-link class="btn-look" to="/center">查看订单</router-link>
<!-- 跳至首页 -->
<router-link class="btn-goshop" to="/">继续购物</router-link>
十六、Center个人中心组件
router/routes.js
import Center from '@/pages/Center'
......
{
name: "center",
path: "/center",
component: Center,
meta: { show: true }
},
Center路由组件中又有 路由组件:myOrder我的订单 和 groupOrder团购订单组件。
1、个人中心二级路由搭建
点击左侧 我的订单 或 团购订单。显示 相应的路由组件。
router/routes.js
{
name: "center",
path: "/center",
component: Center,
meta: { show: true },
children: [
{
path: "myorder",
component: myOrder
},
{
path: "grouporder",
component: groupOrder
},
// 重定向,当进入center时,默认显示myorder
{
path: "/center",
redirect: '/center/myorder'
}
]
},
pages/Center/index.vue
<!--左侧列表-->
<div class="order-left">
<dl>
<dt><i>·</i> 订单中心</dt>
<dd>
<router-link to="/center/myorder">我的订单</router-link>
</dd>
<dd>
<router-link to="/center/grouporder">团购订单</router-link>
</dd>
......
<dl>
</div>
......
<!-- 右侧内容 -->
<div class="order-right">
<router-view></router-view>
2、获取并展示myOrder组件我的订单列表
获取我的订单列表 请求地址: /api/order/auth/{page}/{limit}
请求方式: GET
参数类型:
参数名称 | 类型 | 是否必选 | 描述 |
---|---|---|---|
page | string | Y | 页码 |
limit | string | Y | 每页显示数量 |
api/index.js
// 获取我的订单列表
export const reqMyorderList = (page, limit) => requests({
url: `/order/auth/${page}/${limit}`,
method: "get"
})
pages/Center/myOrder/index.vue
data() {
return {
// 当前第几页
page: 1,
// 每一页展示数据个数
limit: 3,
// 我的订单数据
myOrder: {},
};
},
mounted() {
// 获取我的订单数据
this.getData();
},
methods: {
// 获取我的订单数据
async getData() {
const { page, limit } = this;
let result = await this.$API.reqMyorderList(page, limit);
if (result.code == 200) {
this.myOrder = result.data;
}
},
// 获取组件分页器当前点击的那一页,(用到了自定义事件,子给父传数据)
getPageNo(page) {
// 修改page为当前点击的页
this.page = page;
// 获取最新点击页的数据
this.getData();
}
},
展示:
<div class="orders">
<table
class="order-item"
v-for="(order, index) in myOrder.records"
:key="order.id"
>
<thead>
<tr>
<th colspan="5">
<span class="ordertitle"
>{{ order.createTime }} 订单编号:{{ order.outTradeNo }}
<span class="pull-right delete"
><img src="../images/delete.png" /></span
></span>
</th>
</tr>
</thead>
<tbody>
<tr v-for="(cart, index) in order.orderDetailList" :key="cart.id">
<td width="60%">
<div class="typographic">
<img :src="cart.imgUrl" width="100px" />
<a href="#" class="block-text">{{ cart.skuName }}</a>
<span>x{{ cart.skuNum }}</span>
<a href="#" class="service">售后申请</a>
</div>
</td>
<!-- 合并行,所以只需一次数据,所以加v-if="index==0" -->
<td
v-if="index == 0"
:rowspan="order.orderDetailList.length"
width="8%"
class="center"
>
{{ order.consignee }}
</td>
<td
v-if="index == 0"
:rowspan="order.orderDetailList.length"
width="13%"
class="center"
>
<ul class="unstyled">
<li>总金额¥{{ order.totalAmount }}.00</li>
<li>在线支付</li>
</ul>
</td>
<td
v-if="index == 0"
:rowspan="order.orderDetailList.length"
width="8%"
class="center"
>
<a href="#" class="btn">{{ order.orderStatusName }} </a>
</td>
(17)用户登录后的导航守卫(路由独享守卫与组件内守卫)
例如,为防止,不经过某路由就直接 在地址栏输入本路由地址而跳转至 本路由,导致本路由获取不到前面路由的某些数据,所以给本路由加 路由独享守卫。
**业务:只能从购物车跳到交易页面,只能从交易页面跳到支付页面。**而不能在地址栏直接更改地址跳转至交易页面等。
路由独享守卫:
router/routes.js
{
name: "trade",
path: "/trade",
component: Trade,
meta: {
show: true
},
// 路由独享守卫,
beforeEnter: (to, from, next) => {
if (from.path == '/shopcart') {
// 如果从shopcart路由跳到当前路由trade,则放行
next();
} else {
// 不是从shopcart路由跳到当前路由trade,不放行,还是停留到原来的路由
next(false);
}
}
},
{
name: "pay",
path: "/pay",
component: Pay,
meta: {
show: true
},
// 路由独享守卫,
beforeEnter: (to, from, next) => {
if (from.path == '/trade') {
// 如果从trade路由跳到当前路由pay,则放行
next();
} else {
// 不是从trade路由跳到当前路由pay,不放行,还是停留到原来的路由
next(false);
}
}
},
组件内守卫:
pages/PaySuccess/index.vue
name: "PaySuccess",
// 钩子函数
// 在渲染该组件的对应路由被confirm前 调用
// 不能获取组件实例this,因为守卫执行前,组件实例还没被创建
beforeRouteEnter(to, from, next) {
// 当然,也可以用路由独享守卫也可以实现此
if (from.path == "/pay") {
next();
} else {
next(false);
}
},
// 在当前路由改变,该组件被复用时调用(即地址栏中的参数改变,展示对应数据的该组件时调用)
// 举例:从 /foo/1 到 /foo/2 之前跳转时调用。由于会渲染同样的Foo组件,因此组件实例会被复用。
// 可以访问组件实例的this
// beforeRouteUpdate(to, from, next) {
// },
// 导航离开该组件对应路由时调用
// 可以访问组件实例的this
// beforeRouteLeave(to, from, next) {
// next();
// }
(18)未登录的导航守卫
若用户未登录,则不能跳到我的订单、交易页面、个人中心。
用到了全局路由守卫
router/routes.js
// 全局路由守卫,前置守卫(初始化时被调用,每次路由切换之前调用)
router.beforeEach(async (to, from, next) => {
// to:即将要进入的目标路由 from:当前导航正要离开的路由 next放行函数
let token = store.state.user.token;
// 用户信息
let name = store.state.user.userInfo.name;
// 当token存在,即用户登录了
if (token) {
// 当地址栏输入/login 用户登陆了,就不能再去登录页面,只能去首页
if (to.path == '/login') {
next('/home')
} else {
// 不是去/login,放行
if (name) {
// 如果用户信息存在,则放行
next();
} else {
// 用户信息不存在,派发action获取用户信息后再放行
try {
await store.dispatch("getUserInfo")
next();
} catch (error) {
// 如果不能获取用户信息,说明token失效,此时需要清除token,重新登录
await store.dispatch("userLogout")
next('/login');
}
}
}
} else {
// 未登录,不能去交易相关页面和个人中心
let toPath = to.path;
// indexOf()返回某个字符串值在字符串中首次出现的位置。如果要检索的字符串值没有出现,则该方法返回 -1。
if (toPath.indexOf('/trade') != -1 || toPath.indexOf('/pay') != -1 || toPath.indexOf('/center') != -1) {
// 如果是去交易相关页面和个人中心,则不能去,需登录后方可跳转
// 把未登录时候没有去成的路由,存储于地址栏中,以便登录后去。
next('/login?redirect=' + toPath);
} else {
// 不是去交易相关页面和个人中心,则放行
next();
}
}
})
pages/Login/index.vue
// 登录成功,判断login路由有无query参数,若有则跳至参数对应的页面。若无则跳转到Home路由组件
// login路由有query参数情况,是因为全局守卫中设置,若用户未登录则不能进交易等页面,此时存储要跳转的路由放在query上,登录后可进入。
let toPath = this.$route.query.redirect || "/home";
this.$router.push(toPath);
(19)图片懒加载
使用插件:vue-lazyload
安装:yarn add vue-lazyload
main.js
// 引入图片懒加载插件
import VueLazeload from 'vue-lazyload';
// 引入懒加载默认图片(即真实图片没加载好之前,加载时显示的图片)
import tp from '@/assets/images/1.png';
// 注册插件
Vue.use(VueLazeload, {
// 懒加载默认图片,(即真实图片没加载好之前,加载时显示的图片)
loading: tp,
})
pages/Search/index.vue
<!-- v-lazy自定义指令图片懒加载 -->
<img v-lazy="good.defaultImg" />
温故 Vue插件 知识:
src/plugins/myPlugins.js
// Vue插件一定暴露一个对象
let myPlugins = {};
// vue提供install可供我们开发新的插件及全局注册组件等
// install方法第一个参数是vue的构造器,第二个参数是可选的选项对象(即配置)
myPlugins.install = function (Vue, options) {
// 可以设置 Vue.prototype.$bus
// Vue.directive
// Vue.component
// Vue.filter......
// 例如:设置自定义指令
Vue.directive(options.name, (el, binding) => {
// 将元素的内容转为大写
el.innerHTML = binding.value.toUpperCase();
})
}
// 对外暴露
exprot default myPlugins;
main.js
import myPlugins from '@/plugins/myPlugins';
Vue.use(myPlugins,{
name: 'upper',
});
Aba.vue
<!-- 使用自定义指令 -->
<h1 v-upper="msg"></h1>
......
data() {
return {
msg: "abc"
}
}
可能这时已经忘了自定义指令,去看看vue文档吧!很详细。
(20)vee-validate插件 表单验证
最好安装2版本的,yarn add vee-validate@2
src/plugins/validate.js
// 封装的表单验证插件
// 引入vee-validate插件 表单验证
import Vue from 'vue';
import VeeValidate from 'vee-validate';
// 中文提示信息
import zh_CN from 'vee-validate/dist/locale/zh_CN';
Vue.use(VeeValidate);
// 表单验证
// 第一个参数 'zh_CN',设置提示信息为中文(默认为英文)
VeeValidate.Validator.localize('zh_CN', {
messages: {
...zh_CN.messages,
// 用于确认密码的提示信息,若两密码不同,则提示确认密码必须与密码相同
is: (field) => `${field}必须与密码相同`,
},
attributes: {
// 提示信息,无'手机号',则是phone无效;有'手机号',则是手机号无效
phone: '手机号',
code: '验证码',
password: '密码',
password1: '确认密码',
agree: '协议'
}
})
// 自定义校验规则
VeeValidate.Validator.extend("agree", {
validate: (value) => {
return value;
},
getMessage: (field) => field + "必须同意",
})
main.js
// 引入封装的表单验证插件
import "@/plugins/validate";
src/pages/Register/index.vue 注册组件
<div class="content">
<label>手机号:</label>
<!-- v-validate用于表单验证,自定义指令(封装的表单验证插件在plugis文件夹中) -->
<!-- required: true 必须要校验 regex: /^1\d{10}$/正则表达式,配置校验规则 -->
<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
placeholder="请输入验证码"
v-model="code"
name="code"
v-validate="{ required: true, regex: /^\d{6}$/ }"
:class="{ invalid: errors.has('code') }"
/>
<button style="width: 100px; height: 38px" @click="getCode">
获取验证码
</button>
<span class="error-msg">{{ errors.first("code") }}</span>
</div>
<div class="content">
<label>登录密码:</label>
<input
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
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">
<!-- 'agree': true 是自定义校验规则 -->
<input
name="agree"
v-model="agree"
type="checkbox"
:checked="agree"
v-validate="{ required: true, 'agree': true }"
:class="{ invalid: errors.has('agree') }"
/>
<span>同意协议并注册《尚品汇用户协议》</span>
<span class="error-msg">{{ errors.first("agree") }}</span>
</div>
// 点击完成注册按钮
async userRegister() {
// 如果表单各项都验证成功,才能向服务器发请求进行注册
const success = await this.$validator.validateAll();
if (success) {
try {
// const {xxx} = this es6语法,相当于const xxx = this.xxx
const { phone, code, password } = this;
await this.$store.dispatch("userRegister", {
phone,
code,
password,
});
// 注册成功,跳转至登录页面
this.$router.push("/login");
} catch (error) {
alert(error.message);
}
(21)路由懒加载
路由懒加载 在Vue Router官方文档中,版本选3。
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
结合 Vue 的异步组件 (opens new window)和 Webpack 的代码分割功能 (opens new window),轻松实现路由组件的懒加载。
router/routes.js
无需像以下代码一样 全部引入路由组件
// import Home from '@/pages/Home'
// import Search from '@/pages/Search'
// import Login from '@/pages/Login'
// import Register from '@/pages/Register'
......
{
path: "/home",
// component: Home,
meta: {
show: true
}
},
只需在配置路由时写 component: () => import('@/pages/Home'),
:
{
path: "/home",
// component: Home,
component: () => import('@/pages/Home'),
meta: {
show: true
}
},
(22)处理map文件
打包,yarn build
js文件夹中会有map文件。
项目打包后,代码都是经过压缩加密的,如果运行时报错,输出的错误信息无法准确得知是哪里的代码报错,有了 map 文件就可以像未加密的代码一样,准确的输出是哪一行哪一列有错。
但是我们有时候并不需要这个文件,通过以下的设置可以让 Vue 打包的时候不生成 .map 文件,缩小生产环境的包大小。
在vue.config.js设置让 Vue 打包的时候不生成 .map 文件
module.exports = {
productionSourceMap: false,
(23)服务器
将我们的前端的前台项目部署在自己买的服务器上。
(架构有很多种,有所有资源等都放在一台服务器,好处是成本低;但是用户量多,一台服务器就不够了,所以一台服务器放Tomcat,一台服务器放数据库,一台服务器OSS放资源文件等。但是用户量更多,就多个放Tomcat的服务器,分布式缓存服务器、剥离前端和后端等等)
中小型企业开发一个项目刚开始都是使用前后端分离架构,不考虑三高、微服务等。因为一开始并没有太大用户流量,所以使用成本低的前后端分离架构,是后续好扩展好改造的架构。
1、购买服务器
阿里云、腾讯云、
腾讯云便宜,已买腾讯云的服务器。
2、安全组
在腾讯云搜 安全组,设置为这样:
因为我的服务器是Windows的,所以不需要xshell,远程控制,输入用户名密码即可控制服务器。
3、nginx反向代理
当用户访问服务器ip地址,(有域名则访问域名,无域名访问ip地址)
当用户访问服务器ip地址,就会展现网站首页:需要如下配置:在nginx的conf文件夹中的nginx.conf配置:
server {
listen 80;
server_name localhost;
location / {
root dist;
index index.html index.htm;
}
打包好的dist文件需要放下图此处,即放nginx文件夹中。
还需能访问到http://gmall-h5-api.atguigu.cn这台服务器的数据。(此服务器是尚硅谷的相应后端接口服务器)
代理服务器就是位于发起请求的客户端与原始服务器端之间的一台跳板服务器,正向代理可以隐藏客户端,反向代理可以隐藏原始服务器。
Nginx (engine x) 是一个高性能的HTTP和反向代理web服务器。
location /api {
proxy_pass http://gmall-h5-api.atguigu.cn;
}
记得,要打开nginx服务(查看任务管理器看有无nginx服务)若无,则双击nginx.exe或,通过命令行:nginx.exe即可。
并且服务器要保持开机。
这样就可以在很多地方通过 IP(没有域名前提下)访问我的项目了。
上传本地文件到服务器,看腾讯云文档,很详细:
上传文件
- 在本地计算机,使用快捷键【Windows + R】,打开【运行】窗口。
- 在弹出的【运行】窗口中,输入 mstsc,单击【确定】,打开【远程桌面连接】对话框。
- 在【远程桌面连接】对话框中,输入轻量应用服务器公网 IP 地址,单击【选项】。如下图所示:
- 在【常规】页签中,输入轻量应用服务器公网 IP 地址和用户名 Administrator。如下图所示:
- 选择【本地资源】页签,单击【详细信息】。如下图所示:
- 在弹出的【本地设备和资源】窗口中,选择【驱动器】模块,勾选需要上传到 Windows 轻量应用服务器的文件所在的本地硬盘,单击【确定】。如下图所示:
- 本地配置完成后,单击【连接】,远程登录 Windows 轻量应用服务器。
- 在 Windows 轻量应用服务器中,单击 >【这台电脑】,即可以看到挂载到轻量应用服务器上的本地硬盘。如下图所示:
- 双击打开已挂载的本地硬盘,并将需要拷贝的本地文件复制到 Windows 轻量应用服务器的其他硬盘中,即完成文件上传操作。
例如,将本地硬盘(E)中的 A 文件复制到 Windows 轻量应用服务器的 C: 盘中。
下载文件:
如需将 Windows 轻量应用服务器中的文件下载至本地计算机,也可以参照上传文件的操作,将所需文件从 Windows 轻量应用服务器中复制到挂载的本地硬盘中,即可完成文件下载操作。