github网址MT-PC
1. 在package.json文件中更改dev和start,都在配置的末尾加上--exec babel-node
2. 建立.babelrc文件,文件内容为
{
"presets": ["es2015"]
}
3. 安装插件:npm install babel-preset-es2015
4. 重启服务 npm run dev
nuxt.config.js:
modules: [
'@nuxtjs/axios',
],
axios: {
},
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文件,还可以配置很多其他文件
layouts/default.vue
header组件
topBar
Geo
User
navBar
searchBar
content:按需要加载
footer组件
在default.vue中引入的时候
这个height一定要设置为100%, 否则就出现 只有一部分是灰色 的情况
因为element-ui默认设置为60px,所以我们要设置为100%,就整个背景都是灰色的了
欢迎你 {{user}}
退出
立即登录
注册
用最简单的dom结构实现比较复杂交互
因为"我的美团" 这部分的内容既要兼顾着同级平行结构
又要有照顾到下面"我的订单"等那部分的内容
所以在这里并不将它和"我的订单"等部分内容放在一个结构里,如下:
-
我的美团
我的订单
我的收藏
抵用券
账户设置
-
手机APP
...
...
官网上这部分的列表结构是有标题有内容
所以我们采取利用dl不是ul,,因为dl中dt和dd正好符合标题和内容这样的结构,如下:
-
网址导航
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)热门搜索栏
(2)相关推荐栏
4. 绑定事件,实现聚焦显示
focus: function(){
this.isFocus = true;
},
blur: function(){
this.isFocus = false
},
blur: function(){
//setInterval和setTimeout中传入函数时,函数中的this会指向window对象,所以用self现将this存起来
let self = this;
setTimeout(function(){
self.focus = false
},200)
}
数据结构:
menu: [
{
type:'food',
name:'美食',
id:11,
child:[
{
title:'美食',
child:['火锅', '汉堡', '小龙虾', '烤冷面', '小可爱']
}
]
},
]
dom结构:
- 全部分类
-
{{item.name}}
DOM结构:
//在每个分类子项这样遍历
{{item.title}}
{{v}}
当鼠标划过全部分类部分,触发事件@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值为空,组件就会不显示
也就是说,我没法实现:移动到分类项下的组件
所以要解决这个问题
给 分类项下的组件 绑定事件
@mouseenter="temEnter"
//-->如果从全部分类出来,移入到是子分类,就将定时器清除,kind不为''
temEnter: function(){
clearTimeout(this._timer),
},
@mouseleave="temLeave"
//-->如果从全部分类移出来,不是移入子分类,那就将kind改变为空,不显示子分类
temLeave: function(){
this.kind = ''
}
休闲生活、住酒店、我是商家,登录,二维码部分:life.vue
- 位置、引入:
- 位置:components/index/life.vue
- 在pages/index.vue中引入
- 中间轮播图部分:
- 位置:components/index/silder.vue
- 在components/index/life.vue中被引入
- 写法:参照Element-UI
https://element.eleme.cn/#/zh-CN/component/carousel
注册组件:register
- 位置、引入
- 位置:pages/register.vue
- 访问 localhost:3000/register
- 编写组件
- 创建组件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'
- 创建模板:layouts/blank.vue
- 使用模板
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 = '';
}, 1500)
- 实现登录逻辑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.vue1. 我们已经定义了接口/users/getUser,通过请求这个接口就能获取到用户的用户名
2. 但是我们用什么时候请求接口呢,有两种方式:
(1) 在vuex中同步这种状态,
(2) 不增加SSR负担,在组件中页面渲染完毕之后
我们再去获取接口,我们这里用异步获取
在mounted生命周期:组件挂载到页面,渲染完毕再去请求,达到异步获取的效果
请求接口,我们可以用promise.then,也可以用async和await,我们这里用async,await
- 退出逻辑pages/exit.vue
- 利用中间件
问:退出(exit.vue)组件中,为什么用中间件来实现退出操作呢,
答: 因为,我们点击users/components/public/header/user.vue文件中的退出后
跳转到 退出页面(page/exit.vue)之后,自动的去执行退出操作
所以利用middleware机制,触发这个获取退出的接口,让这个接口响应完之后,
我们再做自动化的执行动作
- 补充:开启SMTP服务
关于数据
- 获取数据获取有两种方式:
- 数据库:
- 数据库数据的导入
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下使用数据
{{$store.state.geo.position.city}}
- 获取全部分类下的子类,通过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]
- 其他需要了解知识点
- vuex
- Nuxt工作流部分的nuxtServerInit
- 项目总结:
https://www.cnblogs.com/jielin/p/10258316.html
- 实战问答
https://coding.m.imooc.com/questiondetail.html?qid=101986
(通过更改qid后面的内容查看问答)
- 关于axios.get,axios.post,router.get/post
axios.get: 请求页面获取数据
axios.post: 通过传递参数,请求页面获取数据的
router.get/post: 对于请求这个路由的浏览器,服务端返回给浏览器的数据
- 如何判断SSR效果是不是正确:通过查看源码,因为这个是服务端打回给自己的模板
搜索相关:接口,搜索逻辑等
- 简要接口介绍,具体见代码: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)
- lodash详解:
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 ---> 得到所有热门城市
4. 将得到的子类数据 提交到vuex
- 用vuex中的数据重新渲染searchbar.vue中的热门推荐
1. 第一个改动:
- 热门搜索
-
{{item.name}}
2. 第二个改动:
- 有格调部分components/index/artistic.vue,直接通过接口获取数据并渲染
- 接口:server/interface/search.js:/search/resultsByKeysWords
- 渲染:
1. 鼠标划过触发over事件
over事件:
1) 得到鼠标划过当前元素的kind值和keyword值
2) 把keyword和city(从vuex中取)作为参数传到/search/resultsByKeywords中获取数据
3) 将得到的数据做一个过滤,必须有图片的才能显示
4) 将得到的数据再做一个格式化,得到我们渲染dom需要的格式
2. 设置一个默认显示:
因为这个over事件是鼠标滑动才执行的
也就是如果我初始化页面,鼠标没有滑动,那么此时什么都不显示
这不是我们所期望的
解决:在mounted中就发送一次请求,让页面显示数据
和over事件执行的逻辑一样,只不过这个keyword是我们自己设定的默认显示数据
城市选择页面:changeCity
- 位置、引入
- 位置:pages/changeCity.vue
- 访问:localhost:3000/changeCity
- 模板:使用默认default.vue模板
- changeCity中组件
components:{
iSelect,
Hot,
Categroy
}
- 这个页面的难点
- 拼音首字母怎么写,如果写26个英文字母标签再插入,是很失败的
- 如何通过后端给定接口,返回城市后,根据字母来分类
- 一个字母对应城市的显示
- 点击字母,快速定位到该字母对应的所有城市
按省份选择iselect.vue 那栏
- 位置、引入:
- 位置:components/changeCity/iselect.vue
- 在pages/changeCity.vue中被引入
- 逻辑:
- 搜索框参见Elmement-UI:
https://element.eleme.cn/#/zh-CN/component/input
- 确定需要哪些数据province,city…
- 将省份和城市做关联(利用watch监听属性),根据省份获取城市(利用axios)
省份:
城市:
联系:
根据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结构:
引入lodash:import _ from 'lodash'
两个事件:
fetch-suggestions="querySearchAsync" -> 用户输入内容的时候触发的事件
@select="handleSelect" -> 当列表被点击选中的时候,触发这个方法
querySearchAsync:_.debounce(async function(query, cb){
1. 如果cities有值的话,直接在cities里面搜索
2. 如果citie没有值的话,从geo/city接口获取数据
3. 将获取到的数据格式化,我们只需要value值
4. 将数据进行过滤,就是城市中包含 我搜索关键字的才留下
}, 200),
handleSelect:function(item){
1. 将当前城市设置为item
2. 跳转页面,回到初始页
}
- 注意:直接搜索 范围是全国
热门城市hot.vue 那栏
- 位置、引入:
- 位置:components/changeCity/hot.vue
- 在pages/changeCity.vue中被引入
- 逻辑:
- 结构采用dl dt dd,因为是一个标题,很多内容
- 在mounted声明周期函数中获取数据渲染
async mounted(){
let {status, data:{hots}} = await this.$axios.get(`/geo/hotCity`)
if(status == 200){
this.list = hots;
}
}
按拼音首字母选择categroy.vue 那栏
- 位置、引入:
- 位置:components/changeCity/categroy.vue
- 在pages/changeCity.vue中被引入
- 逻辑:
- 确定显示字母用的节点,利用dl dt dd,举个栗子:
- 按拼音首字母选择
-
{{item}}
- 点击字母,快速定位到该字母对应的所有城市->利用a标签的#,如下
遍历字母:
- 按拼音首字母选择
-
{{item}}
遍历每个字母对应的城市:
- {{item.title}}
-
{{ c }}
上面的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),就是点击搜索出来的页面:products
- 位置、引入
- 位置:pages/products.vue中被引入
- 访问:localhost:3000/products
- 模板:使用默认default.vue模板
- 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中更改两个热门搜索,一个搜索列表,添加 类似如下语句
{{item.name}}
- 待实现功能:
- 点击排序:点击按价格排序或者按人气排序可以实现下面列表的排序
- 鼠标划过景点简介时候,地图可以定位到相应位置
- 注意decode和encode的问题:query的很多插件在源码中进行了decode,所以用的时候,有的已decode了,自己就没必要再写一遍了,会报错
哈尔滨美团哈尔滨哈尔滨融创乐园:Crumbs
-
位置、引入
- 位置:components/products/crumbs.vue
- 在pages/products.vue中被引入
-
逻辑:
- DOM结构,参见Element-UI:
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/products/categroy.vue
- 在pages/products.vue中被引入
- categroy中组件
components:{
iselect ->下拉框(划过酒店住宿,周边游出现的下拉框)
}
- 逻辑:
智能排序,景点详情部分:list
- 位置、引入
- 位置:components/products/list.vue
- 在pages/products.vue中被引入
- list中组件
import Item from './product.vue'
components:{
Item ->每个景点的简要介绍:像几颗星,门票价格,位置等
}
- 逻辑:
地图控件Amap
- 位置、引入
- 位置:components/public/map.vue
- 在pages/products.vue中被引入
https://lbs.amap.com/api/javascript-api/guide/overlays/toolbar
详情页开发 detail.vue
-
-
位置、引入
- 位置:pages/detail.vue
- 访问 localhost:3000/detail.vue
- 模板:使用默认default.vue模板
- detail.vue中组件
components:{
Crumbs, ->哈尔滨美团 > 哈尔滨美食 > 哈尔滨火锅
Summa, ->商品详情
List ->商家团购及优惠下的列表
}
- 跳转到该路由的链接:components/products/product.vue
{{ meta.name }}
-
逻辑:
- 判断是否显示:商家团购及优惠,显示的条件是登录或者有数据,利用v-if实现
- 思考:访问(详情页)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
- 位置、引入
- 位置:components/details/list.vue
- 在pages/detail.vue中被引入
- list.vue中的组件
components:{
item ->每条数据
}
- 逻辑:
- DOM结构:
- {{ list.filter(item=>item.photos.length).length }}款套餐
- 数据的获取:两种方式
- SSR:我在页面下发的时候就把数据塞进去了
- 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
- 位置、引入
- 位置:pages/cart.vue
- 访问 localhost:3000/cart.vue
- 模板:使用默认default.vue模板
- cart.vue中组件
components:{
list ->订单列表
}
- 跳转到该路由的链接:components/details/item.vue
window.location.href=`/cart/?id=${id}`
- 逻辑
- DOM结构:设计一个平行结构,考虑购物车为空和不为空的两种情况
...
...
...
...
购物车为空
- 订单列表list.vue(components/cart/list.vue)
- DOM结构:参见Element-UI:
https://element.eleme.cn/#/zh-CN/component/table
- 数据:
父组件pages/cart.vue通过SSR获取数据(通过这个接口:/cart/getCart)
传给子组件list.vue 所有订单数据,由子组件全部渲染出来
- 逻辑:
父组件通过接口获取数据,传入子组件数组,存储在cartData中,
子组件通过Element-UI结构渲染数据,
如果我在子组件中更改了购买商品的数量,也就是cartData中的值被更改了,
那么,我们在父组件监听的total(所有订单总价),也就会重新计算
然后重新渲染父组件中 下面这个结构中的数据
应付金额:¥{{ total }}
- 注意:仔细看一下list.vue的数据计算和DOM结构!有一部分需要好好理解
- 提交订单:点击"提交订单",请求/order/createOrder接口,如果请求成功,跳转页面至全部订单页
全部订单页:order
- 需求分析
- 位置、引入
- 位置:pages/order.vue
- 访问 localhost:3000/order.vue
- 模板:使用默认default.vue模板
- 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加上一些其他参数创建订单
2. 将订单存储到数据库中
/order/getOrders返回数据库中全部订单
最后:注册路由,让路由生效
server/index.js中:
import order from './interface/order'
app.use(order.routes()).use(order.allowedMethods())
- 逻辑
- DOM结构:参见Element-UI:
https://element.eleme.cn/#/zh-CN/component/tabs
- 获取全部订单,通过SSR方式渲染到pages/order中的list组件(components/order/list.vue)
- 从接口/order/getOrders获取全部订单数据
- 将数据格式化为 我们渲染页面想要的格式
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
}
})
},
问题
- 搜索失去焦点,热门推荐还在
- 还有莫名其妙会报错,会出现什么靓丽什么的搜索结果
- 注册时候同一个验证码也可以注册