vue2项目练习-购物商城项目

创建项目

基于vueCli自定义创建项目架子

我在vue2my文件夹中创建的项目,在这个文件夹中打开终端,vue create shopping,
选择第三个自定义,
选择babel,router,vuex,css Pre-processors,Linter/Formatter
选择2.x 选择n 选择less 选择Standard 选择lint on save 选择第一个 选择n
创建成功后,在项目终端npm run serve,进入http://localhost:8080/ 成功即可
注意!!!写项目vocode工作区打开的是项目根目录!!不可以是根目录的父级目录!!不然不会成功还容易莫名其妙报错!虽然我也不知道为什么但是只打开项目根目录一下子就好了!

调整初始化目录

理解:默认的目录不够完美,想要将目录调整成符合企业规范的目录
三步走:
1.删除多余的文件
vue自动生成的和vue页面的图片和vue展示代码
assents里面的logo,components里的helloworld.vue,views里面的两个.vue文件
2.修改路由配置和App.vue文件
对路由规则进行清空,router的index.js文件清空后:

1
2
3
4
5
6
7
8
9
10
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const router = new VueRouter({
routes:[]
})

export default router

App.vue只留下路由出口

1
2
3
4
5
6
7
8
9
<template>
<div id="app">
<router-view/>
</div>
</template>

<style lang="less">

</style>

3.新增两个目录api/utils
① api接口模块:发送ajax请求的接口模块
② utils 工具模块:自己封装的一些工具方法模块

vant组件库

组件库:第三方封装好了很多的组件,整合到一起就是一个组件库
vue组件库一般按照不同平台进行分为pc端和移动端
第三方vue组件库vant-ui。vant2支持vue2,vant4支持vue3
https://vant-ui.github.io/vant/v2/#/zh-CN/
安装和使用方法网站有提供文档可以直接使用。

安装vant组件库

Vue 2 项目,安装 Vant 2:
npm i vant@latest-v2 -S
上面这行是官方文档的代码,但是我这样安装失败,
安装出问题,跟着别人用:npm i vant@latest-v2 -S –legacy-peer-deps
安装成功

按需导入

vant分为全部导入和按需导入,推荐自动按需导入。
全部导入的方法在官方文档中有,此项目采用自动按需导入
1.安装vant组件库(上面已完成)
2.安装插件npm i babel-plugin-import -D
-D的意思是把这个插件安装成开发依赖,只在开发过程中使用
官方文档的的命令我还是失败,使用npm i babel-plugin-import -D –legacy-peer-deps
安装插件成功
3.babel.config.js中配置(分为低版本和高版本,官方文档上面的一种是低版本适用的,下面的是高版本使用的,这里使用高版本)
找到项目中的这个文件,然后贴贴,不要替换掉之前的,把文档里的往里面添加,添加后的文件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
// 对于使用 babel7 的用户,可以在 babel.config.js 中配置
plugins: [
['import', {
libraryName: 'vant',
libraryDirectory: 'es',
style: true
}, 'vant']
]
}

4.main.js按需导入注册
官方文档:
接着你可以在代码中直接引入 Vant 组件,插件会自动将代码转化为方式二中的按需引入形式
比如你要使用buton插件和其他插件,那么在main.js中
import { Button,插件名2 } from ‘vant’;
Vue.use(Button)
Vue.use(插件名2)
如果导入失败重启项目重启终端试试
但是!这样下去按需导入的越来越多会导致main.js的代码越来越多,所以通常要把这部分按需导入的内容放到utils文件夹下面,在里面新建一个文件vant-ui.js,把刚才的代码放进来

1
2
3
4
5
6
7
8
最终main.js需要加代码
import '@/utils/vant-ui'
vant-ui.js文件代码
// 按需导入
import Vue from 'vue'
import { Button } from 'vant'
// 这里注意上面是vant不是vants!!!
Vue.use(Button)

5.测试使用
在.vue中
主要按钮
渲染不出来关了vscode重来一遍就可以

项目中的vw适配-基于postcss

写的代码都是px,但是要移动端适配,使用插件帮我们把px自动转化成vw
步骤看vant官网-进阶用法-浏览器适配文档
1.安装插件
npm下载:npm install postcss-px-to-viewport@1.1.1 -D –force
2.根目录新建postcss.config.js文件,填入配置(和src是并列的)

1
2
3
4
5
6
7
8
9
10
11
// postcss.config.js
module.exports = {
plugins: {
'postcss-px-to-viewport': {
//vw适配的标准屏的宽度
//如果设计图是750,调成1倍图是375这里写370
//如果设计图是640的,那么调成1倍图标准图是320
viewportWidth: 375
}
}
}

然后就可以在.vue文件的style中写代码了

代码编写

路由设计配置

根据项目分析路由

分析项目页面,设计路由,先配一级路由再配二级路由
一级路由:单个页面,独立展示的
(登录login,首页架子layout,搜索页search,搜索列表search,商品详情prodetail,结算支付pay,订单管理myorder)
二级路由:首页架子下面的(首页,分类,购物车,我的)

配置一级路由

1.在views中新建文件夹,每一个一级路由建一个文件夹:登录login,首页架子layout,搜索页search,搜索列表search,商品详情prodetail,结算支付pay,订单管理myorder
2.在每个文件夹下面建立一个主核心文件 index.vue,注意seach里面有两个.vue,搜索页用的inde.vue,搜索列表是list.vue
3.在router文件夹index.js中配置路由规则

1
2
3
4
5
6
7
8
9
10
11
12
//原本路由文件
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const router = new VueRouter({
routes: []

})

export default router
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//所有一级路由配置后
import Vue from 'vue'
import VueRouter from 'vue-router'
// 如果文件是文件夹里面的index.vue文件,那么写到文件夹就可以,会自动找到里面的index文件
import Login from '@/views/login'
import Layout from '@/views/layout'
import Search from '@/views/search'
import SearchList from '@/views/search/list'
import ProDetail from '@/views/prodetail'
import Pay from '@/views/pay'
import MyOrder from '@/views/myorder'

Vue.use(VueRouter)

const router = new VueRouter({
routes: [
{ path: '/login', component: Login },
// 下面这里首页架子layout因为一点进来就是,所以用/
{ path: '/', component: Layout },
{ path: '/search', component: Search },
{ path: '/searchlist', component: SearchList },
// 这里加Id因为动态路由传参,确认将来是哪个商品,路由参数中携带id
{ path: '/prodetail/:id', component: ProDetail },
{ path: '/pay', component: Pay },
{ path: '/myorder', component: MyOrder }
]
})

export default router

配置二级路由(底部导航)

二级路由关键的是导航栏,四个二级路由就是导航栏的四个小图标,所以先实现底部导航
通过vant组件库文档实现底部导航tabbar
1.vant-ui.js中按需引入
在vant2文档中找到导航组件-Tabbar标签栏-引入
把上面的

1
2
3
4
import { Tabbar, TabbarItem } from 'vant';

Vue.use(Tabbar);
Vue.use(TabbarItem);

