github网址https://github.com/LFY836126/MT-App
慕课实战视频:https://coding.imooc.com/class/280.html
javascript
项目安装:css
解决:使用babelhtml
1. 在package.json文件中更改dev和start,都在配置的末尾加上--exec babel-node 2. 创建.babelrc文件,文件内容为 { "presets": ["es2015"] } 3. 安装插件:npm install babel-preset-es2015
支持axios npm install @nuxtjs/axios前端
nuxt.config.js: modules: [ '@nuxtjs/axios', ], axios: { }, ```
版本介绍:vue
新知识点的网址:java
https://segmentfault.com/a/1190000011557953
/https://www.jianshu.com/p/622561ec7be2
https://segmentfault.com/q/1010000016473273?sort=created
https://www.jianshu.com/p/04e596da7d33
https://www.liaoxuefeng.com/wiki/1022910821149312/1099752344192192
https://www.jianshu.com/p/0d59a4270997
https://www.cnblogs.com/xiaozhumaopao
项目目录node
components changeCity -->城市选择页面的全部 iselect.vue -->按省份选择等,那一栏的 hot.vue -->热门城市 那栏 categroy.vue -->按拼音首字母选择 那栏 products -->产品列表页,就是点击搜索出来的页面 categroy.vue -->分类,区域的部分 crumbs.vue -->中间哈尔滨美团>哈尔滨失恋博物馆 iselect.vue -->分类,区域栏中偏右边的部分,像周边游,香坊区等部分 list.vue --> product.vue --> detail -->产品详情页,就是点击产品出现的页面 crumbs.vue --> item.vue --> list.vue --> summary.vue --> index artistic.vue -->页面下半部分,有格调的那个部分 life.vue -->中间包括轮播图的那部分,几乎全是图片的部分 menu.vue -->所有分类部分 silder.vue -->单独的轮播图组件,在life.vue文件中引用 public header -->包括搜索框往上面的部分 index.vue -->用于导出header下的其余组件 nav.vue -->页面右上角,什么个人美团,网址导航那部分 searchbar.vue -->整个搜索框部分 topbar.vue -->除了搜索框的全部顶部部分 user.vue -->用户登录注册部分 geo.vue -->页面左上角,城市切换部分 footer index.vue -->底部部分 pages index.vue -->中间部分 register.vue -->注册组件 login.vue -->登陆组件 exit.vue -->退出组件 register.vue -->注册组件 changeCity -->城市选择组件 products.vue -->产品列表页 detail.vue -->产品详情页 layout default.vue -->最终显示页面 blank.vue -->放置register.vue,login,exit的模版文件 server dbs models -->放置数据库数据 user.js -->users表,包括usename,password,email categroy.js city.js menu.js poi.js province.js config.js -->数据库配置文件(smtp服务, redis链接, mongodb链接) interface utils axios.js -->定义axios的配置项 passport.js -->利用koa-passport简便的实现登陆注册功能(序列化,反序列化,local策略) users.js --> 登陆系列接口定义(登陆,退出,获取用户名,注册,验证等) geo.js -->城市,系列接口定义(获取全部城市,热门城市,获取省份等) index.js -->定义支持服务的接口文件(passport, session, 路由, 数据库, 处理post请求等) store modules -->vuex子模块 geo.js -->当前城市 home.js -->所有分类下的详细分类 index.js -->vuex模块(汇总子模块而且定义一些操做) redis启动->找到安装目录(develop)->redis-server mongoose启动->找到安装目录(develop)->mongod 支付逻辑在13-1的7.06分处,能够本身写 nuxt.config.js 配置文件:能够引入项目所需文件,像css文件,还能够配置不少其余文件
逻辑目录:ios
layouts/default.vue header组件 topBar Geo User navBar searchBar content:按须要加载 footer组件
思考git
如何节省网络请求github
首先浏览器发出request请求,创建http链接,服务器端能够拿到request.ip,也就是浏览器端向我发起请求的时候,根据http协议,我就能够知道ip地址,而后我拿到ip地址去数据中心作映射,这个ip对应哪一个城市,而后就可拿到城市名称,服务器拿到city以后下发给浏览器
思考:如何节省网络请求?
传统方法:发送两次请求
在请求文档的时候,那个时候服务器已经知道你的ip了,在那个时候,彻底能够拿到ip对应的城市,这个数据是能够当时返回给你的,不须要额外再创建一次链接,利用vuex同步状态,再利用ssr,就能够作到一次请求就能够拿到数据
layouts/default.vue
<template> <el-container class="layout-default"> <el-header height="197px"> <myHeader></myHeader> </el-header> <el-main> <nuxt/> </el-main> <el-footer height="100%"> <myFooter></myFooter> </el-footer> </el-container> </template> <script> import myHeader from '@/components/public/header/index.vue'; import myFooter from '@/components/public/footer/index.vue'; export default { components:{ myHeader, myFooter, } } </script>
footer部分:mt-app/components/public/footer/index.vue
footer注意的地方
在default.vue中引入的时候 <el-footer height="100%"> <myFooter></myFooter> </el-footer> 这个height必定要设置为100%, 不然就出现 只有一部分是灰色 的状况 由于element-ui默认设置为60px,因此咱们要设置为100%,就整个背景都是灰色的了
位置、引入
位置、引入
登陆部分:登陆或者未登陆两种状态
<!-- 登陆 --> <template v-if="user"> 欢迎你 <span class="username">{{user}}</span> <nuxt-link to="/exit" class="exit">退出</nuxt-link> </template> <!-- 未登陆 --> <template v-else> <nuxt-link to="/login" class="login">当即登陆</nuxt-link> <nuxt-link to="/register" class="register">注册</nuxt-link> </template>
nav.vue位置、引入:
个人美团部分
用最简单的dom结构实现比较复杂交互 由于"个人美团" 这部分的内容既要兼顾着同级平行结构 又要有照顾到下面"个人订单"等那部分的内容 因此在这里并不将它和"个人订单"等部份内容放在一个结构里,以下: <li class="list"> <nuxt-link to="/my">个人美团</nuxt-link> <dl> <dd><nuxt-link to="/order">个人订单</nuxt-link></dd> <dd><nuxt-link to="/order">个人收藏</nuxt-link></dd> <dd><nuxt-link to="/order">抵用券</nuxt-link></dd> <dd><nuxt-link to="/order">帐户设置</nuxt-link></dd> </dl> </li> <li> <nuxt-link to="/order">手机APP</nuxt-link> </li> ... ...
网址导航部分
官网上这部分的列表结构是有标题有内容 因此咱们采起利用dl不是ul,,由于dl中dt和dd正好符合标题和内容这样的结构,以下: <li class="list site"> <nuxt-link to="/site">网址导航</nuxt-link> <div class="subContainer"> <dl class="hotel"> <dt>酒店旅游</dt> <dd>国际机票</dd> </dl> <dl class="food"> <dt>吃美食</dt> <dd>烤鱼</dd> </dl> <dl class="movie"> <dt>看电影</dt> <dd>热影电影</dd> </dl> <dl class="app"> <dt>手机应用</dt> <dd> <a href="#"> <img src="//s0.meituan.net/bs/fe-web-meituan/e5eeaef/img/appicons/meituan.png" alt="美团app" title="美团app"> </a> </dd> </dl> </div> </li>
位置、引入
搜索相关逻辑:
 + 相关搜索:聚焦 有内容时显示相关搜索  + 这两个彼此独立,放在平行结构中,具体实现以下: ``` 1. 利用两个变量 (1)是否聚焦 isFocus:false, (2)搜索框内容是否为空 search: '' 2. 利用计算属性监听: (1)isHotPlace:function(){ //已经聚焦而且搜索内容为空的时候显示热门搜索 return this.isFocus&&!this.search }, (2)isSearchList:function(){ //已经聚焦而且搜索内容不为空的时候显示热门 搜索 return this.isFocus&&this.search } 3. 利用v-if决定是否热门搜索要显示 (1)热门搜索栏<dl class="hotPlace" v-if="isHotPlace"> (2)相关推荐栏<dl class="searchList" v-if="isSearchList"> 4. 绑定事件,实现聚焦显示 <el-input placeholder="搜索商家或地点" v-model="search" @focus="focus" @blur="blur"/> focus: function(){ this.isFocus = true; }, blur: function(){ this.isFocus = false }, ```
问题1:当我聚焦后想点击推荐中的连接的时候,会先触发input事件的blur事件,才能点击,因此在点它(连接)以前,已经触发了blur事件,致使你点击这个连接,没有生效
解决:就是我在失去焦点的时候,把isFocus的变化作个延时的处理
blur: function(){ //setInterval和setTimeout中传入函数时,函数中的this会指向window对象,因此用self现将this存起来 let self = this; setTimeout(function(){ self.focus = false },200) }
问题2:我怎么让推荐的内容随着个人输入内容改变,怎么更改数据发出去
`<el-input placeholder="搜索商家或地点"@input="input"/>`
位置、引入:
逻辑:
结构拆分:一级标题 --->所有分类
数据结构: menu: [ { type:'food', name:'美食', id:11, child:[ { title:'美食', child:['火锅', '汉堡', '小龙虾', '烤冷面', '小可爱'] } ] }, ] dom结构: <dt>所有分类</dt> <dd v-for="(item, index) in menu" :key="index"> <i :class="item.type"/>{{item.name}} <span class="arrow"/> </dd>
结构拆分:二级标题 --->子分类(美食,外卖,酒店等)
逻辑:每一个标题下面对应的内容都不同,我怎么肯定当鼠标划过,我应该显示哪一个内容呢
DOM结构: <div class="detail" v-if="kind"> //在每一个分类子项这样遍历 <template v-for="(item,index) in curdetail.child"> <h4 :key="index">{{item.title}}</h4> <span v-for="v in item.child" :key="v"> {{v}} </span> </template> </div> 当鼠标划过所有分类部分,触发事件@mouseenter="enter"->enter事件 enter事件 改变kind值为 鼠标划过当前i元素(好比说叫x) 的className值 enter: function(e){ this.kind = e.target.querySelector('i').className }, 计算属性curdetail,当kind改变,从新计算curdetail的值 computed:{ curdetail: function(){ // 设置过滤器 -> 取到全部type和kind相等数据中的第一个 let res = this.menu.filter(item => item.type === this.kind)[0] return res } }, 此时的curdetail中存储的值 就是x对应menu中的数据,而后在dom中进行渲染 而后鼠标离开所有分类大框后绑定事件,@mouseleave="mouseleave" mouseleave事件:让kind值为空,实现鼠标离开后,分类项下的组件不显示 mouseleave(){ let self = this; let self_time = setTimeout(function(){ //延时的缘由:咱们鼠标移动到分类项下的组件时 //必然:先触发mouseleave事件,而后kind就为'' //由于以前设置组件显示:v-if="kind" //因此此时分类项下的组件又不显示了,就很矛盾,因此这里设置了延迟 self.kind = ''; },150) },
关于鼠标滑动事件的处理:
由于所有分类下的分类项和分类项下的组件是并行结构 也就是我要是鼠标移入到分类项下的组件部分,就算作成移出了所有分类 这样的话,依照以前的原理,mouseleave触发事件令kind值为空,组件就会不显示 也就是说,我无法实现:移动到分类项下的组件 因此要解决这个问题 <div class="detail" v-if="kind" @mouseenter="temEnter" @mouseleave="temLeave"> 给 分类项下的组件 绑定事件 @mouseenter="temEnter" //-->若是从所有分类出来,移入到是子分类,就将定时器清除,kind不为'' temEnter: function(){ clearTimeout(this._timer), }, @mouseleave="temLeave" //-->若是从所有分类移出来,不是移入子分类,那就将kind改变为空,不显示子分类 temLeave: function(){ this.kind = '' }
位置、引入:
中间轮播图部分:
https://element.eleme.cn/#/zh-CN/component/carousel
位置、引入
编写组件
建立组件pages/register.vue
1. 表单样式:参见 Element-UI:https://element.eleme.cn/#/zh-CN/component/form 2. 表单数据见代码里的data 3. 中间有个表单验证规则 一个就是:name,emial什么的都不为空 还有一个验证两次密码相不相等的逻辑 // 二次验证,对比两次密码的内容,须要内置一个函数,支持验证函数的自定义 // validator是一个函数,函数的第一个是rule规则,第二个是value值,第三个是回调 validator:(rule, value, callback) => { if(value === ''){ callback(new Error('请再次输入密码')) }else if(value != this.ruleForm.pwd){ callback(new Error('两次输入密码不一致')) }else{ callback() } }, trigger:'blur'
使用模板
export default { layout:'blank', } ``` + 建立模板缘由: 由于这个注册组件样式上并不须要header和footer,因此不能使用咱们配置好的默认模板:default.vue,要新建一个blank.vue的空模板
数据库设计:
server dbs models -->放置数据库数据 user.js -->users表,包括usename,password,email config.js -->数据库配置文件(smtp服务, redis链接, mongodb链接)
axios和passport.js配置关键代码:
server/interface/utils/passport.js:
配置简单表单验证,具体能够上网找关于passport相关语法
// passport是全部的node程序均可以应用的,koa-passport是对它进行了一个封装,适配koa的 import passport from 'koa-passport' // passport-local是passport本地的一个策略 import LocalStrategy from 'passport-local' import UserModel from '../../dbs/models/users' // 第一个参数是一个函数,函数又有三个参数username, password,和回调函数done passport.use(new LocalStrategy(async function(username, password, done){ // console.log(username, password);// 这个username和password就是注册后进行登陆操做,传给signin的参数,也就是我刚刚注册的账户名和密码 // 设置查询条件 let where = { username, }; // 利用模型 let result = await UserModel.findOne(where) if(result != null){ // 根据用户名查出来库里存储的该用户对应的密码,判断是否和当前用户输入的密码同样 if(result.password === password){ return done(null, result) }else{ return done(null, false, '密码错误') } }else{ return done(null, false, '用户不存在') } })) // 若是每次用户进来的时候,都自动经过session去验证 // passport提供的这两个api是固定用法,是库里封装好的api // 序列化:序列化指的是把用户对象存到session里 passport.serializeUser(function(user, done){ // 我查到用户登陆验证成功以后,会把用户的数据存储到session中 done(null, user); }) // 反序列化:从session里取用户数据成对象,session 多是存数据库的或者写文件里的 passport.deserializeUser(function(user, done){ // 在每次请求的时候,会从session中读取用户对象 return done(null, user); }) // 登陆验证成功了,我把数据打到cookies中,由于http通讯是没有状态的,session是存储在cookies中,存在浏览器端,下次再进来的时候,我会从cookies中把你的session的信息提出来,和服务端的session作验证对比,若是能找到的话,就说明这我的是登陆状态,从而达到一个无状态到有状态的转变 export default passport
server/interface/utils/axios.js:
请求路径,网页等,具体能够上网找关于axios相关知识点
import axios from 'axios' const instance = axios.create({ //{process.penv.HOST||'localhost'}:判断当前环境变量的主机,若是host没有设置的话,默认取本机 //{process.env.POST||3000}:判断端口,若是没有的话,设置为3000 baseURL: `http://${process.env.HOST||'localhost'}:${process.env.PORT||3000}`, // 设置超时 timeout:2000, headers:{ } }) export default instance
简要接口介绍,具体见代码:server/interface/users.js
/users/signup 注册接口 /users/signin 登录接口 /users/verify 发送验证码接口 /users/exit 退出 /users/getUser 登录状态获取用户名
在server/index.js中引入路由:
import users from './interface/users' app.use(users.routes()).use(users.allowedMethods())
将axios和passport和users接口在server/index.js中引入
1. 引入: import mongoose from 'mongoose' // 处理和post相关的请求的包 import bodyParser from 'koa-bodyparser' // 操做session的包 import session from 'koa-generic-session' import Redis from 'koa-redis' ... ... 2. 注册: app.use(session({ key : 'mt', prefix: 'mt:uid', store: new Redis() })) // 扩展类型的配置 app.use(bodyParser({ extendTypes: ['json', 'form' , 'text'] })) // passport相关配置 app.use(passport.initialize()) app.use(passport.session()) ... ...
在上述后台配置结束后,在pages/register组件中定义方法,实现注册逻辑
发送验证码:sendMsg
1. 先验证用户名,密码是否符合规则 2. 若是符合规则,将用户输入的用户名(username)和密码(email)做为参数,请求/users/verify接口
注册:register
1. 判断全部校验逻辑是否正确 2. 将用户输入的:username, password, email, code做为参数,请求接口/users/signup 3. 注意:将password利用crypto-js插件进行加密后再传入, password: CryptoJS.MD5(self.ruleForm.pwd).toString(), 4. 注册成功,跳转到登陆页面 location.href = '/login' 5. 注意:定时将错误信息清空,不然会给用户带来误导 setTimeout(function(){ self.error = '';
实现登陆逻辑pages/login.vue
登陆login方法:
1. 将登陆页面用户输入的username和password做为参数,请求接口/users/signin 2. 一样,密码须要加密 self.$axios.post('/users/signin', { username : window.encodeURIComponent(self.username), password : CryptoJS.MD5(self.password).toString() }) 3. 请求成功跳转到主页面 location.href="/"
跳转到主页面后,实现 左上角"当即登陆" -> "用户名"
users/components/public/header/user.vue
1. 咱们已经定义了接口/users/getUser,经过请求这个接口就能获取到用户的用户名 2. 可是咱们用何时请求接口呢,有两种方式: (1) 在vuex中同步这种状态, (2) 不增长SSR负担,在组件中页面渲染完毕以后 咱们再去获取接口,咱们这里用异步获取 在mounted生命周期:组件挂载到页面,渲染完毕再去请求,达到异步获取的效果
退出逻辑pages/exit.vue
利用中间件
问:退出(exit.vue)组件中,为何用中间件来实现退出操做呢, 答: 由于,咱们点击users/components/public/header/user.vue文件中的退出后 跳转到 退出页面(page/exit.vue)以后,自动的去执行退出操做 因此利用middleware机制,触发这个获取退出的接口,让这个接口响应完以后, 咱们再作自动化的执行动做 ```
获取数据获取有两种方式:
数据库:
数据库数据的导入
1. 进入到mongodb数据库安装位置 2. 执行:mongoimport d student -c areas areas.dat
举个栗子:使用数据库中的数据
server/interface/geo.js: import City from '../dbs/models/city' router.get('/province', async(ctx) =>{ let province = await Province.find() ctx.body = { province: province.map(item =>{ return { id: item.id, name: item.value[0] } }) } }) city.js import mongoose from 'mongoose' const Schema = mongoose.Schema const City = new Schema({ id: { type: String, require: true }, value: { type: Array, require: true } }) export default mongoose.model('City', City) ``` + 经过别人的接口获取全部城市数据 - 接口: ``` http://cp-tools.cn/sign sign = 7296092/4224626 ``` - 举个栗子 ``` import axios from './utils/axios' const sign = '3e59babc3d4d2e7bc9a5b4fe302d574e' router.get('/province', async(ctx) =>{ let {status, data: {province}} = await axios.get(`http://cp-tools.cn/geo/province?sign=${sign}`) ctx.body = { province: status === 200 ? province : [] } }) ``` + 咱们这里全部数据获取都主要用接口的方式,能够本身练习一下数据库的方式
简要接口介绍,具体见代码:server/interface/geo.js
简要接口介绍:
/geo/getPosition 在接口发出请求到服务端,服务端根据当前的ip来查库,给出你当前城市的名称 /geo/province 获取省份的接口 /geo/province/:id 给出你指定的id的省份,每个省份都有一个对应的id,根据id能够查询到这个省份下面全部管辖的城市 /geo/city 获取全部城市(不是按省份分类的城市) /geo/hotCity 获取热门城市 /geo/menu 获取所有分类下的菜单数据 接口测试工具:postman
在server/index.js中引入路由
import geo from './interface/geo' app.use(geo.routes()).use(geo.allowedMethods())
如何将接口反映到城市上去
两种办法: (1)直接在组件中请求接口,经过异步的方式,而后更改dom (2)用SSR方式,在服务端渲染的时候,拿到接口的值,返回页面,用户体验更高,由于过来的时候已经带来告终果
获取当前城市,经过SSR方式渲染在初始页面的左上角:
建立文件:
store modules -->vuex子模块 geo.js -->当前城市 index.js -->vuex模块(汇总子模块而且定义一些操做)
逻辑
1. 在store/modules/geo.js中定义 改变位置的actions和mutations ->setPosition 2. 在store/index.js中引入geo.js 3. store/index.js中请求接口/geo/getPosition ---> 获得当前位置 4. 将获得的位置提交到vuex 5. components/public/header/geo.vue下使用数据
获取所有分类下的子类,经过SSR方式渲染到components/index/menu.vue
建立文件:
store modules -->vuex子模块 geo.js -->当前城市 home.js -->所有分类下的子类,和热门城市 index.js -->vuex模块(汇总子模块而且定义一些操做) ``` + 逻辑 ``` 1. 在store/modules/home.js中定义 actions和mutations setMenu 主页左边所有分类的子类 2. 在store/index.js中引入home.js 3. store/index.js中 请求接口/geo/menu ---> 获得全部子类 4. 将获得的子类数据 提交到vuex 5. components/index/menu.vue下使用数据 上面dom数据渲染改成:(item, index) in $store.state.home.menu 下面计算属性curdetail改成 let res = this.$store.state.home.menu.filter(item => item.type === this.kind)[0] ```
其余须要了解知识点
https://www.cnblogs.com/jielin/p/10258316.html
https://coding.m.imooc.com/questiondetail.html?qid=101986
关于axios.get,axios.post,router.get/post
axios.get: 请求页面获取数据 axios.post: 经过传递参数,请求页面获取数据的 router.get/post: 对于请求这个路由的浏览器,服务端返回给浏览器的数据
简要接口介绍,具体见代码:server/interface/search.js
接口
/search/top /search/resultsByKeysWords 根据任何一个关键词能够查出来全部相关的列表 /search/hotPlace 热门景点/热门搜索 /search/products 查询列表,咱们点击某一个关键词并进入后,它会在产品列表页推荐全部的产品 /search/products/:id 根据每一个产品的id查询这个产品的详情
在server/index.js中引入路由
import geo from './interface/geo' app.use(geo.routes()).use(geo.allowedMethods())
搜索:经过调用接口直接返回数据
注意:每输入一个字母都进行一次请求,显然浪费性能,因此引入lodash插件
import _ from 'lodash' // 只有在最后一次点击的300ms后,真正的函数func才会触发。 input: _.debounce(async function(){ let self = this; // 将后面的那个市字去掉, 由于第三方服务的限制,带着这个字就查不到 let city = self.$store.state.geo.position.city.replace('市', ''); self.searchList = []; let {status, data:{top}} = await self.$axios.get('/search/top', { params: { input : self.search, city } }) // 数据截取十条 self.searchList = top.slice(0, 10) }, 300)
https://segmentfault.com/a/1190000015312430
问题:Error: timeout of 1000ms exceeded
在axios.js配置文件中timeout改成2000
热门城市推荐,经过SSR方式渲染到components/public/header/searchbar.vue
定义 获取数据接口:server/interface/search.js
router.get('/hotPlace', async (ctx)=>{ let city = ctx.store?ctx.store.geo.position.city: ctx.query.city; let {status, data:{result}} = await axios.get(`http://cp-tools.cn/search/hotPlace`, { params: { sign, // 服务端没有作编码的要求,因此这里咱们不用编码 city: city, } }) ctx.body = { result: status === 200? result : [] } })
将热门城市数据存到vuex中
建立文件:
store modules -->vuex子模块 geo.js -->当前城市 home.js -->所有分类下的子类,和热门城市 index.js -->vuex模块(汇总子模块而且定义一些操做)
存储步骤:
1. 在store/modules/home.js中定义 actions和mutations setHotPlace 热门推荐 2. 在store/index.js中引入home.js 3. store/index.js中 请求接口/search/hotPlace ---> 获得全部热门城市
用vuex中的数据从新渲染searchbar.vue中的热门推荐
1. 第一个改动: <dt>热门搜索</dt> <dd v-for="(item, index) in $store.state.home.hotPlace.slice(0, 5)" :key="index"> <a :href="'/products?keyword='+encodeURIComponent(item.name)">{{item.name}}</a> </dd> 2. 第二个改动: <p class="suggest"> <a :href="'/products?keyword='+encodeURIComponent(item.name)" v-for="(item, index) in $store.state.home.hotPlace.slice(0, 5)" :key="index">{{item.name}}</a>
有格调部分components/index/artistic.vue,直接经过接口获取数据并渲染
渲染:
1. 鼠标划过触发over事件 over事件: 1) 获得鼠标划过当前元素的kind值和keyword值 2) 把keyword和city(从vuex中取)做为参数传到/search/resultsByKeywords中获取数据 3) 将获得的数据作一个过滤,必须有图片的才能显示 4) 将获得的数据再作一个格式化,获得咱们渲染dom须要的格式 2. 设置一个默认显示: 由于这个over事件是鼠标滑动才执行的 也就是若是我初始化页面,鼠标没有滑动,那么此时什么都不显示 这不是咱们所指望的 解决:在mounted中就发送一次请求,让页面显示数据
#### 城市选择页面:changeCity
位置、引入
changeCity中组件
components:{ iSelect, Hot, Categroy } ``` + 这个页面的难点 - 拼音首字母怎么写,若是写26个英文字母标签再插入,是很失败的 - 如何经过后端给定接口,返回城市后,根据字母来分类 + 一个字母对应城市的显示 + 点击字母,快速定位到该字母对应的全部城市
位置、引入:
逻辑:
https://element.eleme.cn/#/zh-CN/component/input
将省份和城市作关联(利用watch监听属性),根据省份获取城市(利用axios)
省份: <el-select v-model="pvalue" placeholder="请选择"> 城市: <el-select v-model="cvalue" placeholder="请选择" :disabled="!city.length"> 联系: 根据pvalue找到该省的全部城市,城市结构的显示 依赖于该省全部城市的长度 这样就实现了城市和省份相关联 watch:{ pvalue: async function(newPvalue){ let self = this; let {status, data:{city}} = await self.$axios.get(`geo/province/${newPvalue}`); if(status == 200){ self.city = city.map(item =>{ return { value:item.id, label:item.name, } }) // 切换省份以后,将上一次选择的城市的值清空 self.cvalue=''; } } }
在页面被加载以前将全部省份获取过来,(mounted时候,axios请求数据)
mounted: async function(){ let self = this; let {status, data:{province}} = await self.$axios.get(`geo/province`); self.province = province.map(item =>{ return { value: item.id, label: item.name } }) },
直接搜索部分,数据的处理,利用延时处理lodash的debounce函数
DOM结构: <el-autocomplete v-model="input" :fetch-suggestions="querySearchAsync" placeholder="请输入城市中文名或拼音" @select="handleSelect" ></el-autocomplete> 引入lodash:import _ from 'lodash' 两个事件: fetch-suggestions="querySearchAsync" -> 用户输入内容的时候触发的事件 @select="handleSelect" -> 当列表被点击选中的时候,触发这个方法 querySearchAsync:_.debounce(async function(query, cb){ 1. 若是cities有值的话,直接在cities里面搜索 2. 若是citie没有值的话,从geo/city接口获取数据 3. 将获取到的数据格式化,咱们只须要value值
}, 200), handleSelect:function(item){ 1. 将当前城市设置为item 2. 跳转页面,回到初始页 } ``` + 注意:直接搜索 范围是全国
位置、引入:
逻辑:
在mounted声明周期函数中获取数据渲染
async mounted(){ let {status, data:{hots}} = await this.$axios.get(`/geo/hotCity`) if(status == 200){ this.list = hots; } }
位置、引入:
逻辑:
肯定显示字母用的节点,利用dl dt dd,举个栗子:
<dl class="m-categroy"> <dt>按拼音首字母选择</dt> <dd v-for="item in list" :key="item"> <!-- 由于点击字母要实现跳转,因此要用连接 --> <a :href="'#city-'+item">{{item}}</a> </dd> </dl>
点击字母,快速定位到该字母对应的全部城市->利用a标签的#,以下
遍历字母: <dl class="m-categroy"> <dt>按拼音首字母选择</dt> <dd v-for="item in list" :key="item"> <!-- 由于点击字母要实现跳转,因此要用连接 --> <a :href="'#city-'+item">{{item}}</a> </dd> </dl> 遍历每一个字母对应的城市: <dl v-for="item in block" :key="item.title" class="m-categroy-section"> <dt :id="'city-'+item.title">{{item.title}}</dt> <dd> <span v-for="c in item.city" :key="c">{{ c }}</span> </dd> </dl> 上面的href和下面的id实现定位
左侧字母,右侧城市部分,选择合适的数据格式,有利于dom结点的减小
data(){ return{ list:'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''), // block用来存储 后面用字母 分类城市部分数据,title表明字母,city表明该字母对应城市 // block:[title, city:[]] block:[], } },
全部城市获取利用接口/geo/city
let {status, data:{city}} = await self.$axios.get('/geo/city');
将每一个字母对应的城市选择出来, 将数据改成须要的格式,将字母连带着城市进行排序显示
汉语和拼音的转化:利用库
1. 引入: import pyjs from 'js-pinyin'
将字母对应城市选择出来
city.forEach(item => { // pyjs.getFullChars->拼音这个库本身自己的api,拿到这个参数的拼音全拼 // toLocaleLowerCase().slice(0, 1) ->转小写,而后拿到首字母 p = pyjs.getFullChars(item.name).toLocaleLowerCase().slice(0, 1); // 拿到p的unicode值 c = p.charCodeAt(0); // 若是没有这个字母的话,就建立一个新的 if(!d[p]){ d[p] = []; } d[p].push(item.name); }) ``` - 将获得的数据由对象格式变为数组 ``` for(let [k, v] of Object.entries(d)){ // 这个k和v就是[key, value] // for of 上网查 blocks.push({ title: k.toUpperCase(), city: v, }) } ``` - 将字母排序显示 ``` blocks.sort((a, b)=>a.title.charCodeAt(0) - b.title.charCodeAt(0)) self.block = blocks; ```
位置、引入
products中组件
components:{ Crumbs, ->哈尔滨美团哈尔滨哈尔滨融创乐园 Categroy, ->分类,区域的部分 List, -> 智能排序,景点详情部分 Amap ->地图 }
逻辑:pages/products.vue
经过SSR方式拿数据,举个栗子:
async asyncData(ctx){ let keyword = ctx.query.keyword; let city = ctx.store.state.geo.position.city.replace('市','') || "哈尔滨"; // count:一共多少条数据 // pois:数据 let {status,data:{count,pois}} = await ctx.$axios.get('/search/resultsByKeywords',{ params:{ keyword, city, } }) }
将获取到的数据进行
没有通往这个页面的入口,就是能触发 访问localhost:3000/products 操做的地方
在components/public/header/searchbar.vue中更改
两个热门搜索,一个搜索列表,添加 相似以下语句 <a :href="'/products?keyword='+encodeURIComponent(item.name)">{{item.name}}</a>
待实现功能:
位置、引入
逻辑:
https://element.eleme.cn/#/zh-CN/component/breadcrumb
数据:vuex中取数据
{{ $store.state.geo.position.city.replace('市','') }}美团 {{ $store.state.geo.position.city.replace('市','') }}{{ decodeURIComponent(keyword) }}
位置、引入
categroy中组件
components:{ iselect ->下拉框(划过酒店住宿,周边游出现的下拉框) }
逻辑:
DOM结构:
<dl class="classic"> <dt>分类</dt> <dt>所有</dt> <dd v-for="(item,idx) in types" :key="idx"> <-- 下拉框(划过酒店住宿,周边游出现的下拉框) --> <iselect :name="item.type" :list="item.module"/> </dd> </dl>
下拉框 components/products/iselect.vue
DOM结构:举个栗子:酒店住宿
<dl class="tab"> <!-- dt:酒店住宿 --> <dt>{{ name }}<i class="el-icon-arrow-down el-icon--right"/></dt> <dd> <!-- h3:酒店住宿 --> <h3>{{ name }}</h3> <!-- span:所有 公寓民宿 多人出行 --> <span v-for="(item,idx) in list" :key="idx">{{ item }}</span> </dd> </dl>
位置、引入
list中组件
import Item from './product.vue' components:{ Item ->每一个景点的简要介绍:像几颗星,门票价格,位置等 }
逻辑:
DOM结构
每一个景点的信息利用组件(item)循环输出,每一个item包括图片,描述等信息
1. 智能排序 价格排序 人气最高 评价最高 <dd v-for="item in nav" :key="item.name" :class="[item.name,item.acitve?'s-nav-active':'']" @click="navSelect" >{{ item.txt }}</dd> 2. 景点的简要介绍:Item(import Item from './product.vue') <Item v-for="(item,idx) in list" :key="idx"
+ 景点的简要介绍:components/products/product.vue  - DOM结构:参见Element-UI: `https://element.eleme.cn/#/zh-CN/component/rate` - 数据:父组件传递 #### 地图控件Amap
位置、引入
https://lbs.amap.com/api/javascript-api/guide/overlays/toolbar
位置、引入
detail.vue中组件
components:{ Crumbs, ->哈尔滨美团 > 哈尔滨美食 > 哈尔滨火锅 Summa, ->商品详情 List ->商家团购及优惠下的列表 } ``` + 跳转到该路由的连接:components/products/product.vue ``` <h3><nuxt-link :to="{path:'detail',query:{keyword:meta.name,type:meta.module}}">{{ meta.name }}</nuxt-link></h3> ```
逻辑:
判断是否显示:商家团购及优惠,显示的条件是登陆或者有数据,利用v-if实现
<el-row v-if="canOrder || !login"> <el-col :span="24"> <!-- 下面这两个list和div是平行结构,只能有一个显示 --> <!-- 若是登陆显示list组件 --> <list v-if="login" :list="list"/> <!-- 若是没登陆,显示未登陆 --> <div v-else></div> </el-col> </el-row>
思考:访问(详情页)localhost:3000/detail.vue时的请求参数:keyword,type,
为何不在data中获取,而是asyncData中
在访问localhost:3000/detail.vue时的请求参数keyword,type 只能经过:let {keyword,type}=ctx.query,在服务器端获取到 而asyncData中正好是在服务器端执行的, 因此写在asyncData中 代码见:pages/detail.vue中
在detail.vue中请求/search/products后
(请求回来的数据传递路线: detail.vue->list.vue->item.vue)
返回数据格式以下缘由:和data关联,因此,返回数据后,data就不用一样再写一次了
return { keyword, product, type, list, login }
位置、引入
list.vue中的组件
components:{ item ->每条数据 }
逻辑:
DOM结构:
<ul> <li>{{ list.filter(item=>item.photos.length).length }}款套餐</li> <item v-for="(item,idx) in list" :key="idx" :meta="item" /> </ul>
数据的获取:两种方式
SSR:我在页面下发的时候就把数据塞进去了
item组件(components/details/item.vue)
用于渲染DOM结构的数据获取:
pages/detail.vue请求接口/search/products 将数据传递给components/details/list.vue list.vue将数据传递给item组件
点击抢购商品,建立购物车
1. 请求接口/cart/create:建立购物车,将刚建立的购物车id返回 2. 建立成功后,根据购物车id跳转到购物车页面->pages/cart.vue 3. 补充: 实际应用中,浏览器传给服务端一个产品的id 而后这个id对应产品库中的某个商品 而后再将该商品的名称,价钱等信息传给服务端, 可是咱们这里没有真正的产品库,因此 只能经过 直接传给服务端商品的名称,价钱等信息 来获取服务器端对应的数据 这样的方式
建立购物车接口::server/interface/cart.js->/cart/create
接口实现功能: 1. 登陆验证 2. 将购物车信息存入数据库中 3. 将建立好的购物车id返回给客户端 注册路由,让路由生效 server/index.js中: import cart from './interface/cart' app.use(cart.routes()).use(cart.allowedMethods()) ```
位置、引入
cart.vue中组件
components:{ list ->订单列表 }
跳转到该路由的连接:components/details/item.vue
window.location.href=`/cart/?id=${id}`
逻辑
DOM结构:设计一个平行结构,考虑购物车为空和不为空的两种状况
<el-row class="page-cart"> <!-- 购物车不为空的时候 --> <el-col v-if="cart.length" :span="24" class="m-cart"> ... ... <list :cart-data="cart"/> ... ... </el-col> <!-- 购物车为空的时候 --> <el-col v-else class="empty">购物车为空</el-col> </el-row>
 - DOM结构:参见Element-UI: `https://element.eleme.cn/#/zh-CN/component/table` - 数据: ``` 父组件pages/cart.vue经过SSR获取数据(经过这个接口:/cart/getCart) 传给子组件list.vue 全部订单数据,由子组件所有渲染出来 ``` - 逻辑: ``` 父组件经过接口获取数据,传入子组件数组,存储在cartData中, 子组件经过Element-UI结构渲染数据, 若是我在子组件中更改了购买商品的数量,也就是cartData中的值被更改了, 那么,咱们在父组件监听的total(全部订单总价),也就会从新计算 而后从新渲染父组件中 下面这个结构中的数据 <p> 应付金额:<em class="money">¥{{ total }}</em> </p> ``` - 注意:仔细看一下list.vue的数据计算和DOM结构!有一部分须要好好理解 + 提交订单:点击"提交订单",请求/order/createOrder接口,若是请求成功,跳转页面至所有订单页
位置、引入
detail.vue中组件
components:{ List ->订单列表 }
跳转到该路由的连接:pages/cart.vue
this.$alert(`恭喜您,已成功下单,订单号:${id}`, '下单成功', { confirmButtonText: '肯定', callback: action => { location.href = '/order' } }) }
建立订单和返回所有订单接口:server/interface/order.js
/order/createOrder接口实现功能: 1. 根据请求接口的参数的:id(购物车id), price, count加上一些其余参数建立订单
/order/getOrders返回数据库中所有订单 最后:注册路由,让路由生效 server/index.js中: import order from './interface/order' app.use(order.routes()).use(order.allowedMethods()) ```
逻辑
https://element.eleme.cn/#/zh-CN/component/tabs
获取所有订单,经过SSR方式渲染到pages/order中的list组件(components/order/list.vue)
async asyncData(ctx) { const { status, data: { code, list }} = await ctx.$axios.post('/order/getOrders') if (status === 200 && code === 0 && list.length) { return { // 将后端返回数据和前端数据进行映射 list: list.map(item => { return { img: item.imgs.length ? item.imgs[0].url : 'https://i.loli.net/2019/01/10/5c3767c4a52de.png', name: item.name, count: 1, total: item.total, status: item.status, statusText: item.status === 0 ? '待付款' : '已付款' } }), } } }
点击"所有订单"或者"待付款"或者"待使用"等,样式和数据对应改变
点击元素,触发handleClick事件 handleClick(tab) { this.activeName = tab.name } 监听activeName,若是改变,则改变数据 activeName(val) { //cur就是传递给当前应该显示的数据,默认是所有 this.cur = this.list.filter(item => { if (val === 'unpay') { return item.status === 0 } else if (val === 'all') { return true } else { return false } }) },