# waimai **Repository Path**: gitPythonchao/waimai ## Basic Information - **Project Name**: waimai - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2019-07-22 - **Last Updated**: 2020-12-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README 前言 后台是从网上down下来的,接口里面有一部分,另一部分是用mock模拟。 这一份非常详细的从头到尾的笔记是自己一个一个字敲出来的,碰到的各种坑,以及如何解决的,没有依赖任何人(身边也没有前端相关的人),都是通过搜索引擎去解决每一个坑。 ### 最新上传了服务器: 根目录有一个vue-take-server.rar,解压到磁盘其它目录后运行npm start,开启服务器,默认3000端口。 ### 项目启动 下载下来后,运行 ``` npm i //安装依赖 ``` 然后在根目录运行 ``` npm run dev //启动项目,默认8080端口。 ``` 登陆账号为abc,密码为123 **如果觉得对你有参考帮助的价值,请点亮一颗星星** **最近很多同学通过b站给我留言问问题,不能及时回复,放上我的qq 306444399 ,虽然平时很忙也不能保证及时回复沟通,但是尽量吧** **另外学习完vue的同学,可以跟着我一起用微信小程序原汁原味还原网易云音乐,[项目链接点这里](https://github.com/PsychicHira/wangyiyunMusic),如果觉得小程序的项目也不错,也请您点个星吧,O(∩_∩)O~~** ### 项目预览: 一个仿外卖的vue单页应用程序Web App(spa),包含常见的功能 ![img](https://github.com/PsychicHira/vue-take/blob/master/img/1.png) ![img](https://github.com/PsychicHira/vue-take/blob/master/img/2.png) ![img](https://github.com/PsychicHira/vue-take/blob/master/img/3.png) # 技术选型与项目结构 ## 技术选型 ![img](https://github.com/PsychicHira/vue-take/blob/master/img/4.png) ## 路由 ![img](https://github.com/PsychicHira/vue-take/blob/master/img/5.png) ## 项目结构 vue-take ![img](https://github.com/PsychicHira/vue-take/blob/master/img/6.png) # 项目一些准备与说明 ## 项目测试与打包发布 1. gshop-client_blank文件到项目文件(vue-take)下 2. 修改gshop-client_blank文件名称为gshop-client 3. 将gshop-client中的package.json里的"name": "gshop"改为自己的项目名称vue-take 4. 在此文件夹中的config文件夹中修改index.js里的autoOpenBrowser属性改为true,这样在运行时就会自动打开网页 开发环境打包运行 在gshop-client路径的cmd命令行中运行npm run dev打包 生产环境打包运行 1. 在gshop-client路径的cmd命令行中运行npm run build打包,然后文件目录会生成一个dist文件夹,就是打包压缩可以上线的文件 2. 下载serve(应该是个包),npm install -g serve 3. 在gshop-client文件夹中运行serve dist,就可以访问5000端口(这里报错了,页面显示找不到path,因为端口被占用,把其它东西都关了即可) ## 关于字体图标 1. 阿里巴巴去加入购物车(svg格式) 2. 在index.html中添加link标签 3. 把购物车中的整体链接粘贴到link标签中 4. 通过类名来使用 5. 要使用2个类名,第一个是icon-font好像,第二个就是字体的类名 ## 创建git相关 1. 在项目目录中使用git bash 初始化git git init 1. 设置全局签名 git config --global user.name hira_glb git config --global user.email 306444399@qq.com //查看签名使用命令 cat ~/.gitconfig 1. 将目前的项目初始放进git暂存区、本地库 ``` git add . //存入暂存区 git commit -m "初始化项目结构" //存入本底库 ``` 1. 创建git分支,名为dev,并移动到分支处进行开发 git branch dev //查看分支 git branch -v 绿色代表当前分支 git checkout dev 1. 创建SHH ``` $ ssh-keygen -t rsa -C warden_spirit@163.com //QQ邮箱不能用 文件名和密码都是Aa5604551 7v+iujB+XcNymkI86GkEuZ/MK1oh0qMwWxEdt37y+Oo (不知道做什么用,先复制) // 这里碰见坑,原因是要把c盘用户里面的.ssh文件夹全部删除,重新生成,不然连接不上 // 在生成ssh的时候,文件名密码之类的,全部enter,尤其不能设置文件名,坑死人个b了 ``` 1. 目录中会生成Aa5604551.pub,把内容粘贴到github中,关联SSH 2. 创建git远程库,在github中new project,名为vue-take 3. 创建远程地址别名 ``` git remote add vue-take git@github.com:PsychicHira/vue-take.git ``` 4. 把项目推送到git中 ``` git push vue-take master ``` # 项目开始 ## 安装stylus ``` cnpm install stylus -D cnpm install stylus-loader -D ``` - 测试 把App.vue文件的style里面加上 lang="stylus" ,启动npm run dev ,完美运行 根据目录结构创建底部和路由的vue文件 ## 公共样式文件(共用样式) 1. 按项目目录重新创建 2. 在common中创建stylus文件夹,创建mixins.styl 3. 在vue模版中,给style增加 lang="stylus" rel="stylesheet/stylus" 4. 在static文件夹中创建style文件夹,创建reset.css ## 创建基本文件让项目可以运行 1. 创建App.vue和main.js > *遇到问题*: > > > ​ 此时遇到坑,npm run dev 启动不了,提示没有没有stylus模块,这个时候cnpm i,提示包都装全了 > > > > 再次启动,页面提示Cannot GET / > > > > 搜索: > > > > *解决问题:* > > > > 最后发现了是 > > > > ``` > > > > ``` > > > > 的问题,缺少loader,先删除之 1. 成功启动了项目(要先安装stylus) ## index.html的移动端配置 - viewport ```html ``` - 解决0.3s延时 在head标签上面添加 ```javascript ``` ## 路由搭建 ### 思路重点(自己需要感受的逻辑) 1. main.js 是主入口文件 2. App.vue 文件作为大的外层架子,所有的内容都在app.vue 上面呈现。 3. main.js 的 el 控制 index.html 4. main.js 引入 App.vue 文件,实例化一个Vue 5. 通过 render ,让 App.vue 的内容 ,全替换掉 index.html 里 ID 为 app 的 div 区域 6. App.vue 里面,自然要把路由的 view 放进去了 ### 安装 ``` npm install vue-router ``` ### 引入(在main.js) ```javascript import VueRouter from 'vue-router' Vue.use(VueRouter) import router from './router/router.js' tips: //挂载到实例上 //一旦配置了router //就多了2个标签和2个属性 //router-view router-link //$route $router //怎么用法要复习 ``` ### 配置路由 (在 router.js 中引入路由模版,很简单就不说了) ### App.vue 分析: 1. 页面由2个部分组成,上面是路由,下面的底部导航 2. 引入底部vue文件,挂载到conponents属性中 > tips > > 如果初始化的时候已经选了严格模式那应该怎么办,这样难不倒我啦! > > 直接把config/index.js里面的dev属性,useEslint设置为false ## 组件FooterGuide.vue 这是App底部的导航,使用mui框架 1. 安装mint-ui ```javascript cnpm install mint-ui -S ``` 2. 在main.js中引入全部组件 ```javascript // 引入全部组件 import Vue from 'vue'; import Mint from 'mint-ui'; Vue.use(Mint); ``` 3. 发现了引用的东西,没有样式,就很奇怪,查了以前的项目和百度,要在引入mint-ui之前引入样式 4. 引入的时候发现,node_modules 文件夹中有的文件夹带下划线 _ 有的不带,百度一下,说带下划线的是cnpm装的,建议能npm的还是用npm 5. 引入样式后,成功显示 6. 然后把mint-ui的html复制到 FooterGuide.vue 里,同时把a标签换成 router-link ,因为要点击控制router ,恰好a标签的更改也不会产生样式的变化 7. 给router.js点击产生的类取个别名,控制点击样式(router-link被点击自动添加进标签的类) ```javascript linkActiveClass:'mui-active' ``` 8. 完成,回头修改图标和样式 ## 切图——4个路由页面的 Msite.vue Order.vue Search.vue Profile.vue ## 准备icon 1. 在阿里巴巴icon平台选好需要的icon 2. 生成链接,在index.html引入 3. 以类名的方式添加,如 ```html ``` ## 组件HeadTop.vue 由于顶部导航的内容有变化,可以看作和路由是一体的,所以可以提取成一个模块,提高复用性 1. 新建一个 HeadTop.vue 2. 让其它路由页面引入 HeadTop.vue ## 使用slot插槽(vue内置语法) HeadTop.vue 这个头部导航,根据每个路由不同,内容也变化,所以单独提取出来以后,要通过组件之间传值的方式,把父组件的title传给 HeadTop.vue。 如: Order.vue ```javascript ``` HeadTop.vue ```javascript ``` ## 使用swiper插件(首页轮播滑动插件) 被官方文档使用方法坑死了,傻逼玩意 官方文档的东西不一定百分百正确,要自行斟酌 1. 安装 ``` npm install swiper ``` 2. 在需要使用的页面(Msite.vue)引入 ```javascript import Swiper from 'swiper' import 'swiper/dist/css/swiper.min.css' ``` 3. html结构如下 - 在写页面结构的时候,类名已经按这样写完毕,所以Msite不要动了 ```html
Slide 1
Slide 2
Slide 3
``` 4. 在页面(Msite.vue)添加一个钩子函数,初始化Swiper ```javascript mounted(){ new Swiper ('.swiper-container', { loop: true, // 如果需要分页器 pagination: { el: '.swiper-pagination', } }) } ``` ## 组件ShopList.vue(把首页的 ShopList 商家列表单独提取出来) 1. 新建文件,在components新建ShopList,(和HeaderTop一样,单独弄出来组件) 2. 在Msite.vue中引入 ShopList.vue ,并注册成组件,在模版中使用 > tips > > - 首页滑动到了一个位置,点击其它路由,锚点还在那个高度 > - 解决:给路由页面的模版内的外层容器添加,overflow:hidden ## 后台 ### 启动后台 后台是基于node.js开发的,现成的 1. vue-take-serve文件夹(服务器代码文件夹),进入里面,运行命令行 ``` npm start ``` 2. 如何查看端口号:里面的bin文件夹有个www文件,里面有端口号,是4000 ### postman 1. 下载安装不多说 2. 注册登陆 3. 创建调试集合名字vue-take 但是本次不需要,直接导入 > tips > > - Vue.use() 是注册到全局组件中 > > - js里面, 只有function的东西会调用的时候才执行,其它都是创建的时候立即执行,立即执行的比如: > > ``` > const fs = require('fs') > new Promise() > ``` ## axios 1. 安装 ``` npm install axios ``` 2. 引入(在 main.js 中) ```js import axios from 'axios' ``` 3. 在项目中测试接口,创建一个按钮,点击触发事件,提示不能跨域 ```js methods:{ g:function(){ this.$axios.get('./api/position/40.10038,116.36120') .then(function (response) { console.log(response.data); }) .catch(function (error) { console.log(error); }) } } ``` 4. 解决跨域 - 在config的index.js配置文件的dev中,添加proxyTable,使用api替代服务器地址 - 修改 index.js配置 5. 然后在调用接口的时候直接使用/api,后面接上接口的其他路径信息,就可以,如: ``` '/api/position/40.10038,116.36120' ``` ## Vuex 安装 ``` npm install --save vuex ``` > tips: > > 安装完东西,控制台总是报异常 > > > > 搜索解决办法: > > ​ 团队作战,ios提交后,Android更新代码,便出现了这个异常; > > ​ 全局搜索,果然有这个依赖在package.json中; > > ​ 资料显示,fsevents是mac端使用,window可忽略。 > > ​ 所以注释掉代码,后重新npm install,依然报错; > > ​ 最后检查,node_modules文件中依然存在fsevents文件,删掉之后,重跑一遍恢复正常! > > ​ 总结 > > ​ 删除插件最好的方法也是最正确的方法 就是直接执行命令 npm uninstall xxx; > > ​ 如果手动注释代码中相关插件的配置,还需要删除node_modules已存在的文件,才能彻底从项目中移除。 > > 使用 npm uninstall fsevents 了一下 ### 在目录结构下的store,创建vuex仓库 ### 如何将 vuex 在项目中使用 1. 1. 在 store 中的 index.js 引入相关模块 ```js //index.js import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) ``` 2. 再导出一个vuex实例 ```js //index.js export default new Vuex.Store({}) ``` 3. 然后在 main.js 主入口中引入vuex主文件 ```js //main.js import store from './store/index.js' ``` 4. 挂载store ```js //main.js new Vue({ el: '#app', render: c => c(App), router: router, store }) ``` ### 在vuex核心文件里,写相应的内容 1. index.js(把vuex模块分开写,然后引入主文件里) ```js import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) import state from './state' import mutations from './mutations' import getters from './getters' import actions from './actions' export default new Vuex.Store({ state, mutations, getters, actions, }) ``` 2. state.js ```js export default { name:'laoliu', latitude: 40.10038, // 纬度 longitude: 116.36867, // 经度 address: {}, // 地址信息对象 categorys: [], // 分类数组 shops: [], // 商家数组 } ``` 3. mutations-types.js ```js export const RECEIVE_ADDRESS = 'receive_address' // 接收地址信息 export const RECEIVE_CATEGORYS = 'receive_categorys' // 接收分类数组 export const RECEIVE_SHOPS = 'receive_shops' // 接收商家数组 ``` 4. mutations.js (mutation 的主要用来处理state里面的数据,交互异步的东西给actions做) ```js import {RECEIVE_ADDRESS,RECEIVE_CATEGORYS,RECEIVE_SHOPS} from './mutations-types' export default { [RECEIVE_ADDRESS](state,address){ state.address = address }, [RECEIVE_CATEGORYS](state,categorys){ state.categorys = categorys }, [RECEIVE_SHOPS](state,shops){ state.shops = shops } } ``` 5. actions.js > tips: > > 浏览器跨域的一种方式:配置代理。 > > 浏览器有一个自带的代理服务器,监听8080本地端口,想跨域,就修改这个代理服务器的配置,用一种“欺骗”的行为达到跨域的目的 ```js import {RECEIVE_ADDRESS,RECEIVE_CATEGORYS,RECEIVE_SHOPS} from './mutations-types' //要用到axios,这里我没有封装ajax的API,直接写了,所以要引入axios import axios from 'axios' export default { receive_address({commit}){ //发送异步请求 axios.get('/api/position/40.10038,116.36120') .then(function (response) { // commit(RECEIVE_ADDRESS,{}) // 狗比官网, RECEIVE_ADDRESS 这样是不要加单引号的 commit(RECEIVE_ADDRESS,response.data) }) .catch(function (error) { console.log(error) }) }, receive_categorys({commit}){ axios.get('/api/index_category') .then(function (response) { commit(RECEIVE_CATEGORYS,response.data.data) }) .catch(function (error) { console.log(error) }) }, receive_shops({commit,state}){ axios.get('/api/shops?'+state.latitude+'&'+state.longitude) .then(function (response) { commit(RECEIVE_SHOPS,response.data.data) }) .catch(function (error) { console.log(error) }) } } ``` > tips: > > code的意思是: code为0的时候,返回数据 > > ## 动态渲染 ShopList.vue (商家列表) 1. 因为需要用到 Actions 里的东西,所以要引入 mapActions 来触发 Actions 里的东西 ```js import {mapActions} from 'vuex' export default { mounted:function () { this.$store.dispatch('receive_shops') }, computed:{ ...mapState(['shops']), ...mapActions(['receive_shops']) } } ``` 2. Actions 里的异步请求得到的值会赋给 state 里面的 shops 变量(商家数组),要用state.shops来v-for渲染到dom,所以要引入 mapState ```js import {mapState} from 'vuex' ``` 3. 使用v-for渲染DOM ## 动态渲染 Msite.vue (轮播的导航图) 思路: 1. receive_categorys 是个数组,长度是16,轮播有2个,每个8个小导航 2. 那么需要把16个元素分成8个一组,放进一个数组中,也就是一个二维数组[ [8个],[8个] ] 3. 为什么要弄成二维数组呢?为了v-for遍历渲染dom: 4. 外层的数组来决定几个轮播,内层的数组来遍历出来每一个小导航 1. 导航大海报可以滑动并轮播,所以要用到 sweiper 插件(上面已经安装引入过了) 2. 两个轮播,每个轮播有8个小导航,也就是“分组”,vuex里state.categorys 3. 在 Msite.vue 里要用到异步的 action 来获取 categorys,所以要引入过来 ```js import {mapState} from 'vuex' ``` 4. 映射这个map ```js computed:{ //...mapActions(['receive_categorys']), //这里应该是多余的,不注释掉,receive_categorys会触发2次(如果Actions里给这个方法打印一个123,这行也映射,就会执行2次) ...mapState(['categorys']), } ``` 5. 然后在生命周期 mounted 中分发 actions 里面的 receive_categorys 获取分组信息 ```js mounted:function () { this.$store.dispatch('receive_categorys') } ``` 6. 设定几个变量 ```js data(){ return { title:'好吃外卖', categorysArr:[], //来接收vuex里面的 categorys 分组数组,好拿来计算, categorysOut:[], //外层数组,来决定几个轮播 categorysIn:[], //内层数组,来决定每个轮播几个导航 baseImg:'http://localhost:4000' } } ``` 7. 在 methods 中写一个方法,实现上面的思路,当 actions 分发 receive_categorys 异步获取到数据,赋值给state里面的 categorys ,categorys 上面被映射到本组件中了,所以categorys 存在变化,那么就可以用watch监听 categorys 。等在 watch 中监听到categorys有变化,就触发methods里面的 categorysArr() , > 注意:为什么不写在computed里,因为 computed 里面有data中的一些变量,会自动触发方法,当触发的时候,categorysArr还没有拿到值,异步获取的,拿到的时候,页面早已加载完了,所以会报错 ```js methods:{ changeArr:function () { this.categorysArr.forEach((item)=> { if(this.categorysIn.length<8){ this.categorysIn.push(item) }else{ this.categorysOut.push(this.categorysIn) this.categorysIn=[] this.categorysIn.push(item) } }) this.categorysOut.push(this.categorysIn) } } ``` 1. watch监听categorys: ```js watch:{ categorys(){ this.categorysArr = this.categorys this.changeArr() //这里是把一维数组变成二维数组的方法 this.$nextTick(()=>{ new Swiper ('.swiper-container', { loop: true, // 如果需要分页器 pagination: { el: '.swiper-pagination', } }) }) } } ``` 2. v-for渲染dom,略,但是有个细节:关于img的src属性的语法 ```html
``` ## 补充加载中的提示图svg,提高用户体验 ## 解决 ShopList.vue 商家列表中的星星消失问题 图片路径样式有问题 ## 组件Star.vue——抽离star模块 不多说,很简单 > tips:关于stylus的& > > &,这是个新鲜东西。它是父级的引用 > > form表单内,点击了任何按钮,都默认提交了表单 ## 根据给star模块传的值,来动态生成星星 思路: 1. 父组件给Star组件传值,传2个,一个是评价分数,Star组件拿到后,渲染出全星、半星、空星,第二个是尺寸,Star组件里的星星,有2X和3X图区别,所以要给定尺寸 2. Star组件接收父组件的值 3. 动态生成全星、半星、空星 4. - 把rating计算成一个数组 - 整数是全星,大于0.5才显示半星,5减去整数是空星 - 把结果存入一个数组 - 通过数组v-for遍历出来对应的on half off 5. 计算的思路: 6. - 判断rating.floor()是否小大于1 - 如果大于1,循环创建“on”,push进数组 - 否则,判断是否有half - 最后的余数,生成“off”进数组 Star.vue ```js props: ['rating','size'] ``` ShopList.vue 给star组件要传2个值,一个是分数,一个是size,因为star模块里面的星星img有很多种尺寸,ShopList用的是24 ```js //注册Star组件 components:{ Star } //使用组件并绑定2个值(父组件向子组件传值) ``` > tips: > > 这么写是错误的,绑定的属性,如果有变量,不能加引号,日了狗: > > ```html > > ``` > > > 始终报错: > > ```js > //模版 > > > //代码 > computed:{ > creatStarsArr(){ > // this.score=this.rating > // let num= this.score.floor() > // if(num>=1){ > // for(let i=0;i<=num;i++){ > // this.starsArr.push('on') > // } > // } > // if(this.score*10-num*10>=5){ > // this.starsArr.push('half') > // } > // if(5-this.starsArr.length>0){ > // for(let i=0;i<=5-this.starsArr.length;i++){ > // this.starsArr.push('off') > // } > // } > this.starsArr.push('on') //push不进去 > } > ``` > > 报错有一个原因,就是floor()找不到,自己sb写错了用法,正确的是: > > ```js > Math.floor(this.score) > ``` > > 仍然错误,于是发现,自己对computed的理解不够 > > 思路是,v-for遍历是主参数是computed创建的函数名,在此函数中return一个数组,作为主参数的内容来遍历 > > 而我用自己的思路,用mounted来实现数据计算,把结果给data中声明的一个数组,用此数组来v-for遍历 > > ```js > data(){ > return{ > score:0, > starsArr:[] > } > } > > mounted:function () { > let starsArr=[] > this.score=this.rating > let num= Math.floor(this.score) > console.log(num) > if(num>=1){ > for(let i=0;i this.starsArr.push('on') > } > } > if(this.score*10-num*10>=5){ > this.starsArr.push('half') > } > if(5-this.starsArr.length>0){ > for(let i=0;i<=5-this.starsArr.length;i++){ > this.starsArr.push('off') > } > } > } > ``` ## Login.vue > tips > > - 要了解 $router 相关的常用方法,如$router.back() 1. 新建文件,Login.vue页面 2. 制作一个左上角回退的功能 ```html 给左上角的<增加一个点击 $router.back() 回退方法 < ``` 3. 制作一个在Login页面底部导航不显示的功能 - 配置路由的时候,加上mata属性 ```js routes: [ {path: '/', redirect: '/Msite', mata: {show: true}}, {path: '/Msite', component: Msite, mata: {show: true}}, {path: '/Search', component: Search, mata: {show: true}}, {path: '/Order', component: Order, mata: {show: true}}, {path: '/Profile', component: Profile, mata: {show: true}}, {path: '/Login', component: Login, mata: {show: false}} ] ``` - 在App.vue 中,给底部导航组件,添加v-show,属性为$router.meta设置的布尔值 - 这样一来,在 FooterGuide 组件中,有v-show来控制底部导航组件是否显示 ```html ``` ## ### 切换登陆方式 显示哪种登录方式是用class中的“ON”来控制的 思路: 1. 使用true/flase来控制ON的显示和隐藏 ```js //data中省名msg:true :class="{on:msg}" :class="{on:!msg}" ``` 2. 短信登陆界面ON,密码界面登陆的就取反 ```js 短信登录 密码登录 ``` 3. 同样,下面的关联div也是如此,使用同一个flag就可以了 手机号验证 1. 给手机号input添加一个v-model,变量为phone(为什么呢?因为接口文档的里面是phone,这样后面提交请求的服务器验证手机号是否存在时方便) ```js ``` 2. 如果手机号输入格式正确,那么获取验证码的颜色由灰变为黑, - 此button的文字颜色是否变化,是根据变量phone来决定,所以此button要有一个计算属性 - 颜色变化用一个类来表示 - 那么就要绑定一个class,内容以对象的形式来添加 > tips:一个模版中能写的变量只有3种:promie、data、computed。什么叫模版中能写的变量呢?就是template里面的 {{ }} 大括号里的内容 ```js //绑定一个class,内容以对象的形势来添加 //此button的文字颜色是否变化,是根据变量phone来决定,所以此button要有一个计算属性 computed:{ phoneNum(){ return /^1\d{10}$/.test(this.phone) } } //颜色变化用一个类来表示 .get_verification position absolute top 50% right 10px transform translateY(-50%) border 0 color #ccc font-size 14px background transparent &.color //在get_verification下面增加&,表示是它的引用,也就是级别在它下面 color black ``` 1. 消除 disabled 的禁用input属性 - disabled 只能接受要么是true要么是NULL,false是无效的 - 错误写法: ```js :disabled="{phoneNum?null:'disabled'}" :disabled="{phoneNum?null:disabled}" ``` - 正确写法: 判断 phoneNum 验证正则的计算方法是否为true ```js :disabled="phoneNum?null:'disabled'" ``` ### 触发“获取验证码”显示,并且增加倒计时 > 如下错误的time()方法,多次点击,仍然触发多次定时器 > > ```js > //模版 > > //代码 > data(){ > return{ > timeNum:'' > } > }, > methods:{ > time(){ > if(tid){ > clearTimeout(tid) > } > this.timeNum=this.timeMax > let tid = setInterval(()=> { > this.timeNum-- > if(this.timeNum<=0){ > clearInterval(tid) > } > },1000) > } > } > ``` > > 正确的代码: > > tips: 0也是属于false > > ```js > time(){ > if(!this.timeNum){ > this.timeNum = 30 > let tid = setInterval(()=> { > this.timeNum-- > if(this.timeNum<=0){ > clearInterval(tid) > } > },1000) > } > } > ``` ### 密码的显示/隐藏切换 1. 圆球的移动 2. 开关背景的变色 3. input的type属性由tel和password切换 ### 图片验证码的点击重刷新 1. 设置一个data属性,叫imgUrl,赋值为API中的验证码请求地址 ```js imgUrl:'http://localhost:4000/captcha' ``` 2. 在标签中绑定src,关联变量imgUrl ```js :src="imgUrl" ``` 3. 给img增加点击事件,每次点击重新给imgUrl赋值 ```js @click="refreshImg" refreshImg(){ return this.imgUrl='http://localhost:4000/captcha?' }j ``` 4. 每次都给imgUrl赋值,是一样的话,就不会生效,可以在后面加上?Data.now(),这样每次链接都不同,但是?后面对请求没什么影响 ```js refreshImg(){ return this.imgUrl='http://localhost:4000/captcha?'+Date.now() } ``` ### 点击登陆后的弹窗验证提示 1. 在组件文件夹内创建 AlertTip 文件夹,创建 AlertTip.vue 文件 2. 模版和代码部分如下 ```js ``` > tips: > > 父元素向子元素传值,如果只传字符串 > > ``` >
> > ``` > > 如果传变量 > > ``` >
> > ``` > > 思路: 1. 不管短信验证码登陆还是账号密码登陆,点击登陆以后,要给 AlertTip 传一个提示信息,如:手机号错误、密码错误等 2. 在登录时要判断是哪个方式登陆,根据不同方式,内部再根据不同方式里面不同的数据,来判断是否登陆成功 实施: 1. 挂载 AlertTip 组件,使用v-show显示隐藏 > tips: > > 不知道为什么,login.vue里面的模版,在section作为模版唯一外层盒子的情况下,在外层还有个div,还有一个class为on的属性。使用AlertTip组件在里面报错,把此div删掉,把AlertTip放置进section里面最下面就可以了 > > ```js > > > data(){ > showTip:false > } > ``` > > 2. 绑定一个属性,给 AlertTip 传值用,一会再用v-bind关联data变量,目的是父元素做判断,子元素显示判断结果登陆错误失败之类的信息 ```html ``` 3. 点击 button 登陆按钮,会提交form表单,因为登陆按钮是在form表单内的,需要阻止表单的提交,并且让表单提交后进入一个方法 ```html
``` 4. 写 login 登陆验证方法 - 需要判断的手机号、密码、用户名等用v-model双向绑定,变量名要和接口的键一样,同时在data中声明 ``` 手机号 phone 短信验证码 code 账号 name 密码 pwd 图片验证码 captcha ``` - 进入login方法中,判断哪种登陆方式,进行验证,用之前data中的msg属性来控制,msg为true的时候,为手机短信登陆,false的时候为账号密码登陆,所以用msg来作为if判断的条件 ``` login(){ if(this.msg){ console.log('手机短信验证码登陆') }else{ console.log('账号密码登陆') } } ``` - 手机短信登陆验证 > 用之前验证手机号位数是否正确的函数,来当作if的条件,因为正好之前 phoneNum() 返回的是真假值 > > 然而如下写报错 > > ``` > if(phoneNum()){ > console.log('号码') > return > } > > ``` > > 正确写法:(调用方法也要加this,后面不用加括号) > > ``` > if(this.phoneNum){ > console.log('号码') > return > } > > ``` > > - 声明一个 tipText 变量,为的是将不同验证的结果传递给它,绑定到 AlertTip 组件当中去使用 ```js //判断哪种登陆方式 if(this.msg){ }else{ } ------- //手机短信登陆方式(if(this.msg)的内层流程) //如果 手机号位数 不正确(使用上面判断手机号位数是否正确的函数),进入流程 if(this.msg){ if(!this.phoneNum){ this.alertTipText('号码不正确') }else if(!/^\d{6}$/.test(this.code)){ this.alertTipText('验证码不正确') } }else{ //如果 验证码位数 不是6位数(没有判断验证码位数的函数,所以在此写一个正则,一样的),进入流程 if(!this.name){ this.alertTipText('账号不正确') }else if(!this.pwd){ this.alertTipText('密码不正确') }else if(!this.captcha){ this.alertTipText('验证码不正确') } } ``` 5. 把 tipText 传递给子组件 ```js ``` 6. Login.vue给子组件传递一个 closeTip 方法,用来让子组件关闭弹窗 ```js //Login.vue closeTip(){ this.tipText = '' this.showTip = false } //AlertTip.vue
确认
methods: { closeTip() { this.$emit('closeTip') } } ``` 7. 账号密码的登陆判断和上面一样,略 ### 荣联云短信平台 > tips: > > 当形参中有多个参数的时候,可以不用写成单独的,因为不方便记顺序,可以写成对象形式,如 > > ```js > function abc (a,b,c,d){ > } > --------------------------- > function abc ({a,b,c,d}){ > } > ``` > > - 网址:www.yuntongxun.com - 账号 306444399@qq.com - 密码 a5604551 - 注册手机 18652176960 1. 左部导航有个测试号码,进入填写测试号码 2. 进入服务器文件夹中的短信接口文件 3. 修改其中的几个值 4. 在后台找到控制台首页,里面有开发者主账号,就可以改了 5. 后台的请求接口是 http://localhost:4000/sendcode?phone=手机号 6. 使用postman测试成功 ### 点击获取验证码,发送短信验证码请求 1. 在获取验证码的time方法中,增加axios请求 ```js import axios from 'axios' //time方法其它内容略 axios.get('http://localhost:4000/sendcode?phone='+this.phone) ``` 2. 给获取验证码按钮点击事件增加一个 .prevent 阻止默认事件,不然会执行整个form表单的login登陆弹窗事件 ``` @click.prevent="time" ``` 3. API文档 4. 请求如果失败,停止计时器,并提示错误信息 > tips: > > - 不在data声明的变量,在方法里this.变量也可以创建变量?(是可以的) > > - 点击一个图片,改变其src属性:使用event事件 > > ```js > function changeSrc(event){ > event.target.src='xxxxxxxxxxxxx' > } > ``` ### 短信登陆(发送登陆请求) > 发送请求以后,总是报错 > > > > 查阅原因说是跨域问题,源代码 > > ```js > axios.post('http://localhost:4000/login_sms',{phone:this.phone,code:this.code}).then(function (rep) { > console.log(rep) > }).catch(function (err) { > console.log(err) > }) > ``` > > 改成(之前上面有跨域配置,url都要改的,忘记了) > > ```js > axios.post('http://localhost:4000/login_sms',{phone:this.phone,code:this.code}).then(function (rep) { > console.log(rep) > }).catch(function (err) { > console.log(err) > }) > ``` > > 请求成功 ### 账号密码登陆(后台账号是abc,密码是123,不然怎么判定是否登陆成功) ```js axios.post('/api/login_pwd',{name:this.name,pwd:this.pwd,captcha:this.captcha}) ``` 增加功能:验证码登陆错误时,自动刷新验证码图片 ### 登陆成功跳转路由到profile > 代码报错 > > ```js > axios.post('/api/login_pwd',{name:this.name,pwd:this.pwd,captcha:this.captcha}).then(function (rep) { > console.log(rep.data) > if(rep.data.code===0){ > this.$router.replace('/Profile') > }else { > > } > }).catch(function (err) { > console.log(err) > }) > ``` > > 原因应该是this问题,改成es6写法,修正this指向问题 > > ```js > axios.post('/api/login_pwd',{name:this.name,pwd:this.pwd,captcha:this.captcha}).then((rep) => { > console.log(rep.data) > if(rep.data.code===0){ > this.$router.replace('/Profile') > }else { > > } > }).catch((err) => { > console.log(err) > }) > ``` 将登录状态存入vuex,来修改登录后,几个地方的显示“未登录”的地方 1. 将登陆后的返回值保存在vuex - 声明变量userInfo ```js //state.js userInfo:{} ``` - 在mutations-type中声明常量 ```js export const SAVE_USERINFO = 'save_userInfo' //存入用户信息 ``` - mutations.js中创建改变state常量中的方法 ```js //引入SAVE_USERINFO import {RECEIVE_ADDRESS,RECEIVE_CATEGORYS,RECEIVE_SHOPS,SAVE_USERINFO} from './mutations-types' //方法 [SAVE_USERINFO](state,userInfo){ state.shops = userInfo } ``` - actions.js中发送请求 ```js //引入SAVE_USERINFO import {RECEIVE_ADDRESS,RECEIVE_CATEGORYS,RECEIVE_SHOPS,SAVE_USERINFO} from './mutations-types' //方法 save_userInfo({commit},userInfo){ commit(SAVE_USERINFO,userInfo) } ``` - 在Login.vue中验证后分发SAVE_USERINFO(其实也可以直接全局操作vuex,直接this.$store.state.userInof = xxxxxx) ```js axios.post('/api/login_pwd',{name:this.name,pwd:this.pwd,captcha:this.captcha}).then((rep) => { if(rep.data.code===0){ //路由页面跳转 this.$router.replace('/Profile') //使用actions分发,让数据存入state this.$store.dispatch('save_userInfo',rep.data.data) }else { this.alertTipText('登陆失败') } } ``` - 短信登陆分发action同↑ 2. Profil(登录/注册) - 引入mapSatate ```js import {mapState} from 'vuex' ``` - 映射userInfo ```js ...mapState(['userInfo']) ``` - 在模版位置中(如果手机登陆就显示手机号,否则就不显示) ```html ``` 3. Msite(右上角) - 引入mapSatate - 映射userInfo - 如果userInfo里面的_id存在就显示icon,不存在就显示登录注册 ```html ``` ### 持久保存vuex登录(刷新不退出) 1. 接口文档 2. node后台实际上设定了session存储为1天,会自动把登陆后的data数据(里面有id和name/phone)存储到session 3. 现在只要发送一个登陆接口请求,就可以获取到上次的登陆session,把值传给vuex里的state.userInfo 4. 既然是持久化登陆,在刷新时就要判定,那么就要在App.vue中的钩子mounted里发送一个 异步 请求 ``` mounted: async function getInfo(){ await axios.get('/api/userinfo').then((res)=>{ if(res.data.code===0){ this.$store.state.userInfo = res.data.data console.log('自动登陆成功了') }else{ console.log('自动登陆失败了') this.$router.replace('/login') } }) } ``` ### 退出登陆功能 1. 使用mint-ui增加退出按钮 ```js ``` 2. 增加点击登出方法,logout 3. 发送登出请求 ```js logout(){ this.$axios.get('/api/logout').then((res)=>{ console.log('登出成功') //跳转到登陆界面 this.$router.replace('/Login') }) } ``` ## MOCK ### 用mock.js模拟接口数据 1. 安装 ``` npm i mockjs -S ``` 2. 现在想要使用mock模拟一个服务器 3. 创建一个mockServer.js文件 4. 在 mockServer.js 引入mock、data.json ``` import Mock from 'mockjs' import data from './data.json' ``` 5. 在 mockServer.js 指定一个url和模版,只要访问了这个url,就会返回这个模版(详见官方文档) ``` Mock.mock('/data',data) ``` 6. 在main.js主文件中引入mock.js文件 ``` import './mock/mockServer' ``` 7. 此时发送给一个ajax请求,res.data的内容,就是data.json里面的内容 ``` this.$axios.get('/data').then((res)=>{ console.log(res.data) } ``` > tips: mock会拦截ajax请求,所以请求的时候url不需要代理 > > - 为什么上面不报错,下面报错 > > 因为初始值是没有数据的,上面是对象的2层,2层是undefined,3层是undefined的里面的值,肯定就报错了。 > > 如何解决,上面div加个v-if,当初始值有的时候,再渲染 > > - computed在两个时候会执行:第一是初始的时候执行一次,第二是内部相关数据发生变化的时候 ### 优化mock接口 1. 让请求的不同地址,返回对应的接口,什么意思呢:目前的接口如下 2. 接口内有商家信息、食物、评价(就是对应了3个子路由组件) 3. 根据不同的请求url,返回不同的接口(其实就是方便调用),为什么要加是code这个键呢,为了模仿真实接口的格式 ```js 把 Mock.mock('/data',data) 改成 Mock.mock('/info',{code:0,data:data.info}) Mock.mock('/goods',{code:0,data:data.goods}) Mock.mock('/ratings',{code:0,data:data.ratings}) ``` 4. 新声明State变量,结合action分发存入 - 在mutatuibs-type中新声明常量 ```js export const RECEIVE_SHOP_INFO = 'receive_shop_info' //接收商家信息 export const RECEIVE_SHOP_GOODS = 'receive_shop_goods' //接收商家产品 export const RECEIVE_SHOP_RATING = 'receive_shop_rating' //接收商家评价 ``` - State中新声明变量(根据data.json来声明类型) ``` shopInfo:{}, //商家信息 shopGoods:[], //商家产品 shopRating:[] //商家中的用户评价 ``` - mutations和actions里面引入常量 - 设置mutations ```js //获取商家信息 [RECEIVE_SHOP_INFO](state,shopInfo){ state.shopInfo = shopInfo }, //获取商家产品 [RECEIVE_SHOP_GOODS](state,shopGoods){ state.shopGoods = shopGoods }, //获取商家评价 [RECEIVE_SHOP_RATING](state,shopRating){ state.shopRating = shopRating } ``` - 设置actions ```js //接收商家信息 receive_shop_info({commit}){ axios.get('/info').then((res)=>{ commit(RECEIVE_SHOP_INFO,res.data.data) }) }, //接收商家产品 receive_shop_goods({commit}){ axios.get('/goods').then((res)=>{ commit(RECEIVE_SHOP_GOODS,res.data.data) }) }, //接收商家评价 receive_shop_rating({commit}){ axios.get('/rating').then((res)=>{ commit(RECEIVE_SHOP_RATING,res.data.data) }) } ``` ## Shop.vue页面 此页面有3个子路由: - ShopGoods - ShopRating - ShopInfo ### 路由配置 1. 配置router ```js {path: '/Shop', component: Shop, meta: {show: true}} ``` 2. 创建page目录下Shop文件夹和Shop.vue 3. 配置shop的子路由,ShopHead、shopInfo、shopGoods、shopRating 4. 在components文件夹下创建ShopHead、shopInfo、shopGoods、shopRating文件夹,同时创建同名vue文件 5. 在router.js中配置Shop子路由,并设置Shop页面的子路由重定向 ```js {path: '/Shop', component: Shop, meta: {show: true},children:[ {path:'/Shop/Head',component:ShopHead}, {path:'/Shop/Goods',component:ShopGoods}, {path:'/Shop/Info',component:ShopInfo}, {path:'/Shop/Rating',component:ShopRating}, {path:'',redirect:'/Shop/Goods'} ]} ``` 6. 在Shop.vue下引入4个子路由,并注册 7. 这个时候静态的样式 > 发现错误 > > url("https://fuss10.elemecdn.com/f/5c/ead54394c3de198d3e6d3e9111bbfpng.png") > > 样式里不能用外链 > > 解决错误, > > (未证实,但是这么做的)把背景图存储在本地common/img文件夹内 > > 1. 在webpack.prod.conf.js文件里output里面添加:publicPath:'./' > > 2.在utils.js文件里添加 publicPath:'../../' > > 3.在config/index.js文件里,添加assetsPublicPath:'./' > > 8. 发现没有样式,排查半天,发现要在标签是加 router-link-active ```html 点餐 ``` ### Shop页面跳转子路由 > 目前出现了问题,就是无法实现Shop页面内的子路由切换 > > 查出了错误,是components在模版设置的时候少加了s,以及傻逼加了name > > 还有我这个傻逼,router打错了 ### 两个子组件的思路 - ShopHead 1. mapState shopInfo(商家信息) 2. 把信息渲染到标签 3. 在ShopHead的钩子mounted中分发 ```js mounted:function () { this.$store.dispatch('receive_shop_info') } ``` 4. 在把数据渲染到标签中(先略) - ShopGoods 1. ShopGoods 组件是一个较复杂的路由组件 2. 内部使用了另外 3 个组件 a. ShopCart: 购物车组件 b. CartControl: 购物车操作组件 c. Food: 食品详情组件 3. 使用第三方库 better-scroll: UI 滑动 ### 丰满ShopGoods 1. 创建静态模版 2. mapState shopInfo(商家信息) 3. v-for把数据渲染到左侧列表 ```html ``` 4. 去掉没有icon的错img ### better-scroll——滑动插件 1. 安装 ``` npm install better-scroll -D ``` 2. 在使用文件中(ShopGoods)引入 ``` import BScroll from 'better-scroll' ``` 3. 在分发数据之后才能调用 better-scroll ,因为要state.shopGoods里面有数据并且v-for渲染到页面再实例化 better-scroll,才有效果 4. 那么什么时候state.shopGoods里有数据呢?就是分发完毕的时候,所以actions里面此处的分发要改成同步等待 ```js //设定一个回调函数,commit存入数据之后来执行 async receive_shop_goods({commit},cb){ let result await axios.get('/goods').then((res)=>{ result = res.data.data }) commit(RECEIVE_SHOP_GOODS,result) //commit存入数据之后来执行 cb && cb() } ``` 5. 在钩子函数mounted中来执行action分发 ```js mounted:function () { this.$store.dispatch('receive_shop_goods',()=>{ this.$nextTick(()=>{ new BScroll('.menu-wrapper') new BScroll('.foods-wrapper') }) }) } ``` > 之前左侧滚动不了,是因为没有增加一个class为content的div,看文档知道 better-scroll 是里面的div来移动 > > 右侧也出现问题,可以滑动,但是渲染出来的结果只有第一个类目和其下面的7个食物,解决办法同上,加了个class为content的div ### 左侧导航和右侧列表移动互相关联 1. 左侧有个current类,来控制颜色变白的选中效果 2. 目标功能: 3. - 点击左侧分类,右边列表可以移动到分类位置、 - 滑动右侧列表,左侧分类响应变化 4. 分析 5. - current类作为左侧导航选中的标识类名 currentIndex - 让滑动的数值与右侧列表渲染好了之后的标题距离顶部的数值计算,于是: - 滑动的数值:scrollY、列表渲染好了之后的标题距离顶部的数值:top - 如何进行计算,傻逼没说清楚 6. 在滑动过程中获取到 scrollY 7. - 基础的参数配置示例 - better-scroll 中有很多事件,用法都是通过 实例.on 来使用 - better-scroll 中拥有的事件,通过上面的 实例.on 来使用 - 为了拿到滑动过程中的Y轴的值,需要用到scroll事件 - 上面说的 选项中的 [probeType](https://ustbhuangyi.github.io/better-scroll/doc/zh-hans/options.html#probetype) 是什么意思呢,就是数值是几,对应的scroll触发机制是什么 - [probeType](https://ustbhuangyi.github.io/better-scroll/doc/zh-hans/options.html#probetype) 属于选项中的配置,如何使用呢?要在实例中使用 ```js let foodScroll = new BScroll('.foods-wrapper',{ probeType:2 }) ``` - on出一个scroll事件 - ```js foodScroll.on('scroll',({x,y})=>{ console.log(y) }) ``` - 至此右侧滑动可以实时检测Y轴的变化 8. 列表渲染好了之后的标题距离顶部的数值:top > tips:回调函数型的方法,可以这样写: _methods 。 下划线的方法名代表回调函数方法 (方便自己看) - 找到所有分类的标签li > 遇见问题: > > this.$refs.foodTop.children,报错,找不到含有ref:foodTop表现的ul下面的子元素 > > this.$refs.foodTop[0].children,这样就可以,找到第一个li > > 解决问题: > > html结构如下 > > ```html >
>
>
    >
  • >

    {{item.name}}

    > ``` > > 要找到
  • 这个标签,获取此标签的top值。上面报错是因为把 ref="foodTop" 加在了ul标签上,那么ul是个数组,数组是不能直接获取下面的DOM节点 > > 把ref加在content上就没问题了 > > ------ > > 遇见问题 > > 这样写forEach报错,因为arrLi是伪数组,查阅资料,伪数组是object类型 > > ```js > let arrLi = this.$refs.foodTop.getElementsByClassName('food-list-hook') > arrLi.forEach((item)=>{ > console.log(item) > }) > ``` > > 解决问题 > > 使用 Array.prototype.slice.call(arrLi) 把伪数组转为真数组 > > ```js > let arrLi = this.$refs.foodTop.getElementsByClassName('food-list-hook') > Array.prototype.slice.call(arrLi).forEach((item)=>{ > console.log(item) > }) > ``` - 把找到的所有标签top值push进一个数组 ```js let foodTops = [] foodTops.push(0) let arrLi = this.$refs.foodTop.getElementsByClassName('food-list-hook') Array.prototype.slice.call(arrLi).forEach((item)=>{ foodTops.push(item.clientHeight) }) ``` > 遇到问题:此时打印foodTops: > > 经过推理,top的值应该是到上一个标签的距离(也就是clientHeight) > > 解决办法:用+=把top值存储进数组 > > ```js > Array.prototype.slice.call(arrLi).forEach((item)=>{ > top += item.clientHeight > foodTops.push(top) > console.log(foodTops) > }) > ``` - 检测scrollY的变化,来找到scrollY在哪个tops之间 - find()方法,来找到大于scrollY值的那个li标签top的值 ```js watch:{ scrollY:function(){ var a a = this.foodTops.find((top)=>{ return top >= this.scrollY }) console.log(a) } } ``` 1. 把检测到的 大于scrollY值的那个li标签top的值 存进data,取名叫current ```js //data: data () { return { scrollY:Number, foodTops:Array, current:Number } } --------------------------------------------------------- //watch: watch:{ scrollY:function(){ this.current = this.foodTops.find((top)=>{ return top >= this.scrollY }) } } ``` 2. 思路:设置一个计算方法,让index===current()的时候给对应的index增加current类名 - 给标签设置一个绑定的class属性(注意indexCurrent是个计算方法,这里不能加括号) ```html