直接cv到vant-ui.js
(视频上保存会自动合并import的组件,我的不会,手动给它们放进去不报错就行)
2.layout.vue中粘贴官方代码
把下面基础用法中van-tabbar标签包裹的内容,v-model的东西删掉目前不要,记得把它们放到div中

1
2
3
4
5
6
<van-tabbar >
<van-tabbar-item icon="home-o">标签</van-tabbar-item>
<van-tabbar-item icon="search">标签</van-tabbar-item>
<van-tabbar-item icon="friends-o">标签</van-tabbar-item>
<van-tabbar-item icon="setting-o">标签</van-tabbar-item>
</van-tabbar>

cv到架子里,layout文件夹index.vue
这时候打开运行页面能够看到底部有导航栏了
3.修改导航的文字和图标和颜色
(1)图标修改
文字很好修改,修改图标要去vant组件库找,vant2文档-基础组件-icon图标,右边有好多的小图标,想要换成哪个就把上面标签icon=”值”的值改成图标下面的值即可
这里我没按视频的,自己选了几个好看的用
(2)颜色修改
颜色的修改还是看vant2文档,icon 的 color 属性用来设置图标的颜色,但是这个我试了没用,跟着视频走,不要去icon图标里找,去tabbar导航栏找-自定义颜色
文档
其中active-color是图标激活时的颜色,inactive-color是图标未激活时的颜色
以上三步完成后的底部导航

1
2
3
4
5
6
    <van-tabbar  active-color="#f8612c" inactive-color="#000">
<van-tabbar-item icon="flower-o" >首页</van-tabbar-item>
<van-tabbar-item icon="shop-collect-o">分类页</van-tabbar-item>
<van-tabbar-item icon="gift-o" >购物车</van-tabbar-item>
<van-tabbar-item icon="user-circle-o">我的</van-tabbar-item>
</van-tabbar>

导航栏完成后,根据导航栏配置二级路由:配置导航链接和路由出口 1.在架子layout文件夹里面建立四个.vue文件,代表四个导航栏出口目标 home.vue;category.vue;cart.vue;user.vue 四个文件夹配置最基本的代码,如
1
2
3
4
5
6
7
8
9
10
<template>
<div> User</div>
</template>
<script>
export default {
name: 'UserIndex'
}
</script>
<style>
</style>
2.在router-index.js中配置二级路由 修改添加以下代码,注意关键词children和书写格式 ctrl+D可以选同一行的两个相同部分同时修改,cv后修改名字很好用 (再加一行代码redirect: '/home', // 重定向,做一点优化,让访问/的时候访问Home)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 二级路由
import Home from '@/views/layout/home'
import Category from '@/views/layout/category'
import Cart from '@/views/layout/cart'
import User from '@/views/layout/user'

Vue.use(VueRouter)

const router = new VueRouter({
routes: [
{ path: '/login', component: Login },
// 下面这里首页架子layout因为一点进来就是,所以用/
//修改增加的主要是下面这些
{
path: '/',
component: Layout,
redirect: '/home', // 重定向,做一点优化,让访问/的时候访问Home
childeren: [
{ path: '/home', component: Home },
{ path: '/category', component: Category },
{ path: '/cart', component: Cart },
{ path: '/user', component: User }
]
}
//...后面的一级路由没变动,省略
]
})

export default router

3.给标签栏配置路由模式
vant2文档-tabbar标签栏-路由模式文档中:
标签栏支持路由模式,用于搭配 vue-router 使用。路由模式下会匹配页面路径和标签的 to 属性,并自动选中对应的标签
核心:在导航标签后面加route!!不是router!!!,在小图标里面加to地址

1
2
3
4
5
6
  <van-tabbar  route active-color="#f8612c" inactive-color="#000">
<van-tabbar-item to="/home" icon="flower-o" >首页</van-tabbar-item>
<van-tabbar-item to="/category" icon="shop-collect-o">分类页</van-tabbar-item>
<van-tabbar-item to="/cart" icon="gift-o" >购物车</van-tabbar-item>
<van-tabbar-item to="/user" icon="user-circle-o">我的</van-tabbar-item>
</van-tabbar>

然后配置路由出口,写在导航栏上面,这样内容在上导航栏在下

1
<router-view></router-view>

(如果一点击导航栏不显示跳转后页面的内容而且底部导航栏也不见了,那么是router-index.js里面的children拼写错误)

登录页

登录页静态布局

准备工作
  1. 在src下新建文件夹style-common.less重置默认样式
    以后样式都放在style文件夹
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 重置默认样式
    * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    }

    // 文字溢出省略号
    .text-ellipsis-2 {
    overflow: hidden;
    -webkit-line-clamp: 2;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    }
  2. main.js中导入应用
    1
    import '@/styles/common.less'
  3. 将图片素材放到assets文件夹里面
登录页静态布局编写

1.头部组件说明(vant-NavBar组件)
在views文件夹-login文件夹-index.vue中编写,登录可以分为t头部和主体两部分,头部用到组件vant-NavBar
去官方文档-导航组件-NavBar
先引入-到utils中导入NavBar
然后在login-index.vue中使用,用什么导入什么,这里我导入的是

1
2
3
<van-nav-bar
title="用户登录"
left-arrow @click-left="$router.go(-1)"></van-nav-bar>

添加一个点击左边会跳转到上一级路由的逻辑
然后运行,进入http://localhost:8080/#/login 可以看到上方有导航栏了,点击左上角箭头能返回
2.通用样式覆盖
希望以后所有的页面左上角的箭头都能变成蓝色的这个小箭头,把他变成通用样式
页面打开,F12看小箭头的类van-icon van-icon-arrow-left van-nav-bar__arrow
其中van-nav-bar__arrow是这个小箭头的样式
回到刚才的styles-common.less中配置
注意是写道less文件不是写到.css文件

1
2
3
4
5
6
//配置导航左边箭头的通用样式,加两个类的权重让它的权重更高一些
.van-nav-bar{
.van-nav-bar__arrow{
color:#631ee4;
}
}

3.其他静态结构编写
copy的文件代码

登录页逻辑功能(重难点)

在以后项目中,通常将axios请求方法封装到request模块
理解:使用 axios 来请求后端接口
一般都会对 axios 进行 一些配置(比如:配置基础地址,请求响应拦截器等)
所以项目开发中,都会对 axios 进行基本的二次封装,单独封装到一个request 模块中,便于维护使用
再理解:后面代码把axios用instance创建实例,这样这目前的功能是instance这个实例带来的,后续找别的接口再封装新的实例,这样各个实例各个接口各个功能之间不会被污染,创建后的配置配置的是创建出来的实例
接口文档:https://apifox.com/apidoc/shared-12ab6b18-adc2-444c-ad11-0e60f5693f66/doc-2221080

演示地址:http://cba.itlike.com/public/mweb/#/

