#在Github新建Vue-MintShop项目,然后clone到本地
git clone [email protected]:W-Qing/Vue-MintShop.git
cd Vue-MintShop
#创建客户端项目
vue init webpack mintshop-client
cd mintshop-client
npm install
npm run dev 访问: localhost:8080
**MintShop-client **
- |-- build : webpack 相关的配置文件夹(基本不需要修改)
- |-- build : webpack 相关的配置文件夹(基本不需要修改)
- |-- config: webpack 相关的配置文件夹(基本不需要修改)
- |-- index.js: 指定的后台服务的端口号和静态资源文件夹
- |-- node_modules
- |-- src : 源码文件夹
- |-- main.js: 应用入口 js (初始化vue实例并使用需要的插件 )
- |-- static: 静态资源文件夹
- |-- .babelrc: babel 的配置文件
- |-- .editorconfig: 通过编辑器的编码/格式进行一定的配置
- |-- .eslintignore: eslint 检查忽略的配置
- |-- .eslintrc.js: eslint 检查的配置
- |-- .gitignore: git 版本管理忽略的配置
- |-- index.html: 默认的主渲染页面文件
- |-- package.json: 应用包配置文件
- |-- README.md: 应用描述说明的 readme 文件
编码测试
npm run dev
访问: http://localhost:8080
编码, 自动编译打包(HMR), 查看效果
打包发布
npm run build
npm install -g serve
serve dist
访问: http://localhost:5000
在项目主目录下的static文件夹内新建css文件夹
在css文件夹内新建重置样式文件reset.css
在index.html 中引入
<link rel="stylesheet" href="/static/css/reset.css">
当用户一次点击屏幕之后,浏览器并不能立刻判断用户是要进行双击缩放,还是想要进行单击操作。因此,iOS Safari 就等待 300 毫秒,以判断用户是否再次点击了屏幕。 于是,300 毫秒延迟就这么诞生了。
安装fastclick库 解决点击响应延时 0.3s 问题
npm Install fastclick --save
在main.js中引入,并绑定到body
import FastClick from 'fastclick'
FastClick.attach(document.body);
安装stylus依赖包
npm install stylus stylus-loader --save-dev
在common文件夹下新建stylus文件夹
在stylus文件加下面新建mixins.styl文件
注意在组件内编写样式时要声明lang和rel
<style lang="stylus" rel="stylesheet/stylus">
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-naRIF3Rr-1615041312256)(http://owoccema2.bkt.clouddn.com/Readme/vue/mintshop.png)]
src
- |-- components------------非路由组件文件夹
- |-- FooterGuide---------------底部组件文件夹
- |-- FooterGuide.vue--------底部组件 vue
- |-- pages-----------------路由组件文件夹
- |-- Msite---------------首页组件文件夹
- |-- Msite.vue--------首页组件 vue
- |-- Search----------------搜索组件文件夹
- |-- Search.vue---------搜索组件 vue
- |-- Order--------------订单组件文件夹
- |-- Order.vue-------订单组件 vue
- |-- Profile--------------个人组件文件夹
- |-- Profile.vue-------个人组件 vue
- |-- App.vue---------------应用根组件 vue
- |-- main.js---------------应用入口 js
App vue template
###7.1 下载vue-router
#创建项目时已下载
npm install vue-router --save
/*
路由模块
*/
import Vue from 'vue'
import VueRouter from 'vue-router'
// 引入路由组件文件夹下的组件
import Msite from '../pages/Msite/Msite.vue'
import Search from '../pages/Search/Search.vue'
import Order from '../pages/Order/Order.vue'
import Profile from '../pages/Profile/Profile.vue'
// 全局注册Vue-router组件
Vue.use(VueRouter)
// 配置路由表并导出
export default new VueRouter({
//去掉地址中的哈希#
mode: 'history',
routes: [{
path: '/',
redirect: '/msite'
},
{
path: '/msite',
component: Msite,
},
{
path: '/search',
component: Search,
},
{
path: '/order',
component: Order,
},
{
path: '/profile',
component: Profile,
}
]
})
// 引入路由 其实就是引入上一步配置好的路由表
import router from './router'
new Vue({
el: '#app',
render: h => h(app),
// 为根组件加入路由
router
})
与外层被注入框架index.html中的
是一致的
是指定绑定目标为元素的根路径,而App.vue文件里的
则是提供注入绑定元素的内容,两者在运行时指的是同一个DOM元素通过切换url地址里的hash值(miste/order/search/profile),页面会显示不同的路由模板内容。
功能及实现
代码
<footer class="footer_guide border-1px">
<a href="javascript:;" class="guide_item on">
<span class="item_icon">
<i class="iconfont icon-food">i>
span>
<span>外卖span>
a>
footer>
此时,页面已达到理想效果。接着修改template模板,为其加入路由与样式的切换控制。
<div class="guide_item" @click="goto('/msite')" :class="{on: isCurrent('/msite')}">
<span class="item_icon">
<i class="iconfont icon-food">i>
span>
<span>首页span>
div>
再补充相应的函数方法
export default {
methods: {
goto (path) {
this.$router.replace(path)
},
isCurrent (path) {
// console.log(this.$route.path)
return this.$route.path === path
}
}
}
至此,底部组件完成,可实现点击不同的选项切换不同的路由组件。
功能区域划分
图片资源
Msite组件页面的轮播图及商家列表都需要用到一些图片资源文件,所以在msite.vue同级目录下新建images文件夹,以便放置各种不同类型的图片资源。
代码
芝罘区鲁东大学北区(青年南路)
登录|注册
**要注意首页的头部标题部分的样式,在其他的组件中都可以进行重用。**所以将header标签的类名由msite_header改成header。接下来在其他组件中可以直接使用(当然header里的部分样式其他组件用不到,到时再进一步抽取公共的css样式。)
接下来的几个路由组件都类似,都是先修改template模版,然后引入mixins.styl 样式文件和上面提到的公共的header部分的样式。
搜索
在order.vue同级目录下新建images文件夹,再新建order文件夹,存放订单组件用到的图片资源。
订单列表
登录后查看外卖订单
{
{title}}
//Msite、Order、Search、Profile中都要引入注册才能使用
import HeaderTop from '../../components/HeaderTop/HeaderTop.vue'
export default {
components: {
HeaderTop
}
}
然后使用
标签设置这个头部组件
这里以Msite.vue为例,先删除静态模版里的Header部分,替换成HeaderTop组件
登录|注册
下载安装: npm install swiper --save
Msite.vue的HTML部分:
<div class="swiper-container">
<div class="swiper-wrapper">
<div class="swiper-slide">1div>
<div class="swiper-slide">2div>
<div class="swiper-slide">3div>
div>
<div class="swiper-pagination">div>
div>
script部分引入并初始化:
<script>
import Swiper from 'swiper'
//同时引入swiper的 css文件
import 'swiper/dist/css/swiper.min.css'
export default {
//注意要在页面加载完成之后(mounted)再进行swiper的初始化
mounted () {
//创建一个swiper实例来实现轮播
new Swiper('.swiper-container', {
autoplay: true,
// 如果需要分页器
pagination: {
el: '.swiper-pagination',
clickable: true
}
})
}
}
</script>
具体用法参考Swiper官方文档
部分及相应的stylus样式代码移动到新建的ShopList.vue组件资源文件准备
配置路由跳转
<a href="javascript:" class="profile-link">
...
a>
<router-link to="/Login" class="profile-link">
...
router-link>
编写Login.vue代码
@click="$router.back()"
实现点击页面的箭头返回上一级路由/Profile的功能实现控制Footer的显示隐藏
已确定底部的四个路由组件需要显示Footer部分
而Login组件为一级路由组件,且不需要显示底部的FooterGuide导航组件
所以为路由组件添加meta元数据来标识是否显示Footer
{
path: '/msite',
component: Msite,
meta: {
showFooter: true
}
},
/*Order、Searh、Profile组件都要添加meta*/
在App.vue组件中通过代表当前路由的$route
就能得到添加的meta属性,然后根据属性值来确定是否显示FooterGuide组件
<FooterGuide v-show="$route.meta.showFooter">FooterGuide>
其他细节
注意到一个问题,在一个路由组件(Msite)将页面下拉,再切换到其他路由组件(Profile),页面不会自动回到顶部。
/*解决方法 其他页面中类似*/
.profile
width 100%
/*添加一行overflow hidden*/
overflow hidden
npm start
具体API文档详见mintshop-server/API.md,然后可以使用Postman来进行接口测试
npm install axios--save
/*
ajax 请求函数模块
*/
import axios from 'axios'
/**
* 向外部暴漏一个函数 ajax
* @param {*} url 请求路径,默认为空
* @param {*} data 请求参数,默认为空对象
* @param {*} type 请求方法,默认为GET
*/
export default function ajax(url = '', data = {
}, type = 'GET') {
// 返回值 Promise对象 (异步返回的数据是response.data,而不是response)
return new Promise(function (resolve, reject) {
//(利用axios)异步执行ajax请求
let promise // 这个内部的promise用来保存axios的返回值(promise对象)
if (type === 'GET') {
// 准备 url query 参数数据
let dataStr = '' // 数据拼接字符串,将data连接到url
Object.keys(data).forEach(key => {
dataStr += key + '=' + data[key] + '&'
})
if (dataStr !== '') {
dataStr = dataStr.substring(0, dataStr.lastIndexOf('&'))
url = url + '?' + dataStr
}
// 发送 get 请求
promise = axios.get(url)
} else {
// 发送 post 请求
promise = axios.post(url, data)
}
promise.then(response => {
// 成功回调resolve()
resolve(response.data)
})
.catch(error => {
// 失败回调reject()
reject(error)
})
})
}
/*与后台交互模块 (依赖已封装的ajax函数)
*/
import ajax from './ajax'
/**
* 获取地址信息(根据经纬度串)
* 这个接口的经纬度参数是在url路径里的,没有query参数
*/
export const reqAddress = geohash => ajax(`/position/${
geohash}`)
/**
* 获取 msite 页面食品分类列表
*/
export const reqCategorys = () => ajax('/index_category')
/**
* 获取 msite 商铺列表(根据query参数:经纬度)
* 将经纬度两个数据作为一个参数对象传入
* 也可以两个数据分别传入ajax, 然后再放入一个对象参数内, 如下面的手机号验证码接口
*/
export const reqShops = ({
latitude,
longitude
}) => ajax('/shops', {
latitude,
longitude
})
/**
* 账号密码登录
*/
export const reqPwdLogin = (name, pwd, captcha) => ajax('/login_pwd', {
name,
pwd,
captcha
}, 'POST')
/**
* 获取短信验证码
*/
export const reqSendCode = phone => ajax('/sendcode', {
phone
})
/**
* 手机号验证码登录
*/
export const reqSmsLogin = (phone, code) => ajax('/login_sms', {
phone,
code
}, 'POST')
/**
* 获取用户信息(根据会话)
*/
export const reqUser = () => ajax('/userinfo')
/*
* 请求登出
*/
export const reqLogout = () => ajax('/logout')
问题分析:
目前为止运行的所有页面都是静态页面
接下来先测试使用封装的ajax接口请求函数来异步获取数据
// 先在App.vue中引入封装的接口函数
import {
reqCategorys} from './api'
// 然后再调用接口,测试打印数据
export default {
async mounted () {
const result = await reqCategorys()
console.log(result)
},
components: {
FooterGuide
}
}
打开浏览器,运行项目会报错GET http://local:4000/index_category 404(Not Found)
此时想起后端API端口为4000,然后在api文件夹下的index.js中修改测试
// 定义BASE_URL
const BASE_URL = 'http://local:4000'
// 然后修改请求接口的url
export const reqCategorys = () => ajax(BASE_URL + '/index_category')
然后再打开项目,发现依然报错access-control-allow-origin
提示请求为跨域请求
配置代理并测试接口:
// Paths
// 静态资源文件夹
assetsSubDirectory: 'static',
// 发布路径
assetsPublicPath: '/',
// 代理配置表,在这里可以配置特定的请求代理到对应的API接口
// 例如将'localhost:8080/api/xxx'代理到'www.example.com/api/xxx'
proxyTable: {
'/api': {
// 匹配所有以 '/api'开头的请求路径
target: 'http://localhost:4000', // 代理目标的基础路径
// secure: false, // 如果是https接口,需要配置这个参数
changeOrigin: true, // 支持跨域
pathRewrite: {
// 重写路径: 去掉路径中开头的'/api'
'^/api': ''
}
}
},
修改api文件夹index.js里接口函数的请求路径
// const BASE_URL = 'http://local:4000'
const BASE_URL = '/api'
export const reqAddress = geohash => ajax(`${
BASE_URL}/position/${
geohash}`)
export const reqCategorys = () => ajax(BASE_URL + '/index_category')
// 下面修改后的接口省略...
因为修改了项目的config文件,所以需要重启项目npm run dev
此时可以在控制台看到跨域请求到的数据{code: 0, data: Array(16)}
npm install vuex --save
用来管理从后台获取的状态数据/*
vuex最核心的管理对象store
*/
// 首先引入Vue及Vuex
import Vue from 'vue'
import Vuex from 'vuex'
// 引入四个基本模块
import state from './state'
import mutations from './mutations'
import actions from './actions'
import getters from './getters'
// 一定要声明使用插件
Vue.use(Vuex)
// 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件
export default new Vuex.Store({
state,
mutations,
actions,
getters
})
/*
状态对象 state
*/
export default {
latitude: 40.10038, // 纬度
longitude: 116.36867, // 经度
address: {
}, // 地址相关信息对象
categorys: [], // 食品分类数组
shops: [] // 商家数组
}
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation
每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)
我们可以使用常量替代 Mutation 事件类型,新建mutations-types文件
/*
包含n个mutation的type名称常量
*/
export const RECEIVE_ADDRESS = 'receive_address' // 接收地址信息
export const RECEIVE_CATEGORYS = 'receive_categorys' // 接收分类数组
export const RECEIVE_SHOPS = 'receive_shops' // 接收商家数组
然后在mutations.js文件内引入使用**(注意书写格式)**
/*
vuex 的 mutations 模块
*/
import {
RECEIVE_ADDRESS,RECEIVE_CATEGORYS,RECEIVE_SHOPS} from './mutation-types'
// [方法名](state,{param}){}
export default {
[RECEIVE_ADDRESS](state, {
address}) {
state.address = address
},
[RECEIVE_CATEGORYS](state, {
categorys}) {
state.categorys = categorys
},
[RECEIVE_SHOPS](state, {
shops}) {
state.shops = shops
}
}
Action 类似于 mutation,不同在于:
// Action:通过操作mutation间接更新state的多个方法的对象
// 注意要引入api接口函数
import {
reqAddress, reqCategorys, reqShops} from '../api'
import {
RECEIVE_ADDRESS, RECEIVE_CATEGORYS, RECEIVE_SHOPS} from './mutation-types'
export default {
// 异步获取地址
async getAddress ({
commit, state}) {
// 从state状态中获取到经纬度用来设置reqAddress的参数(看接口文档)
const geohash = state.latitude + ',' + state.longitude
// 1. 发送异步ajax请求
const result = await reqAddress(geohash)
// 2. 根据结果提交一个mutation
commit(RECEIVE_ADDRESS, {
address: result.data})
},
// 异步获取分类列表
async getCategorys ({
commit}) {
const result = await reqCategorys()
commit(RECEIVE_CATEGORYS, {
categorys: result.data})
},
// 异步获取商家列表
async getShops ({
commit, state}) {
// 对象的解构赋值
const {
latitude, longitude} = state
// 注意参数的顺序
const result = await reqShops({
latitude, longitude})
commit(RECEIVE_SHOPS, {
shops: result.data})
}
}
在项目中注册store
//项目的main.js文件
import store from './store'
new Vue({
store
})
测试异步获取当前地址数据
// 地址信息要尽早的获取,所以请求可以写在App.vue中
// 首先删除之前测试使用封装的ajax接口的代码
async mounted () {
// 通过this.$store.dispatch 方法触发调用Action
this.$store.dispatch('getAddress')
}
import {
mapActions} from 'vuex'
async mounted () {
this.getAddress()
}
methods: {
...mapActions(['getAddress'])
}
读取并显示获取到的当前地址数据
// 利用mapState语法糖去读取state对象
import {
mapState} from 'vuex'
computed: {
...mapState(['address'])
}
<HeaderTop :title="address.name">
// 将静态地址信息换成异步获取的地址数据 注意:title为绑定数据
</HeaderTop>
mounted方法中通过this.$store.dispatch调用Action来获取异步数据
// 忘记Actuon里对应的方法名时查看Action.js
mounted () {
this.$store.dispatch('getCategorys')
...
}
通过mapState语法糖来读取获取到的异步数据
// 忘记state名时查看State.js
computed: {
...mapState(['address', 'categorys'])
}
分析template结构并处理categorys数据
业务分析: 此时获取到的categorys是所有食品类别的一维数组,而如果要完成轮播图,需要将其变成这种
[[page1], [page2], [page3]]
二维数组categorysArr。比如本项目中有两页轮播图,一页为8个数据。应该把categorys数组处理成
[[data1 - 8], [data1 - 8]]
这种数据格式
//template
<div class="swiper-slide" v-for="(pages,index) in categorysArr" :key="index">
<a href="javascript:" class="(data,index) in pages" :key="index">
......
a>
div>
computed () {
...mapState(['address', 'categorys']),
/*
根据categorys一维数组生成一个2维数组
小数组中的元素个数最大是8
*/
categorysArr () {
// 1.先从当前组件中得到所有食品分类的一维数组
const {
categorys} = this
// 2.准备一个空的二维数组--categorysArr
const arr = []
// 3.准备一个小数组--pages(最大长度为8)
let minArr = []
// 4.遍历categorys得到处理后的二维数组catagorysArr
categorys.forEach(data => {
// 如果当前小数组(pages)已经满了, 创建一个新的
if (minArr.length === 8) {
minArr = []
}
// 如果minArr是空的, 将小数组(pages)保存到大数组(categorysArr)中
if (minArr.length === 0) {
arr.push(minArr)
}
// 将当前分类信息保存到小数组(pages)中
minArr.push(data)
})
return arr
}
}
注意第4步forEach里的逻辑顺序
1.为什么当minArr数组的长度为0时就将它与大数组关联起来,不是等它存满(8个)?
因为有可能categorys里的数据个数不为8的倍数,最后一个minArr内的数据不足8个。
2.为什么先判断minArr的长度为8的情况再判断等于0的情况?
因为先判断minArr的长度为0,将其放入大数组中与大数组关联起来,那么等到当前这个minArr填充完成之后新建的小数组则无法与大数组关联起来。
所以先判断长度为8的情况,再判断数组长度为0的情况。就可以确保之前的minArr填充完成后,新建的minArr都可以被放到大数组里与大数组关联起来。
categorysArr () {
const {
categorys} = this
const arr = []
for (let i = 0,len = categorys.length;i < len; i += 8){
arr.push(categorys.slice(i, i + 8))
}
return arr
}
将数据显示到页面上
// 因为食品分类的图片信息都有一个baseImageUrl所以在data里定义
data () {
return {
baseImageUrl: 'https://fuss10.elemecdn.com'
}
},
<div class="swiper-slide" v-for="(pages,index) in categorysArr" :key="index">
<a href="javascript:" class="link_to_food" v-for="(data,index) in pages" :key="index">
<div class="food_container">
<img :src="baseImageUrl+data.image_url">
div>
<span>{
{data.title}}span>
a>
div>
分页器Swiper其实应该是在轮播列表显示(即categorys数组有了数据)以后才初始化。
最开始categorys为空数组,有了数据才会显示轮播列表,而要监视categorys的数据变化,就要用到watch。
// 新建watch 监听categorys
watch: {
categorys (value) {
// categorys数组中有数据了
// 但界面还没有异步更新
}
}
// 删除mounted中的new Swiper...代码
但其实state里的状态数据改变(categorys接收数据)与异步更新界面(显示轮播列表)是两个步骤。所以需要等一等,界面完成异步更新后才可以进行Swiper的初始化。
// 使用setTimeout可以实现效果, 但是时机不准确
setTimeout(() => {
// 创建一个Swiper实例对象, 来实现轮播
new Swiper('.swiper-container', {
autoplay: true,
// 如果需要分页器
pagination: {
el: '.swiper-pagination',
clickable: true
}
})
}, 100)
利用vm.$nextTick( [callback] )
来实现等待界面完成异步更新就立即创建Swiper对象
// 在修改数据之后立即使用它,然后等待 DOM 更新。
this.$nextTick(() => {
// 一旦完成界面更新, 立即执行回调
new Swiper('.swiper-container', {
autoplay: true,
pagination: {
el: '.swiper-pagination',
clickable: true
}
})
this.$store.dispatch('getShops')
import {
mapState} from 'vuex'
export default {
computed: {
...mapState(['shops'])
}
}
data () {
return {
baseImgUrl: 'http://owoccema2.bkt.clouddn.com/show/MintShop/'
}
}
<li class="shop_li border-1px" v-for="(shop,index) in shops" :key="index">
<a>
<div class="shop_left">
<img class="shop_img" :src="baseImgUrl + shop.image_path">
div>
<div class="shop_right">
<section class="shop_detail_header">
<h4 class="shop_title ellipsis">{
{shop.name}}h4>
<ul class="shop_detail_ul">
...
ul>
section>
<section class="shop_rating_order">
<section class="shop_rating_order_left">
...
section>
div>
a>
li>
在components文件夹下新建Star文件夹,然后将原本在ShopList文件夹下的images文件夹里的stars移动到新建的Star文件夹里并重命名为images
在Star文件夹里新建Star.vue并将ShopList.vue中评分部分的模版和样式剪切进去,注意修改图片路径
<div class="star star-24">
<span class="star-item on">span>
<span class="star-item on">span>
<span class="star-item on">span>
<span class="star-item half">span>
<span class="star-item off">span>
div>
根据类名为组件设置属性props和计算属性
// 类名常量
export default {
props: {
score: Number,
size: Number
},
computed: {
/*
3.2: 3 + 0 + 2
4.7: 4 + 1 + 0
*/
// 该方法产生一个数组starArr来表示on half off类名的span数量(总长度为5)
starClasses () {
const {
score} = this
const starArr = []
// 向starArr中添加n个CLASS_ON
const scoreInteger = Math.floor(score)
for (let i = 0; i < scoreInteger; i++) {
starArr.push(CLASS_ON)
}
// 向starArr中添加0/1个CLASS_HALF
if(score*10-scoreInteger*10>=5) {
starArr.push(CLASS_HALF)
}
// 向starArr中添加n个CLASS_OFF
while(starArr.length<5) {
starArr.push(CLASS_OFF)
}
return starArr
}
}
}
修改template模版
<div class="star" :class="'star-'+size">
<span class="star-item" v-for="(sc, index) in starClasses" :class="sc" :key="index">span>
div>
在ShopList中import引入并注册使用
<Star :score="shop.rating" :size="24">Star>
目前首页的内容数据已经可以异步获取并显示,但在数据还未加载完成时,为了优化用户体验,应该给出页面加载中的提示界面。
首先将svg资源图片放入相应的Msite和ShopList的images文件夹里
然后修改模版的显示条件
<div class="swiper-container" v-if="categorys.length">
...
div>
<img src="./images/msite_back.svg" alt="back" v-else>
<ul class="shop_list" v-if="shops.length">
...
ul>
<ul v-else>
<li v-for="item in 6" :key="item">
<img src="./images/shop_back.svg" alt="back">
li>
ul>
1. 界面相关效果
**2. 前后台交互功能 **
data () {
return {
loginWay: false // true代表短信登陆, false代表密码
}
}
<div class="login_header_title">
<a href="javascript:;" :class="{on: loginWay}" @click="loginWay=true">短信登录a>
<a href="javascript:;" :class="{on: !loginWay}" @click="loginWay=false">密码登录a>
div>
<div class="login_content">
<form>
<div :class="{on: loginWay}">
短信登陆的input...
div>
<div :class="{on: !loginWay}">
密码登陆的input...
div>
form>
div>
既然是要对手机号格式进行检查就要为其绑定数据
<input type="tel" maxlength="11" placeholder="手机号" v-model="phone">
根据手机号格式是否正确来动态的为获取验证码添加一个类名right_phone
<button disabled="disabled" class="get_verification" :class="{right_phone:rightPhone}">获取验证码button>
right_phone的值是根据phone来确定的,所以应该是一个计算属性
computed: {
rightPhone () {
// 利用正则对手机号进行匹配,返回布尔值
return /^1\d{
10}$/.test(this.phone)
}
}
此时button的disabled也应该根据rightPhone的返回值来确定
<button :disabled="!rightPhone" class="get_verification" :class="{right_phone:rightPhone}">获取验证码button>
在style部分加入新定义的right_phone样式
.get_verification
...
&.right_phone
color black
点击获取验证码之后会显示30s倒计时的效果并发送获取验证码的请求
注意要阻止点击button的默认提交表单事件,所以用@click.prevent
<button :disabled="!rightPhone" class="get_verification" :class="{right_phone:rightPhone}" @click.prevent="getCode">获取验证码button>
在data里定义定时变量computeTime,然后感觉时间判断显示的内容
<button class="get_verification">{
{computeTime>0 ? `(${computeTime}s)已发送` : '获取验证码'}}button>
然后在methods里定义定时器
methods: {
getCode () {
// 如果当前没有计时!this.computeTime等于this.computeTime === 0
if(!this.computeTime) {
// 启动倒计时
this.computeTime = 30
this.intervalId = setInterval(() => {
this.computeTime--
if(this.computeTime <= 0) {
// 停止计时
clearInterval(this.intervalId)
}
}, 1000)
}
// 发送ajax请求(向指定手机号发送验证码短信)
}
}
利用两个type不同的input来实现密码的显示隐藏
在data里定义showPwd(默认为false)控制两者的显隐,同时使用v-model绑定数据pwd
<section class="login_verification">
<input type="text" maxlength="8" placeholder="密码" v-if="showPwd" v-model="pwd">
<input type="password" maxlength="8" placeholder="密码" v-else v-model="pwd">
...
section>
为滑块区域添加点击监听,用来切换showPwd的值
同时切换滑块的各种样式
<div class="switch_button" :class="showPwd?'on':'off'" @click="showPwd=!showPwd">
<div class="switch_circle" :class="{right: showPwd}">div>
<span class="switch_text">{
{showPwd ? 'abc' : '...'}}span>
div>
添加必要的style
>.switch_circle
...
&.right
transform translateX(30px)
首先阻止点击登录button的默认提交表单事件
在methods里定义login方法并收集表单数据(即为未使用v-model绑定data的input添加data)
data () {
return {
...
name: '', // 用户名
code: '', // 短信验证码
captcha: '', // 图形验证码
}
}
在login里根据不同的登录方式对收集的数据进行检查
if(this.loginWay) {
// 短信登陆
if(!this.rightPhone) {
// 手机号不正确
return
} else if(!/^\d{6}$/.test(code)) {
// 验证必须是6位数字
return
}
}else {
// 密码登陆
...
}
要将验证失败的提示信息显示出来,可以使用自定义AlertTip弹窗组件(开发中可以用第三方插件)
import AlertTip from '../../components/AlertTip/AlertTip.vue'
...
export default {
...
components: {
AlertTip
}
}
为AlertTip定义状态数据
data () {
return {
...
alertText: '', // 提示文本
alertShow: false, // 是否显示警告框
}
}
在页面中放置弹窗组件
<section>
...
<AlertTip :alertText="alertText" v-show="alertShow" @closeTip="closeTip"/>
section>
抽取显示弹窗和关闭弹窗的方法,并在login方法和AlertTip中使用
showAlert (alertText) {
this.alertShow = true
this.alertText = alertText
},
closeTip () {
this.alertShow = false
this.alertText = ''
},
login () {
if(this.loginWay) {
// 短信登陆
if(!this.rightPhone) {
// 手机号不正确
this.showAlert('手机号不正确')
return
} else if(!/^\d{6}$/.test(code)) {
this.showAlert('验证必须是6位数字')
return
}
}...
}
根据API文档,动态一次性图形验证码的接口为http://localhost:4000/captcha
同时为这个img添加点击事件,让其可以点击重新发送请求,刷新图片
<img class="get_verification" src="http://localhost:4000/captcha" alt="captcha" @click="getCaptcha" ref="captcha">
为这个img元素添加ref属性,方便在getCaptcha中使用
利用时间戳使其每次加载时的请求参数不一样
getCaptcha () {
// 每次指定的src要不一样
this.$refs.captcha.src = 'http://localhost:4000/captcha?time='+Date.now()
}
利用第三方短信验证码平台(容联云通讯)提供的接口来实现
先在服务端项目将自己的sid与token填入util文件夹下的sms_util.js文件
再在客户端的登录组件中引入接口请求函数(这是为了方便处理,同样也可以在action里调用)
import {
reqSendCode, reqSmsLogin, reqPwdLogin} from '../../api'
在getCode方法里进行调用
// 请求返回的是promise对象,所以用到了async await
async getCode () {
...
// 发送ajax请求(向指定手机号发送验证码短信)
const result = await reqSendCode(this.phone)
if(result.code===1) {
// 手机号验证失败
// 显示提示
this.showAlert(result.msg)
// 停止计时
if(this.computeTime) {
this.computeTime = 0
clearInterval(this.intervalId)
this.intervalId = undefined
}
}
}
// 只要手机号填写正确 短信验证码也可以在服务端的控制台中查看
在login方法里完成短信和密码登录的aiax请求
async login () {
let result // 保存登录成功后返回的数据
...
// 发送ajax请求短信登陆
result = await reqSmsLogin(phone, code)
...
// 发送ajax请求密码登陆
result = await reqPwdLogin({
name, pwd, captcha})
...
}
点击登录发送请求的同时停止计时器,然后将请求的结果进行处理
...
// 停止计时
if(this.computeTime) {
this.computeTime = 0
clearInterval(this.intervalId)
this.intervalId = undefined
}
// 根据结果数据处理
if(result.code===0) {
// 成功
const user = result.data
// 将user信息保存到vuex的state
// todo
// 去个人中心界面
this.$router.replace('/profile')
} else {
// 显示新的图片验证码
this.getCaptcha()
// 显示警告提示
const msg = result.msg
this.showAlert(msg)
}
测试用的用户名: abc,密码: 123。
1. 将用户信息保存到vuex
在state中添加用户信息的状态数据userInfo
userInfo: {
} // 用户信息
在mutation-types中定义常量
export const RECEIVE_USER_INFO = 'receive_user_info' // 接收用户信息
在mutations文件中增加改变state的方法
//先import引入RECEIVE_USER_INFO
[RECEIVE_USER_INFO] (state, {
userInfo}) {
state.userInfo = userInfo
}
在actions文件中增加同步用户信息的方法
// 要先引入RECEIVE_USER_INFO这个mutation
// 同步记录用户信息
recordUser ({
commit}, userInfo) {
commit(RECEIVE_USER_INFO, {
userInfo})
}
在Login组件中调用这个action
// 将user保存到vuex的state
this.$store.dispatch('recordUser',user)
之后可以在个人中心Profile页面读取并显示用户信息userInfo
import {
mapState} from 'vuex'
...
computed: {
...mapState(['userInfo'])
}
<p class="user-info-top">{
{userInfo.name || '登录/注册'}}p>
2. 更新登录后的个人中心界面
使用用户名和密码登录时显示用户名和绑定的手机号信息
使用手机号登录时只需要显示手机号
根据用户是否登录来定义a标签不同的路由
<router-link :to="userInfo._id ? '/userinfo': '/login'">
...
<div class="user-info">
<p class="user-info-top" v-if="!userInfo.phone" > {
{userInfo.name || '登录/注册'}}p>
<p>
...
<span class="icon-mobile-number">{
{userInfo.phone || '暂无绑定手机号'}}span>
p>
div>
router-link>
同时要注意首页Msite顶部的信息也要进行更改
<router-link class="header_login" slot="right" :to="userInfo._id ? '/userinfo': '/login'">
<span class="header_login_text" v-if="!userInfo._id">
登录|注册
span>
<span class="header_login_text" v-else>
<i class="iconfont icon-yonghuming">i>
span>
router-link>
3. 完成自动登录功能
服务器端的routes文件夹下的index.js中已经定义了返回用户信息的方法
// 其中她将用户的userid取出来放入一个session会话中
router.get('/userinfo', function (req, res) {
// 取出userid
const userid = req.session.userid
// 查询
UserModel.findOne({
_id: userid}, _filter, function (err, user) {
// 如果没有, 返回错误提示
if (!user) {
// 清除浏览器保存的userid的cookie
delete req.session.userid
res.send({
code: 1, msg: '请先登陆'})
} else {
// 如果有, 返回user
res.send({
code: 0, data: user})
}
})
})
app.js中已经定义了这个用户登录的session会话的维持时间为24h
app.use(session({
secret: '12345',
cookie: {
maxAge: 1000*60*60*24 }, //设置maxAge是80000ms,即80s后session和相应的cookie失效过期
resave: false,
saveUninitialized: true,
}));
api中的对应接口已经完成
// 获取用户信息(根据会话)
export const reqUserInfo = () => ajax(BASE_URL + '/userinfo')
在action中定义一个方法来调用这个接口
// 异步获取用户信息(先引入reqUserInfo接口)
async getUserInfo ({
commit}) {
const result = await reqUserInfo()
if (result.code === 0) {
const userInfo = result.data
commit(RECEIVE_USER_INFO, {
userInfo})
}
}
最后在App.vue中引入action并触发
async mounted () {
...
// this.getAddress()
this.getUserInfo()
},
methods: {
...mapActions(['getUserInfo'])
}
用户登录后在个人中心页面添加一个退出登录的按钮
下载安装mint-ui来实现
// 安装mint-ui
npm install --save mint-ui
实现自动按需打包
// 安装工具包
npm install --save-dev babel-pulgin-component
// 配置
"plugins": ["transform-runtime",["component", [
{
"libraryName": "mint-ui",
"style": true
}
]]]
引入并注册使用mint-ui的标签组件
// 在入口的main.js引入Button
import {
Button} from 'mint-ui'
// 注册全局组件
Vue.component(Button.name, Button)
在Profile页面中使用mint-ui的标签
<section class="profile_my_order border-1px">
<mt-button type="danger" style="width: 100%" v-if="userInfo._id" @click="logout">退出登录mt-button>
section>
引入mint-ui的confirm确认和toast文本提示框
import {
MessageBox, Toast } from 'mint-ui'
logout () {
MessageBox.confirm('确认退出吗?').then(
action => {
// 请求退出
this.$store.dispatch('logout')
Toast('登出完成')
},
action => {
console.log('取消登录')
}
)
}
在actions.js中定义退出登录的方法
// 首先引入api的index.js中定义的reqLogout接口和mutation
// 异步登出
async logout ({
commit}) {
const result = await reqLogout()
if (result.code === 0) {
commit(RESET_USER_INFO)}
}
//同时改动下列两个文件
//mutations-types文件
export const RESET_USER_INFO = 'receive_user_info' // 重置用户信息
//mutations文件
[RESET_USER_INFO] (state) {
state.userInfo = {
}
}
通过点击商家列表(ShopList)里的某一项进入商家店铺的一级路由界面(Shop.vue),商家店铺界面包括顶部的一个头部的一般组件(ShopHeader.vue)和下面三个可以切换的路由子组件(ShopGoods、ShopInfo、ShopRatings)
{
path: '/shop',
component: Shop,
children: [{
path: '/shop/goods',
component: ShopGoods
},
{
path: '/shop/ratings',
component: ShopRatings
},
{
path: '/shop/info',
component: ShopInfo
},
{
path: '',
redirect: '/shop/goods'
}]
}
在ShopList.vue中为商家列表添加点击事件
<ul class="shop_list" v-if="shops.length">
<li class="shop_li border-1px" v-for="(shop,index) in shops" :key="index" @click="$router.push('/shop')">
...
li>
ul>
Shop.vue中引入各路由组件然后在模版中使用
<div>
<ShopHeader>ShopHeader>
<div class="tab">
<div class="tab-item">
<router-link to="/shop/goods" replace>点餐 router-link>
div>
<div class="tab-item">
<router-link to="/shop/ratings" replace>评价 router-link>
div>
<div class="tab-item">
<router-link to="/shop/info" replace>商家 router-link>
div>
div>
<keep-alive>
<router-view/>
keep-alive>
div>
设计json数据的结构
商家店铺界面包括点餐、评价、和商家信息三个部分的数据(头部显示的数据也是商家信息),而且这三个方面的数据直接没有顺序关系,可以使用对象结构来存储它们。
// 点餐数据里包括各种不同分类的食品,可以用数组goods[]来存放这些数据对象(没有顺序关系但属于同一类型)
// 每一类食品除了分类名称name还有一个foods数据来存放这一类的食品
// foods数组内的每个对象都是一个食品实例
{
"goods":[
{
name: "精选套餐",
foods: [
{
name: "南瓜粥",
price: 9
}
]
}
],
"ratings":[
{
}
],
"info":{}
}
// 评价数据都属于同一类型,可以使用数组来存放[{评价一},{评价二}..]
// 商家信息数据没有顺序,可以统一保存到对象内
使用mockjs模拟数据接口
npm install --save mockjs
/*
使用mockjs提供mock数据接口
*/
import Mock from 'mockjs'
import data from './data.json'
// 返回goods的接口
Mock.mock('/goods', {
code: 0, data: data.goods})
// 返回ratings的接口
Mock.mock('/ratings', {
code: 0, data: data.ratings})
// 返回info的接口
Mock.mock('/info', {
code: 0, data: data.info})
// export default ??? 不需要向外暴露任何数据, 只需要保存能执行即可
在main.js中加载mockServer文件即可
import './mock/mockServer.js'
ajax请求mockjs模拟的数据
api/index.js中定义ajax请求方法
/*
* 获取商家信息(下列请求由mock拦截并返回 不需要代理)
*/
export const reqShopInfo = () => ajax('/info')
/**
* 获取商家评价数组
*/
export const reqShopRatings = () => ajax('/ratings')
/**
* 获取商家商品数组
*/
export const reqShopGoods = () => ajax('/goods')
再写一套用来管理从后台接收到的数据vuex配置
// 1. state
goods: [], // 商品列表
ratings: [], // 商家评价列表
info: {
} // 商家信息
// 2. mutations-type
export const RECEIVE_GOODS = 'receive_goods' // 接收商品数组
export const RECEIVE_RATINGS = 'receive_ratings' // 接收商家评价数组
export const RECEIVE_INFO = 'receive_info' // 接收商家信息
// 3. mutations
[RECEIVE_INFO] (state, {
info}) {
state.info = info
},
[RECEIVE_RATINGS] (state, {
ratings}) {
state.ratings = ratings
},
[RECEIVE_GOODS] (state, {
goods}) {
state.goods = goods
}
// 4. action
// 异步获取商家信息
async getShopInfo ({
commit}) {
const result = await reqShopInfo()
if (result.code === 0) {
const info = result.data
commit(RECEIVE_INFO, {
info})
}
},
// 异步获取商家评价列表
async getShopRatings ({
commit}) {
const result = await reqShopRatings()
if (result.code === 0) {
const ratings = result.data
commit(RECEIVE_RATINGS, {
ratings})
}
},
// 异步获取商家商品列表
async getShopGoods ({
commit}) {
const result = await reqShopGoods()
if (result.code === 0) {
const goods = result.data
commit(RECEIVE_GOODS, {
goods})
}
}
在shop.vue中测试获取商家信息数据
// 可以在控制台的vuex中查看到info数据
mounted () {
this.$store.dispatch('getShopInfo')
}
已经可以获取到mock的模拟数据,接下来开始修改商家界面的头部模板
修改完模版和样式代码,然后读取vuex里的数据
import {
mapState} from 'vuex'
export default {
computed: {
...mapState(['info'])
}
}
并将info里的数据渲染到模版中,其中有几点需要注意
为nav动态绑定背景style(删除原来的样式里的背景图片)
:style="{
backgroundImage: `url(${info.bgImg})`}"
在style里定义三种不同颜色的优惠活动的类名并放进数组,然后把它们和info.supports.type关联起来
data () {
return {
// 注意顺序要与info的type对应
supportClasses: ['activity-green', 'activity-red', 'activity-orange']
}
}
<div class="activity" :class="supportClasses[info.supports[0].type]">
div>
这样写会报一个错误Error in render: "TypeError: Cannot read property '0' of undefined"
因为vuex的数据是异步的,而页面刚加载时info为空对象,info.supports不存在,为undefined。所以再取它下标为0的值会报这个错误。(二级表达式info.bgImg并不会报错)
使用v-if来避免没有数据时也会解析模版
<div class="shop-header-discounts" v-if="info.supports" @click="toggleSupportShow">div>
利用shopShow和supportShow来标识模态框和优惠活动列表是否显示,同时定义切换显隐的方法
data () {
return {
...
shopShow: false,
supportShow: false
}
},
methods: {
toggleShopShow () {
this.shopShow = !this.shopShow
},
toggleSupportShow () {
this.supportShow = !this.supportShow
}
}
可以为弹窗添加一个transition动画(activity-sheet一样)
<transition name="fade">
<div class="shop-brief-modal" v-show="shopShow">div>
transition>
然后找到shop-brief-modal的样式 添加动画过程
&.fade-enter-active, &.fade-leave-active {
transition: opacity 0.5s;
}
&.fade-enter, &.fade-leave-to {
opacity: 0;
}
完成了头部的ShopHeader,接下来是点餐部分的ShopGoods组件。此组件是一个比较复杂的路由组件,主要包含了3个部分:ShopCart组件(底部的购物车)、CartControl组件(购物车里的加减商品按钮组件)、Food组件(点击商品图片查看详细信息的弹窗)
另外还使用了第三方库 better-scroll: 处理UI 滑动
左右结构的模版布局
<div class="goods">
<div class="menu-wrapper" ref="menuWrapper">
<ul>
<li>折扣li>
<li>优惠li>
<li>爽口凉菜li>
<li>...li>
ul>
div>
<div class="foods-wrapper" ref="foodsWrapper">
<ul>
<li class="food-list-hook">
<h1 class="title">折扣h1>
<ul>
<li>南瓜粥li>
<li>红豆薏米美肤粥li>
ul>
li>
<li class="food-list-hook">
<h1 class="title">优惠h1>
<ul>
<li>红枣山药li>
<li>...li>
ul>
li>
<li>其他分类...li>
ul>
div>
div>
请求并读取数据进行模版渲染
import {
mapState} from 'vuex'
export default {
mounted () {
// 使用 axios 请求 mockjs 提供的接口
this.$store.dispatch('getShopGoods')
},
computed: {
...mapState(['goods'])
}
}
<li class="menu-item" v-for="(good, index) in goods" :key="index">
<span class="text bottom-border-1px">
<img class="icon" :src="good.icon" v-if="good.icon">
{
{good.name}}
span>
li>
<li class="menu-item" :class="{current: index===currentIndex}" >li>
设计计算属性:currentIndex
根据哪些数据来进行计算?
既然要实现左右两侧的联动,那么首先要获取一些位置信息。
scrollY: 右侧食品列表滑动的Y轴坐标(滑动过程中实时变化)
tops: 所有右侧分类标题到屏幕顶部的距离,即li的top值组成的数组
(列表第一次显示后就不再变化)
- 在滑动过程中,实时收集scrollY
- 列表第一次显示后,收集tops
- 实现currentIndex的计算逻辑
使用better-scroll实现回弹滑动
npm install --save better-scroll
中文官网
import BScroll from 'better-scroll'
// 要考虑列表显示之后创建BScroll实例的时机
//(参考12.2解决swiper的bug,之前使用watch和nextTick来监听数据更新)
// 这里使用另一种方法 通过action的回调函数来通知组件数据已经更新
mounted() {
this.$store.dispatch('getShopGoods', () => {
// 数据更新后执行
this.$nextTick(() => {
// 列表数据更新显示后执行
new BScroll('.menu-wrapper', {
click: true
})
new BScroll('.foods-wrapper', {
click: true
})
})
})
},
//同时也要修改getShopGoods这个action
// 异步获取商家商品列表
async getShopGoods ({
commit}, callback) {
const result = await reqShopGoods()
if (result.code === 0) {
const goods = result.data
commit(RECEIVE_GOODS, {
goods})
// 数据更新了, 通知一下组件
callback && callback()
}
}
收集scrollY和tops
// 要收集滚动的数据,那么就要利用betterScroll对象来监听滚动事件
_initScroll() {
// 列表显示之后创建
new BScroll('.menu-wrapper', {
click: true
})
// 根据文档配置scroll选项
this.foodsScroll = new BScroll('.foods-wrapper', {
probeType: 2, // 因为惯性滑动不会触发
click: true
})
// 给右侧列表绑定scroll监听
this.foodsScroll.on('scroll', ({
x, y}) => {
console.log(x, y)
this.scrollY = Math.abs(y)
})
} // 将其封装为初始化滚动的方法
// 初始化tops
_initTops() {
// 1. 初始化tops
const tops = []
let top = 0
// 第一个li的top为0
tops.push(top)
// 2. 收集
// 在foods列表下找到所有分类的li
const lis = this.$refs.foodsUl.getElementsByClassName('food-list-hook')
Array.prototype.slice.call(lis).forEach(li => {
top += li.clientHeight
tops.push(top)
})
// 3. 更新数据
this.tops = tops
}
完成计算属性currentIndex的逻辑
currentIndex () {
// 初始和相关数据发生了变化
// 得到条件数据
const {
scrollY, tops} = this
// 根据条件计算产生一个结果
const index = tops.findIndex((top, index) => {
// scrollY>=当前top && scrollY<下一个top
return scrollY >= top && scrollY < tops[index + 1]
})
// 返回结果(也就是当前的scrollY值属于第几个li区间)
return index
}
// 此时可以实现滑动右侧列表更新左侧当前分类
// 但如果快速滑动则会出现因为惯性滑动不能正确收集scrollY值的bug
// 解决方法一:将probeType的值改为3 但如果不想实现监听惯性滑动触发大量事件,则需要计算滑动结束时的scrollY值来确定当前分类
// 在_initScroll里给右侧列表绑定scroll结束的监听
this.foodsScroll.on('scrollEnd', ({
x, y}) => {
console.log('scrollEnd', x, y)
this.scrollY = Math.abs(y)
})
实现点击左侧分类滑动右侧食物列表
<li class="menu-item" :class="{current: index===currentIndex}" @click="clickMenuItem(index)">li>
clickMenuItem (index) {
// 得到目标位置的scrollY
const scrollY = this.tops[index]
// 立即更新scrollY(让点击的分类项成为当前分类)
this.scrollY = scrollY
// 平滑滑动右侧列表 better-scroll里的方法
this.foodsScroll.scrollTo(0, -scrollY, 300)
}
完成加减选购食物的组件
<div class="cartcontrol">
<transition name="move">
<div class="iconfont icon-remove_circle_outline">div>
transition>
<div class="cart-count" >div>
<div class="iconfont icon-add_circle">div>
div>
分析该组件接收的props
// 组件里更改的数据分别对应每一种食物,所以不应该是简单的Number类型的count,而应该是food对象(该组件为其添加food.count属性)
props: {
food: Object
},
// 为加减按钮绑定的点击事件
methods: {
updateFoodCount (isAdd) {
// 这里不能直接处理food.count因为food是在good里的数据对象
// 应该通过vuex触发action来管理数据 并把当前的food对象参数传递过去
this.$store.dispatch('updateFoodCount', {
isAdd, food: this.food})
}
}
为food添加一个新的绑定数据count
//省略mutation-types里定义常量的过程
// action中同步更新food中的count值
updateFoodCount ({
commit}, {
isAdd, food}) {
if (isAdd) {
commit(INCREMENT_FOOD_COUNT, {
food})
} else {
commit(DECREMENT_FOOD_COUNT, {
food})
}
}
// mutation中更改数据(注意引入vue)
import Vue from 'vue'
[INCREMENT_FOOD_COUNT] (state, {
food}) {
if (!food.count) {
// 第一次增加
// food.count = 1 // 这样新增的属性没有数据绑定
/*
对象
属性名
属性值
*/
Vue.set(food, 'count', 1) // 让新增的属性也有数据绑定
} else {
food.count++
}
}
[DECREMENT_FOOD_COUNT] (state, {
food}) {
if (food.count) {
// 只有有值才去减
food.count--
}
}
点击某个食品,弹出该Food弹窗组件展示该食品的信息并可将其加入购物车
export default {
// 要展示食品信息,所以要接收food对象的数据
props: {
food: Object
},
data () {
return {
isShow: false
}
},
methods: {
// Food组件内控制显示Food组件的方法
toggleShow () {
this.isShow = !this.isShow
}
},
components: {
CartControl
}
}
在ShopGoods中引用该组件
<div>
<div class="goods">
...
<div class="foods-wrapper">
...
<ul>
<li class="food-item" @click="showFood(food)">li>
ul>
div>
div>
<Food :food="food" ref="food"/>
div>
注意每个li里的CartControl 组件都使用@click.stop 来阻止了事件冒泡,就是为了防止点击加减按钮同时触发弹出Food组件
同时控制是否显示Food组件(其实可以直接在Food组件上使用v-if,这里是为了练习在父组件中得到子组件对象并调用其方法)
// 显示点击的food
showFood (food) {
// 设置要传递给food组件的数据
this.food = food
// 显示food组件 (在父组件中调用子组件对象的方法)
this.$refs.food.toggleShow()
}
购物车组件中存放的都是count大于0的food,这些数据既可以通过vuex来管理,也可以使用computed来动态计算goods里每个food的count来管理。但computed要经过两层的轮询而且还要考虑数据量的问题,所以还是使用vuex来管理这些cartFoods数据的效率更高。
export default {
totalCount (state) {
return state.cartFoods.reduce((preTotal, food) => preTotal + food.count, 0)
},
totalPrice (state) {
return state.cartFoods.reduce((preTotal, food) => preTotal + food.count * food.price, 0)
},
positiveSize (state) {
return state.ratings.reduce((preTotal, rating) => preTotal + (rating.rateType === 0 ? 1 : 0), 0)
}
}
computed: {
// 在购物车中获取到cartFoods的state 以及商家的info
...mapState(['cartFoods', 'info']),
// 获取相应的Getters里的数据
...mapGetters(['totalCount', 'totalPrice']),
// 通过计算已购食品来设置购物车不同的样式和提示文字
payClass () {
const {
totalPrice} = this
const {
minPrice} = this.info
return totalPrice>=minPrice ? 'enough' : 'not-enough'
},
payText () {
const {
totalPrice} = this
const {
minPrice} = this.info
if(totalPrice===0) {
return `¥${
minPrice}元起送`
} else if(totalPrice<minPrice) {
return `还差¥${
minPrice-totalPrice}元起送`
} else {
return '结算'
}
},
},
watch: {
totalCount: function () {
// 如果总数量为0, 直接不显示
if (this.totalCount === 0) {
this.isShow = false
// return false
}
},
isShow: function () {
if (this.isShow) {
this.$nextTick(() => {
// 实现BScroll的实例是一个单例
if (!this.scroll) {
this.scroll = new BScroll('.list-content', {
click: true
})
} else {
this.scroll.refresh() // 让滚动条刷新一下: 重新统计内容的高度
}
})
}
return this.isShow
}
}
注意:要保证购物车的列表是单例,不然打开多次购物车列表会初始化多个实例,然后再点击会触发多次点击事件。
引入mint-ui实现清空购物车的交互
// 注意CLEAR_CART的mutation不能仅仅把catFoods数组清空,还要先清空goods里food的count
[CLEAR_CART] (state) {
// 清除food中的count
state.cartFoods.forEach(food => {
food.count = 0 })
// 移除购物车中所有购物项
state.cartFoods = []
}
商家评价组件分为上部的Star评分以及下面的用户评价信息列表
// 先在mounted里通过触发action请求ratings数据
mounted () {
// 为getShopRatings这个action添加回调函数
this.$store.dispatch('getShopRatings', () => {
this.$nextTick(() => {
new BScroll(this.$refs.ratings, {
click: true
})
})
})
},
data () {
return {
onlyShowText: true, // 是否只显示有文本的
selectType: 2 // 选择的评价类型: 0满意, 1不满意, 2全部
}
},
// 在computed里获取info和ratings的State数据以及返回好评数量的Getters
computed: {
...mapState(['info', 'ratings']),
...mapGetters(['positiveSize']),
filterRatings () {
// 得到相关的数据
const {
ratings, onlyShowText, selectType} = this
// 产生一个过滤新数组
return ratings.filter(rating => {
const {
rateType, text} = rating
/*
条件1:
selectType: 0/1/2
rateType: 0/1
全部 || 满意、不满意中的一种
selectType===2 || selectType===rateType
条件2
onlyShowText: true/false
text: 有值/没值
符合条件一的全部评价 || 符合条件一的有文字的评价
!onlyShowText || text.length>0
*/
return (selectType === 2 || selectType === rateType) && (!onlyShowText || text.length > 0)
})
}
},
// 可以在这里自己写过滤器
filters: {
dateFormat:function (input) {
var d = new Date(input);
var year = d.getFullYear();
var month = d.getMonth() + 1;
var day = d.getDate() <10 ? '0' + d.getDate() : '' + d.getDate();
var hour = d.getHours();
var minutes = d.getMinutes();
var seconds = d.getSeconds();
return year+ '-' + month + '-' + day + ' ' + hour + ':' + minutes + ':' + seconds;
}
}
filterRatings 就是为要渲染的评价列表设置各种必要条件 使其按照用户选择进行渲染(即页面显示的列表要同时满足条件一与条件二)
// filters文件夹 也可以使用moment或date-fns(推荐)库来实现日期过滤
// npm install moment/date-fns --save
import Vue from 'vue'
// import moment from 'moment'
import format from 'date-fns/format'
// 自定义过滤器
Vue.filter('date-format', function (value, formatStr = 'YYYY-MM-DD HH:mm:ss') {
// return moment(value).format(formatStr)
return format(value, formatStr)
})
// 在main.js文件中引入注册的过滤器
import './filters'
获取商家列表后就已经得到了商家信息info数据,但是还没有创建ShopInfo组件对象
// mounted之后创建BScroll对象
mounted () {
// 如果数据还没有, 直接结束
if (!this.info.pics) {
return
}
// 数据有了, 可以创建BScroll对象形成滑动
this._initScroll()
}
同时要动态计算横向滑动的ul宽度
methods: {
_initScroll () {
new BScroll('.shop-info')
// 动态计算ul的宽度
const ul = this.$refs.picsUl
const liWidth = 120
const space = 6
const count = this.info.pics.length
ul.style.width = (liWidth + space) * count - space + 'px'
new BScroll('.pic-wrapper', {
scrollX: true // 水平滑动
})
}
}
既然要实现搜索功能,那么就要有搜索请求的接口以及vuex数据
// api/index.js里 添加根据经纬度和关键字搜索商铺列表的接口
export const reqSearchShop = (geohash, keyword) => ajax(BASE_URL+'/search_shops', {
geohash, keyword})
// 异步获取商家商品列表的action
async searchShops ({
commit, state}, keyword) {
const geohash = state.latitude + ',' + state.longitude
const result = await reqSearchShop(geohash, keyword)
if (result.code === 0) {
const searchShops = result.data
commit(RECEIVE_SEARCH_SHOPS, {
searchShops})
}
}
在Search组件中触发action
search () {
// 得到搜索关键字
const keyword = this.keyword.trim()
// 进行搜索
if (keyword) {
this.$store.dispatch('searchShops', keyword)
}
}
通过router-link将搜索结果searchShops渲染出来
<router-link :to="{path:'/shop', query:{id:item.id}}" tag="li"
v-for="item in searchShops" :key="item.id" class="list_li">
...
router-link>
<keep-alive>
<router-view />
keep-alive>
我们写的所有Js文件最后都会打包成一个文件,而我们实际的需求是路由组件并不是一次全部加载过来,而是按需加载。所以就要在打包前就对代码进行分割,从而实现路由组件懒加载。
// router/index.js文件中 改变引入方式 实现路由组件懒加载
const Msite = () => import('../pages/Msite/Msite.vue')
const Search = () => import('../pages/Search/Search.vue')
const Order = () => import('../pages/Order/Order.vue')
const Profile = () => import('../pages/Profile/Profile.vue')
// 此时的Msite等都是返回路由组件的函数,只有请求对应的路由路径时(第一次)才会执行此函数并加载路由组件
此时切换路由,可以在控制台NetWork里看到拆分打包后的js文件实现了按需加载
安装npm install --save vue-loader
// 在main.js文件中引入并注册插件
import VueLazyload from 'vue-lazyload'
// 将一张loading图片加载进来
import loading from './common/img/loading.gif'
Vue.use(VueLazyload, {
// 内部自定义一个指令lazy
loading
})
// 在图片标签中使用 (Food组件)
<img v-lazy="food.image">
npm run build --report
可以根据可视化文件分析页面对项目进行优化提示:如有不对请多多指教!希望给您带来帮助!多谢。