基地址:http://cba.itlike.com/public/index.php?s=/api/
方法:
1.安装axios
npm i –save axios –legacy-peer-deps
2.在utils文件夹中新建request.js文件,根据axios官方文档进行导入和创建实例
axios官方文档
https://www.axios-http.cn/docs/instance
把axios里面实例的代码贴到文件夹

1
2
3
4
5
6
const instance = axios.create({
baseURL: 'https://some-domain.com/api/',
timeout: 1000,
headers: {'X-Custom-Header': 'foobar'}
});

然后再根据项目修改基础地址和超时时间
3.创建实例,添加配置,导出实例
根据官方文档cv代码,然后修改axios为上面创建的Instance,并且给axios剥一层.data
官方剥下来的代码和修改后的说明,完整request.js文件代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 导入axios
import axios from 'axios'
// 创建axios实例,对创建出来的实例进行自定义配置
// 这样的好处是不会污染原始的axios实例
// 创建实例代码在axios官方文档cv
const instance = axios.create({
// baseURL看接口文档的基础地址更改
baseURL: 'http://smart-shop.itheima.net/index.php?s=/api',
// 超时时间,这里是5s
timeout: 5000,
headers: { 'X-Custom-Header': 'foobar' }
})

// 上面创建,下面写自定义配置:请求,响应拦截器
// axios官方文档拦截器代码cv
// 官方代码的拦截器是axios的拦截器
// 这里我们希望拦截器作用到上面创建的instance上面
// 所以要把官方文档拦截器中的axios. 改成instance.
// 然后再格局项目修改URL基地址

// 添加请求拦截器
instance.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
return config
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error)
})

// 添加响应拦截器
instance.interceptors.response.use(function (response) {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
// 下面这行最后加一行.data,因为默认axio会多包装一层data,不好处理,要响应拦截器处理一下
return response
}, function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error)
})

// 导出配置好的实例
export default instance

4.进行测试
在login.vue文件进行测试
在script里连接接口进行测试,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
// 导入axios文件
import request from '@/utils/request'

export default {
name: 'LoginPage',
async created () {
// 去接口文档10.图像验证码找接口地址
const res = await request.get('/captcha/image')
// 如果能成功打印出结果代表axios连接测试成功
console.log(res)
// 运行项目,去/login页面看控制台是否有返回的结果
}
}
</script>

!!!问题解决
然后运行,进入login页面看控制台,我的开始报错了,看别人发的,跨域问题,在封装axios里,把设置秒数下面的headers删掉就好啦
就是这句代码 headers: { ‘X-Custom-Header’: ‘foobar’ }

图形验证码功能进一步完成

在上面测试axios的时候使用的是获取图形验证码完成的,这里继续上面的实现图形验证码功能
理解:图形验证码本质是从后台请求回来的图片,用户在左边输入,如果和后台的匹配,那么会继续提供服务,否则不提供服务。本质是强制人工服务,确保你是真人,防御机器攻击
功能:输入正确的图形验证码后,才会发送短信验证码,接口文档的11是获取短信验证码,有三个参数:captchaCode(图形验证码)captchaKey(图形验证码key)mobile(接收验证码手机)
做法理解:
1.动态将请求回来的base64图片解析渲染出来
2.点击验证码图片盒子会刷新验证码
理解参数:key是图形验证码的唯一标识,后台根据图形验证码和key比对确定唯一的正确结果,然后才会给对应的手机号发送验证码
做法实现:
1.去login-index.vue中的增加数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<script>
// 导入axios文件
import request from '@/utils/request'

export default {
name: 'LoginPage',
// 图形验证码输入正确后发短信验证码
data () {
return {
picCode: '', // 用户输入的图形验证码
picKey: '', // 请求完毕的图形验证码的唯一标识
picUrl: ''// 用来做渲染的存储请求渲染的图片地址
}
},
async created () {
// 下面注释掉的是测试用的代码
// 去接口文档10.图像验证码找接口地址
// const res = await request.get('/captcha/image')
// 如果能成功打印出结果代表axios连接测试成功
// console.log(res)
// 运行项目,去/login页面看控制台是否有返回的结果
//* *********************** */
// 下面这些是实现图形验证码输入正确后发短信验证码的部分
// 获取图形验证码
this.getPicCode()
},
methods: {
// 获取图形验证码
async getPicCode () {
// 下面获取后端data
// const res = await request.get('/captcha/image')
// console.log(res)
// 解构一下直接获取数据
const { data: { base64, key } } = await request.get('/captcha/image')
this.picUrl = base64 // 存储地址
this.picKey = key // 存储唯一标识
}
}
}
</script>

2.修改上面的页面,验证码图片不要用写死的图片地址,改为后台传过来的托地址

1
2
3
4
<!-- 原本是这行写死的图片,现在要改为后台传过来的图片地址
<img src="@/assets/code.png" alt="">
-->
<img :src="picUrl" alt="">

!!!问题解决:
这里成功后打开页面能显示出验证码图片,同时刷新会获得不同的验证码图片
如果没有获取到图片检查发现是没有获得picUrl数据,检查配置文件request.js,可能是这里没有.data// 下面这行最后加一行.data,因为默认axio会多包装一层data,不好处理,要响应拦截器处理一下
return response.data
3. 改进:动态渲染图形验证码,并且点击图片时要重新刷新验证码
注册一个点击事件即可

1
<img v-if="picUrl" :src="picUrl" @click="getPicCode">

成功后的注册页面截图

api 接口模块 -封装图片验证码接口

目标:将请求封装成方法,统一存放到api模块,与页面分离
理解:前面的login-index.js文件后续接口请求越来越多可能有请求A,请求B,但是后续的其他页面可能用到请求A,请求B,而且一个页面中写多个请求不好看,所以要对接口请求进行封装(带入后端课的模块化封装代码理解,一个意思)
目的:写一个API模块存放封装好的请求函数(里面有请求A函数,请求B函数….),其他页面直接拿函数调用即可
做法:
1.在api文件夹下建立login.js(存放登录相关的接口请求)

1
2
3
4
5
6
7
// 此处用于存放所有登录相关的接口请求
import request from '@/utils/request.js'
// 1.获取图形验证码
export const getPiccode = () => {
// return必不可少!!!!
return request.get('/captcha/image')
}

2.修改login-index.vue代码
首先把之前导入的axios接口注释掉,改为从login.js中按需导入需要的函数getPiccode()

1
2
3
4
5
6
// 导入axios文件
// import request from '@/utils/request.'
// 封装后上面的代码放到login.js里面了
// 改为按需导入封装好的函数
// getPiccode名字冲突没关系,这个是全局的,下面是实例的,用的时候如果是this.getPiccode就是实例的,没有this.就是全局的
import { getPiccode } from '@/api/login'

然后在代码中methods部分把之前的从axios接口获取数据的部分改为导入的函数getPiccode()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
methods: {
// 获取图形验证码
async getPicCode () {
// 下面获取后端data
// const res = await request.get('/captcha/image')
// console.log(res)
// 解构一下直接获取数据
// 封装后:调用全局的getPiccode()函数
const { data: { base64, key } } = await getPiccode()
this.picUrl = base64 // 存储地址
this.picKey = key // 存储唯一标识
console.log(this.picUrl)// 检查是否获取到了图片
}
}

toast轻提示

理解:用户输入正确的验证码后,该输入手机号然后获取短信验证码了,但是手机号可能输入错误,alter弹窗不好看,toast轻提示更好。不管验证码发送失败或成功,都应该给到用户一个小提醒(在页面中弹出一个黑色半透明的小提示)
补充:toast不止是能进行文本提示,将来添加loading小动画效果也都可以,各种语法在官方文档都有
方法:
先根据vant文档进行注册安装,然后再使用
两种使用方法:
1.导入调用(组件内或非组件中均可以用)
2.通过this直接调用不需要导入(必须在组件内,本质是把方法注册挂载到了vue原型上)


操作: 1.vant2官网文档-基础组件-toast轻提示 先引入注册,utils文件夹-vant-ui.js文件中引入注册 2.使用方法1(导入调用) 这种方法组件内或非组件中均可以用,非常灵活 在login-index.vue中导入,调用直接Toast('提示内容')即可
1
2
3
4
5
6
7
8
9
10
11
import { Toast } from 'vant'
//在methods中图形验证码加载成功后添加轻提示测试
// 获取图形验证码
async getPicCode () {
const { data: { base64, key } } = await getPiccode()
this.picUrl = base64 // 存储地址
this.picKey = key // 存储唯一标识
console.log(this.picUrl)// 检查是否获取到了图片
Toast('测试导入调用轻提示;获取图形验证码成功啦')
}
}

成功后是这样的alt text
2.使用方法2(只能在组件内直接调用,不需要导入)
注释掉刚才方法1添加的所有代码,在刚才Toast(‘’)的位置改成this.$toast(‘提示内容’)即可
必须要能拿到组件实例才能调this.$toast(‘提示内容’)
成功后是这样的alt text

短信验证倒计时+短信验证请求

理解:在点击获取短信验证码成功后,按钮地方变成灰色的倒计时60秒,如:重新发送(28)秒。在倒计时之前要进行一些校验处理,比如只有图片验证码正确,手机号填写正确,才会发送验证码同时才会进行倒计时。通常前面的校验完成后会发送请求,这里同时要封装短信验证请求接口,给用户发送成功的轻提示(此项目短信验证码默认为246810)
实现分析:

点击按钮,实现倒计时效果

由于倒计时从60开始每隔一秒-1,到0之后还要恢复到60,所以需要一个变量总秒数存60,还要一个变量代表当前秒数
1.在data中增加两个个变量
totalSecond:60//倒计时总秒数
second:60//当前秒数。默认相等,开定时器对它–
2.给“获取验证码”按钮添加点击事件,只有当两个变量相对都是60的时候才展示按钮和这个文字

1
<button @click="getCode">{{second===totalSecond?'获取验证码':second+'秒后重新发送'}}</button>

在mehods中提供getCode方法
先data中增加变量timer:null //定时器id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 获取短信验证码
getCode () {
//先判断定时器是否开着,切两个变量都等于60才可以开启倒计时
if (!this.timer&&this.second===this.totalSecond){
// 开启倒计时效果
this.timer=setInterval(() => {
this.second--
//小于1后停止定时器并归位
if(this.second<=0){
clearInterval(this.timer)
this.timer=null
this.second=this.totalSecond
}
}, 1000)
}
}

成功后打开login页面能够实现点击按钮后开始倒计时的效果
3.最后记得在methods下面写销毁:

1
2
3
4
destroyed () {
// 离开页面清楚定时器
clearInterval(this.timer)
}
验证码请求校验处理

理解:在倒计时之前的校验处理(对手机号,验证码),这两部分不能为空,而且手机号格式要正确,验证码要和图片匹配
实现:
1.先提供两个变量并给输入框 v-model 绑定变量

1
2
3
4
5
6
7
8
9
data () {
return {
mobile: '', // 手机号
picCode: '' // 图形验证码
}
},

<input v-model="mobile" class="inp" maxlength="11" placeholder="请输入手机号码" type="text">
<input v-model="picCode" class="inp" maxlength="5" placeholder="请输入图形验证码" type="text">

2.封装一个方法判断是否合法,同时添加轻提示:

1
2
3
4
5
6
7
8
9
10
11
12
 // 校验电话和图形验证码输入框内容
validFn () {
if (!/^1[3-9]\d{9}$/.test(this.mobile)) {
this.$toast('请输入正确的手机号')
return false
}
if (!/^\w{4}$/.test(this.picCode)) {
this.$toast('请输入正确的图形验证码')
return false
}
return true
}

3.完善倒计时函数,请求倒计时前进行电话号和验证码校验

1
2
3
4
5
6
7
8
9
// 获取短信验证码
async getCode () {
if (!this.validFn()) {
//如果没通过没必要往下走了直接return,通过会继续走
return
}
//后面代码不变
...
}

成功后是这样的alt text

封装调用短信验证请求接口,发送请求添加提示

去接口文档找-11登录-获取短信验证码

  1. 在api-login.js中封装接口
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 获取短信验证码
    export const getMsgCode = (captchaCode, captchaKey, mobile) => {
    return request.post('/captcha/sendSmsCaptcha', {
    //下面的和上面的地址都根据接口文档写
    form: {
    captchaCode,
    captchaKey,
    mobile
    }
    })
    }
    同时记得在login-index.vue中导入函数
    1
    import { getPiccode, getMsgCode } from '@/api/login'
  2. 调用接口,添加发送请求和轻提示
    在判断定时器可开始后面,开启倒计时前面增加发送获取短信验证码的请求,和发送成功的轻提示
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 获取短信验证码
    //!!!!!!!!!这里加async了!!!!!!!!!!!!!!!!
    async getCode () {
    if (!this.validFn()) {
    return
    }

    if (!this.timer && this.second === this.totalSecond) {
    // 在这里,判断定时器可开始后面,开启倒计时前面增加发送获取短信验证码的请求,和发送成功的轻提示

    // 这两行注释掉的是验证代码,检查有没有从接口获取成功,出现status:200就是成功
    // const res = await getMsgCode(this.picCode, this.picKey, this.mobile)
    // console.log(res)
    await getMsgCode(this.picCode, this.picKey, this.mobile)
    this.$toast('短信验证码发送成功,请注意查收')

    // 开启倒计时
    ...
    }
    }

登录功能

阅读接口文档,封装登录接口

接口文档-12.手机验证码登录
1.在login.js中封装方法
// 验证码登录接口

1
2
3
4
5
6
7
8
9
10
export const codeLogin = (mobile, smsCode) => {
return request.post('/passport/login', {
form: {
isParty: false,c
mobile,
partyData: {},
smsCode
}
})
}
登录前的校验(手机号,图形验证码,短信验证码)

2.去.vue中调用接口,给登录按钮注册点击事件

1
2

<div class="login-btn" @click="login">登录</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//记得先导入
import { getPiccode, getMsgCode, codeLogin } from '@/api/login'

//添加data
msgCode: ''// 短信验证码
//methods添加
// 登录校验
async login () {
if (!this.validFn()) {
return
}
if (!/^\d{6}$/.test(this.msgCode)) {
this.$toast('请输入正确的手机验证码')
return
}
//调用方法,发送给请求 ,成功添加提示并跳转
// 测试,控制台看是不是200,,但我这里没成功,测试代码有问题,不测试直接能正常跑
// const res2 =await codeLogin(this.mobile, this.msgCode)
// console(res2)
await codeLogin(this.mobile, this.msgCode)
this.$router.push('/') // 成功后跳转到首页
this.$toast('登录成功')
}

在写登录功能之前都挺快挺好的,我跟着写完登录功能后开始变得特别卡,十多秒才加载出来图片验证码,输入后登录说我验证码输出错误,无法成功登录
!!!!找到了一点原因,看报错信息是我没获取到输入的短信验证码,我没有给输入框做绑定,前面把下面这行代码丢掉了!!

1
<input class="inp" v-model="msgCode" maxlength="6" placeholder="请输入短信验证码" type="text">

但是还在在报错无法登录呢,控制台的报错没看懂,而且有时候能加载出图形验证码有时候不行了5s会超时我改成了50s,现在很卡
!!成功了,是我的登录功能的测试代码有问题,把测试代码注释掉就能正常跑了而且很快,不卡也不报错了
成功后是这样的alt text

响应拦截器统一处理错误提示(在最后有问题!!)

理解:每次请求都有成功和失败的可能,在代码中一般都是判断验证是否成功,而失败后也需要错误提示,希望通过响应拦截器把失败的情况统一处理
响应拦截器是拿到数据的第一个数据流转站,可以在这里面统一处理错误情况
再理解:上面的图形验证码部分已写的只判断了验证码格式,没有判断验证码和图片上的内容是否一致,这个事情是后端返回的,在控制台-网络-Response中后台有返回结果,返回的status=200说明一致,返回500说不匹配,message消息也有提示
现在希望这些不在页面中额外处理,而是通过拦截器进行判断:如果status不是200那么抛出一个错误,await只会等待成功的正确的promise

添加响应拦截器

那么要在resquest.js中添加响应拦截器,获取返回的数据,用结果做判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 添加响应拦截器
instance.interceptors.response.use(function (response) {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
const res = response.data
if (res.status !== 200) {
Toast(res.message)
// 抛出一个错误额的promise
return Promise.reject(res.message)
}
return res
}, function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error)
})

!!!我的这里还是不对的,根提供的标准代码比对找不出问题
现在的情况是,有时候进去控制台会报错验证码图片出不来,有时候就能正常运行。
不管我输入的验证码和图片是否匹配都会提示短信发送成功和进行60s倒计时。
只有输入完246810验证码点击登录的时候,才会判断我前面输入的图片验证码和图片是否匹配,而且提示的也不是中文是一堆英文东西
有时候不管输入的对不对都出来一堆英文,网络-响应-看不到信息
问题是批片验证码匹配应该作用在发送短信验证码这里而不是登录这里啊,找半天找不出错误原因,接口文档看了都对应着
现在是保留上面的问题但是如果图片验证和短信验证码都输入正确的话能够成功登录
!!主要问题还是不能判断图片验证码输入正确与否后提供短信验证码,短信验证码一直能提供!!!

登录页逻辑功能完善

登录权证信息存储

理解:在登录成功后,返回的data数据里面有token和userID的信息,这些是登录权证,很重要,是token是请求一些接口时候必须携带的凭证,如果没有携带接口请求可能有问题,userID是用户的唯一标识。所以这两个东西都应该存储起来,以前是存储到本地,但是为了方便,工作中更应该存储到vuex里面米娜,这样是响应式,而且任何组件中都可以非常快速地拿到存储的信息,好获取
在这里我们把登录返回成功的登录权证以对象的形式存储到vuex中的user模块
步骤:构建user模块=》挂载到vuex=》提供共mutatios=》在页面中commit调用

登录权证信息存储操作

1.构建user模块
store文件夹-新建modules文件夹-user.js文件
注意,vuex中状态的提供state可以写成对象state(){}也可以写成冒号state:{},为了保证数据独立性通常写成对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export default {
namespaced: true,
//回顾一下:state提供数据;utations提供数据修改操作;actions提供异步;getters提供基于state派生出来的属性
state () {
return {
// 个人权证相关
userInfo: {
token: '',
userId: ''
}
}
},
mutations: {},
actions: {},
getters: {}
}

2.挂载到 vuex 上
在store文件夹-index.js文件中挂载

1
2
3
4
5
6
7
8
9
10
11
import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'

Vue.use(Vuex)

export default new Vuex.Store({
modules: {
user,
}
})

挂载成功后就可以在控制台看了
成功后是这样的alt text
上面user旁边的namespaced这个东西,有它代表开启了命名空间,说明它和别的是独立的(否则就是公用的)

3.提供一个存储个人权证信息的mutations
user.js

1
2
3
4
5
6
mutations: {
// 所有mutations的第一个参数,都是state
setUserInfo (state, obj) {
state.userInfo = obj
}
}

4.在login-index.vue页面中commit调用
在登录成功拿到res后,基于res进行提交

1
2
3
4
5
6
7
8
9
10
11
12
// 登录按钮(校验 & 提交)
async login () {
if (!this.validFn()) {
return
}
...
const res = await codeLogin(this.mobile, this.msgCode)
//下面这一行是新增的
this.$store.commit('user/setUserInfo', res.data)
this.$router.push('/')
this.$toast('登录成功')
}

成功后是这样的alt text

vuex持久化处理

理解:刚才已经能够把用户信息存入vuex中,但问题是一刷新信息就会消失,要想让数据一直存在,要封装storage存储模块,利用本地存储进行持久化处理
在storage模块封装函数,每次用的时候只需要调用,能够简化代码

vuex持久化处理操作

utils文件夹-新建storage.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 约定一个通用的键名
const INFO_KEY = 'shopping_info'

// 获取个人信息
export const getInfo = () => {
const defaultObj = { token: '', userId: '' }
const result = localStorage.getItem(INFO_KEY)
return result ? JSON.parse(result) : defaultObj
}

// 设置个人信息
export const setInfo = (obj) => {
localStorage.setItem(INFO_KEY, JSON.stringify(obj))
}

// 移除个人信息
export const removeInfo = () => {
localStorage.removeItem(INFO_KEY)
}

2.在user.js中,存入vuex中的同时存入到本地,刷新页面后虽然vuex的丢失了,但是可以去本地读取信息,来初始化vuex的起始数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//首先导入方法
import { getInfo, setInfo } from '@/utils/storage'
//然后增加userInfo: getInfo()和setInfo(obj)
state () {
return {
// 个人权证相关
// userInfo: {
// token: '',
// userId: ''
// }
// 上面的不要了,改进为从本地信息获取vuex的初始化信息
userInfo: getInfo()
}
},
mutations: {
// 所有mutations的第一个参数,都是state
setUserInfo (state, obj) {
state.userInfo = obj
// 存入vuex的同时存入本地
setInfo(obj)
}
},

添加请求loading效果

理解:有时候网不好,所以一次请求的结果可能需要一段时间后才能回来,此时给用户添加loading提示会更友好
好处:
1.节流处理:防止用户再一次请求还没回来之前,多次进行点击,发送无效请求,有利于网站性能
2.友好提示:告知用户,目前是在加载中,请耐心等待,用户体验会更好

loading操作

1.在请求拦截器中,每次请求,打开Loading
utils-request.js请求拦截器中写
在vant2文档-Toast轻提示-右侧加载提示,把文档加载提示的代码丢到请求拦截器中,同时根据文档配置一下loading图标

1
2
3
4
5
6
7
8
9
10
instance.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
// 开启loading,禁止背景点击 (节流处理,防止多次无效触发)
Toast.loading({
message: '加载中...',
forbidClick: true, // 禁止背景点击
loadingType: 'spinner', // 配置loading图标
duration: 0 // 不会自动消失,如果不配置默认两秒自动消失
})
}

成功后是这样的alt text
因为一进入页面就会触发获取验证码图片的请求所以会触发加载中,而设置了duration: 0 不会自动消失,如果不配置默认两秒自动消失

2.在响应拦截器收到信息之后关闭掉loading,还是去官网找文档-API中有方法-Toast.clear关闭提示

注意:Toast默认是单例模式,后面的Toast调用了会将前一个Toast效果覆盖,同时只会存在一个toast效果

1
2
3
4
5
//在响应拦截器中
else {
// 正确情况,直接走业务核心逻辑,清除loading效果
Toast.clear()
}

页面访问拦截-路由前置守卫

理解:对于这个项目来说,大部分页面游客都可以直接访问,但是有些支付页面,订单页面这种,和用户强绑定的页面,游客不可以访问,只有登录后的用户才可以访问,这时候就需要做拦截处理

路由导航守卫文档地址 - 全局前置守卫

1.所有的路由一旦被匹配到,都会先经过全局前置守卫
2.只有全局前置守卫放行,才会真正解析渲染组件,才能看到页面内容
语法:官网文档有
当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中
当访问权限页面时,拦截或放行的关键点:用户是否有登录权证token
在router文件夹-index.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 全局前置导航守卫
import store from '@/store'
const router = new VueRouter({ ... })
// 全局前置导航守卫,注意这三个量的位置顺序不能变

// 所有的路由在真正被访问到之前(解析渲染对应组件页面前),都会先经过全局前置守卫
// 只有全局前置守卫放行了,才会到达对应的页面

// 全局前置导航守卫
// to: 到哪里去,到哪去的完整路由信息对象 (路径,参数)
// from: 从哪里来,从哪来的完整路由信息对象 (路径,参数)
// next(): 是否放行
// (1) next() 直接放行,放行到to要去的路径
// (2) next(路径) 进行拦截,拦截到next里面配置的路径

// 定义一个数组,专门用户存放所有需要权限访问的页面
const authUrls = ['/pay', '/myorder']

router.beforeEach((to, from, next) => {
// console.log(to, from, next)
// 看 to.path 是否在 authUrls 中出现过
if (!authUrls.includes(to.path)) {
// 非权限页面,直接放行
next()
return
}
// 是权限页面,需要判断token
const token = store.getters.token
if (token) {
next()
} else {
next('/login')
}
})

成功后未登录访问/pay链接会自动跳转到login页面

首页

首页-静态结构

1.views/layout/home.vue静态布局使用的已给代码cv
2.封装商品组件,因为在很多地方都要用到商品组件。在components文件夹新建GoodsItem.vue商品组件,记得都要给name
此时运行F12会报错,如Unknown custom element: 因为这里使用了vant组件但是没有注册,要进行按需导入
3.在utils/vant-ui.js按需导入组件

1
2
3
4
5
6
7
import { Search, Swipe, SwipeItem, Grid, GridItem } from 'vant'

Vue.use(GridItem)
Vue.use(Search)
Vue.use(Swipe)
Vue.use(SwipeItem)
Vue.use(Grid)

成功后是这样的alt text

首页-封装接口

1.在接口文档找到对应后端地址和所需参数
在api文件夹新建home.js

1
2
3
4
5
6
7
8
9
10
11
12
// 此处用于存放所有首页相关的接口请求
import request from '@/utils/request'

// 获取首页数据
export const getHomeData = () => {
// get请求的参数传参注意
return request.get('/page/detail', {
params: {
pageId: 0
}
})
}

2.在页面中调用
先导入,然后created里面发请求
成功后是这样的alt text
然后从后台返回的数据data中解构出pageData

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import GoodsItem from '@/components/GoodsItem.vue'
import { getHomeData } from '@/api/home'
export default {
name: 'HomePage',
components: {
GoodsItem
},
data () {
return {
bannerList: [], //轮播图
navList: [], //导航
proList: [] //商品
}
},
async created () {
//从后台返回的数据data中解构出pageData,下面的1,3,6是后端接口返回的数据数组上标注的
const { data: { pageData } } = await getHomeData()
//轮播图
this.bannerList = pageData.items[1].data
//导航
this.navList = pageData.items[3].data
//商品
this.proList = pageData.items[6].data
}
}
  1. 轮播图、导航、猜你喜欢渲染
    布置静态页面的时候那些图片都是写死了的,现在把这些改为后端传过来的数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    <!-- 轮播图 -->
    <van-swipe class="my-swipe" :autoplay="3000" indicator-color="white">
    <van-swipe-item v-for="item in bannerList" :key="item.imgUrl">
    <img :src="item.imgUrl" alt="">
    </van-swipe-item>
    </van-swipe>

    <!-- 导航 -->
    <van-grid column-num="5" icon-size="40">
    <van-grid-item
    v-for="item in navList" :key="item.imgUrl"
    :icon="item.imgUrl"
    :text="item.text"
    @click="$router.push('/category')"
    />
    </van-grid>

    <!-- 猜你喜欢 -->
    <div class="guess">
    <p class="guess-title">—— 猜你喜欢 ——</p>

    <div class="goods-list">
    <GoodsItem v-for="item in proList" :item="item" :key="item.goods_id"></GoodsItem>
    </div>
    </div>

    成功后是这样的alt text

  2. 商品组件动态渲染
    原来首页静态布局商品组件的图片是多个一样的商品重复,现在把写死的商品改为后端接口提供回来的数据
    成功后是这样的,每个商品都不一样alt text

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    <template>
    <div v-if="item.goods_name" class="goods-item" @click="$router.push(`/prodetail/${item.goods_id}`)">
    <div class="left">
    <img :src="item.goods_image" alt="" />
    </div>
    <div class="right">
    <p class="tit text-ellipsis-2">
    {{ item.goods_name }}
    </p>
    <p class="count">已售 {{ item.goods_sales }}件</p>
    <p class="price">
    <span class="new">¥{{ item.goods_price_min }}</span>
    <span class="old">¥{{ item.goods_price_max }}</span>
    </p>
    </div>
    </div>
    </template>

    <script>
    export default {
    props: {
    item: {
    type: Object,
    default: () => {
    return {}
    }
    }
    }
    }
    </script>

搜索页

搜索页面和历史搜索记录

  1. views/search/index.vue 搜索页面静态布局:用给的cv的
  2. 把组件按需导入,导入Icon组件
  3. data 中提供数据,和搜索框双向绑定 (实时获取用户内容)
1
2
3
4
5
6
7
8
9
10
11
data () {
return {
search: ''
}
}

<van-search v-model="search" show-action placeholder="请输入搜索关键词" clearable>
<template #action>
<div>搜索</div>
</template>
</van-search>
  1. 准备假数据,进行基本的历史纪录渲染
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
data () {
return {
...
history: ['手机', '空调', '白酒', '电视']
}
},

<div class="search-history" v-if="history.length > 0">
...
<div class="list">
<div v-for="item in history" :key="item" @click="goSearch(item)" class="list-item">
{{ item }}
</div>
</div>
</div>
  1. 点击搜索,或者下面搜索历史按钮,都要进行搜索历史记录更新 (去重,新搜索的内容置顶)
    搜索有两种方法:在搜索框输入关键词搜索和点击历史搜索记录进行搜索,搜索后进行判断,看看这个关键词在历史记录里有没有出现过,如果出现过那么先删除掉之前的历史记录再把这个添加到最前面,如果没有出现过就直接添加到最前面

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <div @click="goSearch(search)">搜索</div>

    <div class="list">
    <div v-for="item in history" :key="item" @click="goSearch(item)" class="list-item">
    {{ item }}
    </div>
    </div>

    goSearch (key) {
    const index = this.history.indexOf(key)
    if (index !== -1) {
    this.history.splice(index, 1)
    }
    this.history.unshift(key)
    this.$router.push(`/searchlist?search=${key}`)
    }
  2. 清空历史

    1
    2
    3
    4
    5
    <van-icon @click="clear" name="delete-o" size="16" />

    clear () {
    this.history = []
    }

    最后完成是这样的alt text

搜索历史的持久化

目前只要刷新那么搜索历史会重置,要将用户的搜索历史进行持久化处理,往utils文件夹/storage.js中放

  1. 持久化到本地 - 封装方法
1
2
3
4
5
6
7
8
9
10
11
12
const HISTORY_KEY = 'hm_history_list'

// 获取搜索历史
export const getHistoryList = () => {
const result = localStorage.getItem(HISTORY_KEY)
return result ? JSON.parse(result) : []
}

// 设置搜索历史
export const setHistoryList = (arr) => {
localStorage.setItem(HISTORY_KEY, JSON.stringify(arr))
}
  1. 页面中调用 - 实现持久化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
data () {
return {
search: '',
history: getHistoryList()
}
},
methods: {
goSearch (key) {
...
setHistoryList(this.history)
this.$router.push(`/searchlist?search=${key}`)
},
clear () {
this.history = []
setHistoryList([])
this.$toast.success('清空历史成功')
}
}

成功后刷新也不会丢失之前的搜索历史
还可以改进:如果搜索框内容为空那么不可以搜索

搜索列表页

根据搜索的关键字进入搜索列表页,展示搜索的详情,基于关键字进行渲染

页面静态布局和动态渲染

静态布局用的给的代码cv,在search文件夹/list.vue
在api新建文件夹product.js

  1. 计算属性,基于query 解析路由参数
1
2
3
4
5
computed: {
querySearch () {
return this.$route.query.search
}
}
  1. 根据不同的情况,设置输入框的值
1
2
3
4
<van-search
...
:value="querySearch || '搜索商品'"
></van-search>
  1. api/product.js 封装接口,获取搜索商品
1
2
3
4
5
6
7
8
9
10
11
12
13
import request from '@/utils/request'

// 获取搜索商品列表数据
export const getProList = (paramsObj) => {
const { categoryId, goodsName, page } = paramsObj
return request.get('/goods/list', {
params: {
categoryId,
goodsName,
page
}
})
}
  1. 页面中基于 goodsName 发送请求,动态渲染
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
data () {
return {
page: 1,
proList: []
}
},
async created () {
const { data: { list } } = await getProList({
goodsName: this.querySearch,
page: this.page
})
this.proList = list.data
}

<div class="goods-list">
<GoodsItem v-for="item in proList" :key="item.goods_id" :item="item"></GoodsItem>
</div>

最后完成是这样的alt text

商品分类搜索页面

1 封装接口 api/category.js

1
2
3
4
5
6
import request from '@/utils/request'

// 获取分类数据
export const getCategoryData = () => {
return request.get('/category/list')
}

2 分类页静态结构
layout/category.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<template>
<div class="category">
<!-- 分类 -->
<van-nav-bar title="全部分类" fixed />

<!-- 搜索框 -->
<van-search
readonly
shape="round"
background="#f1f1f2"
placeholder="请输入搜索关键词"
@click="$router.push('/search')"
/>

<!-- 分类列表 -->
<div class="list-box">
<div class="left">
<ul>
<li v-for="(item, index) in list" :key="item.category_id">
<a :class="{ active: index === activeIndex }" @click="activeIndex = index" href="javascript:;">{{ item.name }}</a>
</li>
</ul>
</div>
<div class="right">
<div @click="$router.push(`/searchlist?categoryId=${item.category_id}`)" v-for="item in list[activeIndex]?.children" :key="item.category_id" class="cate-goods">
<img :src="item.image?.external_url" alt="">
<p>{{ item.name }}</p>
</div>
</div>
</div>
</div>
</template>

<script>
import { getCategoryData } from '@/api/category'
export default {
name: 'CategoryPage',
created () {
this.getCategoryList()
},
data () {
return {
list: [],
activeIndex: 0
}
},
methods: {
async getCategoryList () {
const { data: { list } } = await getCategoryData()
this.list = list
}
}
}
</script>

3 搜索页,基于分类 ID 请求

1
2
3
4
5
6
7
8
async created () {
const { data: { list } } = await getProList({
categoryId: this.$route.query.categoryId,
goodsName: this.querySearch,
page: this.page
})
this.proList = list.data
}

最后完成是这样的alt text

商品详情页面

和之前一样:页面静态结构-封装接口-动态路由获取参数-基于ID获取数据进行渲染
api文件夹新建product.js,cart.js
views文件夹/prodetail文件夹/index.vue
mixins文件夹,components文佳佳GoodsItem.vue

加入购物车页面

加入购物车弹层

用户浏览商品可能要购买,当用户点击加入购物车或立刻购买时,能够弹出一个小的弹层

  1. 准备弹层,点击能够被唤起
    在vant官网找到反馈组件-ActionnSheet 动作面板,右侧有弹层效果展示,我们用自定义面板。
    不管用哪个都需要先按需引入ActionSheet(utils/vant-ui.js),然后往下找到自定义面板,通过插槽可以自定义面板的展示内容。
    回到代码,views/prodetail/index.vue,在底部div下面添加加入购物车的弹层(复制文档代码)
    给加入购物车和立即购买两个按钮添加点击事件,在下面methds提供方法,在data中提供一个参数showPannel,这个参数值变为true则唤起弹层
    再在data中声明一个参数mode,点击不同按钮触发的方法分别给mode赋值为’cart’和’buyNow’,根据mode的不同值来动态设置弹层标题的文字现实内容
  2. 完善弹层结构
    直接CV的视频提供的代码修改弹层的静态结构和弹层的样式
  3. 结合数据动态渲染弹层
    弹层中的商品图片goods_image,商品购买价格goods_price_min,库存stock_total
    在静态结构中把这些写死的地方改为
    对库存进行判断,有库存才现实提交按钮,没有库存的话提交按钮变成灰色现实文字“该商品已抢完”

弹层优化–封装数字框组件

  1. 理解
    再加入购物车和购物车列表中都会用到数字狂组件,所以把弹层中的数字框组件进行封装
    组件名:CountBox
    静态结构:左中右三部分
    数字框的数字,应该是外部传递进来的(父传子)
    点击+-号,可以修改数字(子传父)
    使用 v-model 实现封装(:value 和 @input 的简写)(父传子和子传父简写)

数字框组件:点击减号加号数字减一加一,同时点击数字能够直接修改为输入的值,也要对数字和库存数量进行判断,最大不超过库存数量
2. 实现
在components文件夹创建组件CountBox.vue,在里面写静态结构,然后把这个组件加入到弹窗中对应的位置上
数字框的默认值应该是父亲给传递过来的,在商品详情页prodetail/index.vue中加入data,addCount给默认值1,作为数字框的默认值,也是数字框绑定的数据
点击加减号都可以向上提交数据,向父组件提交input事件,同时判断数据不能<=1
用户自输入的数据进行判断:在数据变化时拿到数据和库存进行判断,对它进行转数字处理,如果是合法的数字那么继续走,如果是不合法的数字或者<1的数,那么回退成原来的value值

判断token添加登录提示

  1. 理解:加入购物车这个功能只能是已经登录的用户才能够操作,如果是未登录的用户,那么给他登录提示引导他到登陆页,登录完成后再跳转回来;如果用户token存在,俺么继续加入购物车操作
  2. 操作:在prodetail/index.vue里面添加函数判断token是否存在,这个函数在用户点击加入购物车弹窗里的提交时会触发
    这个判断操作封装到了mixins/loginConfirm.js文件中的loginConfirm ()函数下,在页面中导入直接调用函数进行判断

添加商品到购物车

  1. 理解:封装接口,进行加入购物车的请求
    在api/cart.js中封装接口,在页面中调用接口,同时在购物车图标右上角渲染购物车内商品数量的红色角标
  2. 操作:根据后端结构传入参数
    在utils/requers.js中添加代码
    1
    2
    3
    4
    5
    6
    7
    8
    import store from '@/store'
    // 只要有token,就在请求时携带,便于请求需要授权的接口
    const token = store.getters.token
    if (token) {
    config.headers['Access-Token'] = token
    config.headers.platform = 'H5'
    }

购物车页面

  1. 理解:购物车模块的数据库比较多,通常会封装一些小组件(商品组件,数字框组件),为了便于维护,把购物车的数据基于vuex分模块管理
  2. 需求分析
    基本静态结构(快速实现)
    构建 vuex cart 模块,获取数据存储
    基于数据动态渲染购物车列表
    封装getters实现动态统计
    全选反选功能
    数字框修改数量功能
    编辑切换状态,删除功能
    空购物车处理
  3. 操作
    (1)先完成购物车静态结构,然后在store/modules文件夹建立cart.js文件,挂载到index.js中,成功后在F12vue中Root除了User还能看到Cart模块

(2)获取购物车列表是异步的放在actions里面
前提:必须是登陆过的用户才能获取购物车列表
注意:这里发请求是异步的,主要原因是为了避免阻塞页面的渲染和交互。如果前端发起请求是同步的,那么在请求返回之前,浏览器会一直等待,页面就会被阻塞,用户无法进行其他操作,这会给用户带来不好的体验。
同时要手动维护数据,给每一项添加一个isChecked状态,来标记当前商品是否选中,默认值true选中
mutations里面提供一个设置购物车列表cartList的mutation

报错!!

登录页面发送验证码后控制台报错

Uncaught (in promise) Error: A listener indicated an asynchronous response by returning true, but the message channel closed before a response was received
未捕获(在 promise 中)错误:一个监听器通过返回 true 指示了异步响应,但在收到响应之前,消息通道已关闭。
已解决:我将axios响应时间的50s改成5s后报错消失

登录点击后报错

vue.runtime.esm.js:3065 SQLSTATE[23000]: Integrity constraint violation: 1048 Column ‘platform’ cannot be null
Vue.runtime.esm.js:3065 错误信息:SQLSTATE[23000]:实体完整性违反:1048 列 ‘platform’ 不能为空
终于解决了!!!!!!!
原因是在request.js文件中的axios这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 导入axios
import axios from 'axios'
import { Toast } from 'vant'
// 创建axios实例,对创建出来的实例进行自定义配置
// 这样的好处是不会污染原始的axios实例
// 创建实例代码在axios官方文档cv
const instance = axios.create({
// baseURL看接口文档的基础地址更改
baseURL: 'http://smart-shop.itheima.net/index.php?s=/api',
// 超时时间,这里是5s
timeout: 5000,
// 视频和接口文件有下面这行代码,但是我的测试的时候报错了,看别人发的把这行删掉,然后就成功啦
// headers: { 'X-Custom-Header': 'foobar' }
headers: {
platform: 'H5'
}
})
其中 headers: {
platform: 'H5'
}
加上就不会再报错了

具体的错误原因是尝试将NULL值插入到不允许为空(NOT NULL约束)的platform列中

未获取到对应mp4文件

0a2fmquapKejn62L4K2doafhoaWlmqmeoaGk.mp4:1
GET https://m4.pptvyun.com/pvod/e11a0/dy2NW_VbPWwd4odtyKM_KELVHXM/eyJkbCI6MTY3Njg3OTgzNywiZXMiOjYwNDgwMCwiaWQiOiIwYTJmbXF1YXBLZWpuNjJMNEsyZG9hZmhvYVdsbXFtZW9hR2siLCJ2IjoiMS4wIn0/0a2fmquapKejn62L4K2doafhoaWlmqmeoaGk.mp4 502 (Bad Gateway)

未注册组件报错

F12报错
Unknown custom element: - did you register the component correctly? For recursive components, make sure to provide the “name” option.
未知自定义元素: -您是否正确注册了该组件?对于递归组件,确保提供“name”选项

解决:是我的vant-ui.js没有注册使用对应组件Rate