# vue-nuxt-旅游地图-开发 **Repository Path**: owahahah/xianyu-development ## Basic Information - **Project Name**: vue-nuxt-旅游地图-开发 - **Description**: 这个是work - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 0 - **Created**: 2020-11-05 - **Last Updated**: 2022-08-23 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # ==day01-课前准备== ## Promise和Async await axios就是promise, Promise是一个构造函数,返回一个promise对象,是es6标准下的对象,`实现异步的需求` ```js const axios = function(options) { const baseURL = 'http://127.0.0.1:3000' const {url} = options // 1. 创建并返回 Promise 对象 return new Promise((resolve, reject)=>{ // 4. 如果出错, 这个函数的第二个参数, // 可以用来处理错误 if (!url) { reject('请先传入 url') } // 2. 在这个创建对象的构造函数里面传入一个函数组委参数, // 把要做的事情放在里面 const xhr = new XMLHttpRequest() xhr.open('get', baseURL + url) xhr.onload = function () { var res = JSON.parse(xhr.responseText) // 3. 在这个函数里面可以接受两个参数 // 其中第一个就是默认的回调函数, 也就是对应 .then resolve(res) } xhr.send() }); } ``` async和await,主要用户多次调用promise嵌套时候可以考虑使用async await。 > 函数前面添加了async标识后函数中才可以使用await等待promise的调用 ```js async function sendRequest() { let res = await axios({ url: '/category' }) const firstCateId = res.data[0].id console.log('栏目 id', firstCateId); res = await axios({ url: '/post?category=' + firstCateId }) const secondPostId = res.data[1].id console.log('文章 id', secondPostId); res = await axios({ url: '/post/' + secondPostId }) const postDetail = res.data.content console.log(postDetail); const dom = document.querySelector('#post') dom.innerHTML = postDetail } sendRequest() ``` ![image-20200709111809827](img/image-20200709111809827.png) # day01-项目介绍 ## 学什么 学习如何开发一个旅游,出行综合应用类网站,包含发布旅游攻略游记,查找酒店,订飞机票业务模块。 技术亮点: 1. `Nuxtjs`框架应用,基于`Vue`的服务器渲染技术(`SSR`) 2. `Nuxtjs`结合`Element-ui`框架使用 3. `Nuxtjs`(SSR)中使用富文本编辑 4. 灵活应用本地数据存储实现交互 vuex 5. 盖楼式跟帖模块,如何应用组件递归 6. 高德地图实现定位 7. 前端生成付款二维码 8. 一个应用,能学习到3种流行的业务类型 ## 技术栈 * Nuxtjs * Vue * Vue-router * Element-ui * Axios * Token * Vuex * 高德地图 ## 课程安排 * 8天带着写 机票模块 * 6 天分组实战 ## 学习前提 * HTML * CSS * Javascript * Vue基础 ## 课程目标 1. 掌握Nuxtjs开发服务器端渲染应用 (简称SSR) 2. 掌握Nuxtjs结合Element-ui框架的使用 3. 掌握组件化、模块化的构建方式 (黑马头条已经做过) 4. 掌握递归组件的使用(黑马头条已经做过) 5. 掌握在Nuxtjs中使用富文本编辑 (服务端渲染,跟之前的做法有点不一样) 6. 掌握高德地图的基本使用 7. 了解常见的互联网`业务`功能实现 `(老板怎么赚钱的, 团队如何协作)` # nuxt/ssr 简介 ssr 是 vue 的服务端渲染技术 nuxt 是一个可以用来做 ssr 服务端渲染 开发的框架 他们之间的关系, ssr 是技术基础, nuxt 是封装 ## 什么是 ssr Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。全部的操作都是在客户端运行. 在这种情况下, 生命周期 mounted 之前 ,看不到任何东西的, 或者如果我们的客户端瑞浏览器,禁用了js功能的话, 就会一片空白 然而,vuejs 也可以将同一个vue组件在服务器端直接就渲染为 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。 ![image-20200709115544680](img/image-20200709115544680.png) ## ssr 的基本用法 ### 渲染一个 Vue 实例 ``` npm init npm i vue ``` ```js // 第 1 步:创建一个 Vue 实例 const Vue = require('vue') const app = new Vue({ template: `
Hello World
` }) // 第 2 步:创建一个 renderer const renderer = require('vue-server-renderer').createRenderer() // 第 3 步:将 Vue 实例渲染为 HTML renderer.renderToString(app, (err, html) => { if (err) throw err console.log(html) // =>
Hello World
}) // 在 2.5.0+,如果没有传入回调函数,则会返回 Promise: renderer.renderToString(app).then(html => { console.log(html) }).catch(err => { console.error(err) }) ``` ### 与服务器集成 在 Node.js 服务器中使用时相当简单直接,例如 [Express](https://expressjs.com/): ```bash npm install express --save ``` ------ ```js const Vue = require('vue') const server = require('express')() const renderer = require('vue-server-renderer').createRenderer() server.get('*', (req, res) => { const app = new Vue({ data: { url: req.url }, template: `
Url is: {{ url }}
` }) renderer.renderToString(app, (err, html) => { if (err) { res.status(500).end('Internal Server Error') return } res.end(` Hello ${html} `) }) }) server.listen(8080) ``` ## 小结 server side render 以前的 vue 项目,服务器只管给客户端传输空白 html 和 js 实际上所有显示的东西都是在浏览器进行解释, 如果浏览器禁用 js ,一片空白 **渲染在客户端** ssr 服务端直接把 vue 组件变成了 html字符串 再发送给浏览器 即使浏览器禁用 js, 还能看见内容 **渲染在服务器** ![image-20200229113852879](img/image-20200229113852879.png) ssr 的基本套路 1. 在服务端创建 vue 实例 2. 将这个 vue 实例在服务器渲染成 字符串 3. 将字符串传输到客户端 到了客户端已经是html字符串了 ## 为什么/要不要使用服务器端渲染 (SSR)? 与传统 SPA (单页应用程序 (Single-Page Application)) 相比,服务器端渲染 (SSR) 的优势主要在于: - 更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。 - 更快的内容到达时间 (time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备。无需等待所有的 JavaScript 都完成下载并执行,你的用户将会更快速地看到完整渲染的页面。 使用服务器端渲染 (SSR) 时还需要有一些权衡之处: - 开发条件所限。浏览器特定的代码,只能在某些生命周期钩子函数 (lifecycle hook) 中使用;一些外部扩展库 (external library) 可能需要特殊处理,才能在服务器渲染应用程序中运行。 - 涉及构建设置和部署的更多要求。与可以部署在任何静态文件服务器上的完全静态单页面应用程序 (SPA) 不同,服务器渲染应用程序,需要处于 Node.js server 运行环境。 - 更多的服务器端负载。在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用 CPU 资源 (CPU-intensive - CPU 密集),因此如果你预料在高流量环境 (high traffic) 下使用,请准备相应的服务器负载,并明智地采用缓存策略。 在对你的应用程序使用服务器端渲染 (SSR) 之前,你应该问的第一个问题是,是否真的需要它。这主要取决于内容到达时间 (time-to-content) 对应用程序的重要程度。例如,如果你正在构建一个内部仪表盘,初始加载时的额外几百毫秒并不重要,这种情况下去使用服务器端渲染 (SSR) 将是一个小题大作之举。然而,内容到达时间 (time-to-content) 要求是绝对关键的指标,在这种情况下,服务器端渲染 (SSR) 可以帮助你实现最佳的初始加载性能。 # ==day01-项目准备== ## 初始化项目 有点像之前的脚手架,只不过封装的东西更加多了 根据下面的步骤来初始化一个nuxtjs项目 ![image-20200229114906819](img/image-20200229114906819.png) ### 安装 切换至存放项目的路径 然后使用官网提供的脚手架工具 [create-nuxt-app](https://github.com/nuxt/create-nuxt-app),创建一个`nuxtjs`项目。 ``` npx create-nuxt-app xianyun ``` > 注意:在`NPM`版本5.2.0默认安装了`npx`,在命令行窗口输入`npm --version`可查看当前版本号 ![image-20200709123011627](img/image-20200709123011627.png) ![image-20200709123029641](img/image-20200709123029641.png) ![image-20200709123116341](img/image-20200709123116341.png) ![image-20200709123200592](img/image-20200709123200592.png) ![image-20200709123239816](img/image-20200709123239816.png) ![image-20200709123326396](img/image-20200709123326396.png) ![image-20200709123351088](img/image-20200709123351088.png) ![image-20200709123416887](img/image-20200709123416887.png) ![image-20200709123502452](img/image-20200709123502452.png) **以下是最终的选项, 然后自动开始安装** ![image-20200709123602549](img/image-20200709123602549.png) 需要等待片刻安装依赖的下载,下载完成后可以看到下面的提示框,要求输入项目名称和其他一些问题 ### 启动 当运行完时,项目将安装所有依赖项,因此下一步是启动项目: ![image-20200709123651826](img/image-20200709123651826.png) ``` cd xianyun npm run dev ``` ![image-20200709123833435](img/image-20200709123833435.png) 启动会会弹出提示打开 http://localhost:3000/ 如果出现 404 可以用 127 替换尝试 http://127.0.0.1:3000/ ![image-20200709123932923](img/image-20200709123932923.png) > 注意:**没有报错不用管这一步**此时运行项目可能会遇到以下错误提示`HTMLElement is not define nuxt.js`,原因是在`Nuxtjs`的服务器环境加载`Element-ui`遇到兼容问题抛出的错误,(如不报错则表示bug已修复),解决办法如下: > 下载指定版本的`element-ui` > > ``` > npm install --save element-ui@2.4.11 > ``` **项目初始化就完成了。** 脚手架初始示例 如果要使用, 1. 先 fork 到自己的账号下面 2. 克隆自己账号下面的项目 3. 进入项目文件夹 运行 npm install 4. npm run dev https://gitee.com/owahahah/xianyu.git 工作目录 ![1604570948456](img/1604570948456.png) ## 项目演示 **示例:** http://157.122.54.189:9093/ +请求地址 **数据服务:** 数据服务运行步骤 1. 解压缩 2. 目录内运行 ```js npm install --node-sqlite3_binary_host_mirror=https://npm.taobao.org/mirrors/ ``` 3. npm run start 4. **如果数据库运行失败** 我们的后台服务器已经将数据库内嵌, 通常会自动安装, 遇到过某些机器, 会缺失数据库安装, 有提示让你补上 出现下图报错, 可以运行 ![1604571036566](img/1604571036566.png) 在线数据库: (后面学到的机票之类, 建议直接用线上的) http://157.122.54.189:9095/ ## 项目文件结构 ### 文件结构 官网文档: ``` - xianyun 项目根目录 - assets 资源目录 - components 组件目录 - layouts 布局组件目录 - middleware 中间件目录 - pages 页面目录 - plugins 插件目录 - static 静态文件目录 - Store Vuex 状态树 文件 - nuxt.config.js Nuxt.js 应用的个性化配置 - package.json 依赖关系和对外暴露的脚本接口 ``` ### 别名说明 | 别名 | 目录 | | ------------ | --------------------------------------------------------- | | `~` 或 `@` | [src目录](https://zh.nuxtjs.org/api/configuration-srcdir) | | `~~` 或 `@@` | [根目录](https://zh.nuxtjs.org/api/configuration-rootdir) | 默认情况下,`src目录`和`根目录`相同 > 注意:**在您的 `vue` 模板中, 如果你需要引入 `assets` 或者 `static` 目录, 使用 `~/assets/your_image.png` 和 `~/static/your_image.png`方式。 ## 基本配置 ### 修改/删除默认文件 Nuxtjs初始化项目时给我们提供了两个演示文件,对我们接下来的项目开发没任何作用,分别是`pages/index.vue`和`components/logo.vue`。 修改如下: **1.首先是`pages/index.vue`** ```vue ``` **2.删除`components/logo.vue`文件** > 现在访问首页`http://localhost:3000/`,就只能看到`首页`两个字了。 ### 创建页面目录 在 nuxt 框架当中, 不需要配置路由 只要你在 pages 文件夹下面创建文件夹或者文件就能够自动生成路由 接下来创建项目结构目录,方便以后的项目模块扩展。 在`pages`目录下新建文件夹,文件夹分别对应接下来要开发的业务模块 ``` - ... // 其他文件 - pages - index.vue // 已存在的首页文件 - post // 存放旅游攻略模块的文件夹 - index.vue // 旅游攻略模块首页文件 - air // 存放机票模块的文件夹 - index.vue // 机票模块首页文件 - hotel // 存放酒店模块的文件夹 - index.vue // 机票模块首页文件 - user // 存放用户模块的文件夹 - login.vue // 用户登录注册页面 - ... // 其他文件 ``` 如果你已经新增了上面的文件,我们可以直接通过路由访问`pages`下的页面,查看页面是否新增成功。比如我们修改`post/index.vue`内容如下: ```vue ``` 在浏览器中访问地址`http://localhost:3000/post`,页面显示如下: ![1558687169347](img/1558687169347.png) > 1. 顺便给`air/index.vue`, `hotel/index.vue`也新增上面的内容吧,不过需要修改下文字方便区分页面。 > 2. Nuxtjs的页面访问规则和浏览器的`SPA`机制不同,在Nuxtjs中不需要路由配置,`pages`下的页面可以直接通过路径访问,默认查找`index.vue` ### 创建组件目录 虽然现在还没开始开发页面,但是我们可以预测未来的页面中肯定存在很多可以独立封装的组件,所以我们现在可以给`未来的组件`新建存放目录. 在`components`文件夹中新建文件夹: ``` - ... // 其他文件 - components // 存放公共组件的文件夹 - post // 存放旅游攻略模块组件的文件夹 - air // 存放机票模块组件的文件夹 - hotel // 存放酒店模块组件的文件夹 - user // 存放用户模块组件的文件夹 - ... // 其他文件 ``` ### #### 页面过渡效果样式 (可选) 目前还没涉及到页面的跳转,但不妨碍我们给项目配置预先做好铺垫,这份配置只是为了页面切换时优化用户体验设计的,并不是必须的。 在`assets/`目录下创建这个文件`assets/main.css`,添加以下样式: ```css /* 页面切换时候过渡样式 */ .page-enter-active, .page-leave-active { transition: opacity .5s; } /* 打开时候过渡样式 */ .page-enter, .page-leave-active { opacity: 0; } /* 页面顶部页面加载进度条 */ .nuxt-progress{ background:#409eff; height: 1px; } ``` > 只是新建了样式文件还不能产生效果,需要在`nuxt.config.js`配置文件中加载该文件才能生效。 > > **配置方式参考下一小节笔记** > > 参考文档: [过渡动效](https://zh.nuxtjs.org/guide/routing#过渡动效) #### 修改配置文件 以前 webpack 配置都在 webpack.config.js vue-cli 在 vue.config.js **nuxt 它的配置全部都在 nuxt.config.js** 配置文件`nuxt.config.js`对项目进行了全局配置,对每个页面都生效。 复制下面配置替换项目的`nuxt.config.js`文件,如果想手动修改的话可以查看`中文注释行(#10 #18 #32 #56)` ```js import pkg from './package' export default { mode: 'universal', /* ** Headers of the page */ head: { title: "闲云旅游网", // 修改title meta: [ { charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }, { hid: 'description', name: 'description', content: pkg.description } ], link: [ { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }, { rel: 'stylesheet', type: 'text/css', href: '//at.alicdn.com/t/font_1168872_ehvuah8v57g.css'} // 新增全局字体样式 ] }, /* ** Customize the progress-bar color */ loading: { color: '#fff' }, /* ** Global CSS */ css: [ 'element-ui/lib/theme-chalk/index.css', 'assets/main.css' // 新增自定义的页面过渡样式(文件来自3.4.1) ], /* ** Plugins to load before mounting the App */ plugins: [ '@/plugins/element-ui' ], /* ** Nuxt.js modules */ modules: [ // https://axios.nuxtjs.org/setup '@nuxtjs/axios' ], /* ** Axios module configuration */ axios: { // See https://github.com/nuxt-community/axios-module#options // baseURL: "http://157.122.54.189:9095" // 新增备用地址 baseURL: "http://127.0.0.1:1337" // 新增axios默认请求路径 }, /* ** Build configuration */ build: { transpile: [/^element-ui/], /* ** You can extend webpack config here */ extend(config, ctx) { } }, } ``` > 注意:修改`nuxt.config.js`记得要重启项目 (比较保险) **如果创建目录时漏掉了 axios 没有选择** 可以自行安装 npm i @nuxtjs/axios ``` npm install @nuxtjs/axios ``` ### 添加less 预编译样式我们选择`less`,相关配置`nuxtjs`已经帮我们配置好了,不需要改动`webpack`的配置文件,只需要安装依赖包即可 ``` npm install --save less less-loader ``` ## 总结 1. 初始化项目命令: ``` // 安装,注意选项选择 npx create-nuxt-app xianyun // 启动 $ cd xianyun $ npm run dev ``` 2. 新建项目文件结构 3. (可选) 创建一个页面跳转动效的css 4. 修改默认配置文件`nuxt.config.js` ,注意修改配置文件要重启项目(最保险) ## Nuxt和普通的Vue 1. `Nuxt`是同构程序,这里的同构指的是一套代码,可以在浏览器运行,也可以在服务器(`Nodejs`)运行,也就是说可以同时使用浏览器的`API`和`Nodejs`的`API`。 2. 普通的`Vue`页面只能使用浏览器的`API`,即使在`Nodejs`环境下开发也只是使用`Webpack`来编译打包。 3. `Nuxt`是前后端框架的集合,前端采用`Vue`,后端可选框架有`Express、koa2, egg, api`等,所以可以理解为`Vue`是一个页面模板的存在,类似于`art-template` , ejs 4. `Nuxt`支持单页和多页应用。 > 注意:虽然`Nuxt`确实很强大,但是目前市面上应用的却不是很多,因为`nodejs`作为服务器端语言目前还是相对较少的,更多的还是`java,php`等,所以在这个项目当中, 关于 ssr 服务端渲染的并不会扩展, 主要我们会把精力集中在的功能业务开发上,以及一些`Vue`未接触过的用法。 # ==day01-开发任务== ## 项目网址 记得用 git 管理项目 我的:https://gitee.com/owahahah/xianyu-development 老师的:https://gitee.com/imyyliang/xianyun56-development ## 开发安排 - 初始化布局 - 页头页脚公共组件 - 首页轮播图 ## 初始化默认全局布局 这个相当于 app.vue `nuxtjs`提供了一个公共布局组件`layouts/default.vue`,该布局组件默认作用于所有页面,所以我们可以在这里加上一些公共样式,在下一小结中还会导入公共组件 替换`layouts/default.vue`代码如下: ```vue ``` ## 新建公共组件 ### 思路 1. 在`components`中新建应用统一的头部组件和页脚组件。 2. 在默认布局中`layouts/default.vue`中导入公共组件。 > 组件约定:公共组件不需要放到子文件夹中 > > 下拉菜单组件文档: ### 实现步骤 #### 头部组件 在`components`文件夹中新建头部组件`components/header.vue`。 ```vue ``` > 注意: 样式中的` /deep/ .nuxt-link-exact-active`是菜单高亮时的颜色。 在`layouts/default.vue`中导入头部组件 ```vue ``` > 实现`登录注册功能`后再回来判断显示用户信息还是登陆注册按钮。 #### 页脚组件 在`components`文件夹中新建头部组件`components/footer.vue`。 ```vue ``` 在`layouts/default.vue`中导入页脚组件 ```vue ``` 现在可以查看最新的页面效果了。 ### 总结 1. `layouts/default.vue`是默认的布局组件,会作用于任何页面。 2. 在`layouts/default.vue`中导入全局的头部组件和页脚组件 ## 首页轮播图 ### 思路 1. 使用`Element-ui`的幻灯片组件`el-carousel`,新增首页轮播图布局。 2. 请求后端接口替换静态图片数据 > 结合使用el-carousel和el-carousel-item标签就得到了一个走马灯。幻灯片的内容是任意的,需要放在el-carousel-item标签中。 > > 默认情况下,在鼠标 hover 底部的指示器时就会触发切换。 > > 通过设置trigger属性为click,可以达到点击触发的效果。 > > 若将arrow设置为always,则会一直显示, interval自动切换的时间间隔,单位为毫秒。data ### 实现步骤 #### 新增轮播图布局 把`pages/index.vue`内容替换成以下代码: ```vue ``` > 预览首页应该能正常显示轮播图了 banners 是我们自己构建的一个死数据数组, 用来遍历之后生成多张图片进行轮播 #### 请求后端接口数据 接下来使用接口数据来替换本地的静态数据 > 接口文档:在今天的课堂资料文件夹中 > > ![image-20200302111615535](img/image-20200302111615535-1588713445691.png) > > ![image-20200302110449962](img/image-20200302110449962-1588713445691.png) ```vue ``` 替换`template`的图片地址添加`$axios.defaults.baseURL`,因为接口返回的图片链接是相对链接 ```vue ``` ### 总结 1. 使用`Element-ui`的幻灯片组件`el-carousel`实现轮播图,显示死数据,固定的图片 2. 使用`this.$axios`请求后台轮播图接口 获取数据 3. 在模板中使用`$axios.defaults.baseURL`补全图片地址进行显示 # 补充 ## 后台安安装不了解决办法 用线上的链接 ```js baseURL: "http://157.122.54.189:9095" ``` 用你同桌的`ip`(关闭你同桌的防火墙) ```js baseURL: "http://192.168.66.xx:1337" ``` ## 后台启动是提示文件过大 ![0cc8010838ef2a0b0f2b0b364b207577](img/0cc8010838ef2a0b0f2b0b364b207577.png) 修改后台的配置文件 ![1561531061275](img/1561531061275.png) 把加载时间`3000`毫秒改为10秒 ![1561531144816](img/1561531144816.png) -------------------------------------------------------------------------------------- # ==day02== 提问 走马灯的 interval 必须是数字的报错 和 props 的 声明方式 # 复习回顾 ```js async await //--------------------------- 原生 (命令式) var xhr = new XMLHttpRequest() xhr.open xhr.onreadystatechange xhr.send //--------------------------- jq (回调函数的形式) $.ajax({ type, url, success }) ``` ```js promise 封装 .then 替代了 success this.$axios({ url }).then(res=>{}) ``` 语法糖 async await ```js async function fnName() { var res = await this.$axios({ url, method }) console.log(res.data) } ``` - **ssr** > 1. vue 一个渲染过程 (渲染之前 vue 就是一堆 js 代码, 渲染过后会变成 html 页面) > 2. 以前页面上 new Vue() 客户端渲染 spa 单页应用 > 3. nuxt 在服务器预先执行 new Vue SSR 服务端渲染 - **项目脚手架搭建步骤** > npx create-nuxt-app 项目名 > > 回答一些问题之后 > > 创建完毕后进入文件夹, 然后运行项目 > > cd 项目名 > > npm run dev (项目准备完毕可以放上 git 仓库) - **预备后续的开发准备** > 删除默认样式和内容 index.vue / components/logo.vue > > pages 下方四个文件夹 components 下方也是四个 user / hotel /air / post > > pages 里面的 .vue 文件可以自动生成路由 / @ 可以作为根目录别名 > > nuxt.config.js 是全局配置文件 - **项目页面初步开发**--首页 > 1. 头部组件 > 2. 页脚组件 > 3. 轮播图 ## 今日开发任务 - [x] **首页搜索跳转功能** 1. 搜索框布局 2. 点击 tab 进行切换 3. 点击搜索跳转页面 - [x] **登录表单布局** 登录页 tab 切换 登录/注册 根据当前激活项, 显示 loginform / registerform 利用 el-form 布局登录表单 - [x] **实现登录功能** 绑定数据 校验输入 发送请求 - [x] **掌握使用store管理用户登录数据 (vuex)** - 初步简介 store state mutations - 实现 vuex 数据的初始化, 渲染和修改 - 登录完毕将用户数据存放到 vuex , 头部组件判断数据进行显示 - 利用插件 vuex-persistedstate 保持 vuex 数据持久化 ## 首页搜索框和搜索跳转 ### 思路 1. 添加搜索框布局, 定位方式居中 2. 搜索框`tab`切换执行不同的操作 凡是有 tab 切换,第一时间想到有一个记录当前状态的变量 3. 搜索跳转 ### 实现步骤 #### 1.搜索框布局 这里面的代码复制之前,要分辨那些需要 **2.tab 高亮切换3个重点** 1. 有一个 data 变量储存当前激活状态 currentOption 2. tab 增加点击事件, 改变 currentOption 3. 动态给当前激活的 tab 添加 class 把搜索框定位在轮播图上,在`pages/index.vue`的`template`新增以下代码: ```vue ``` 在`pages/index.vue`的`script`替换如下: ```vue ``` 在`pages/index.vue`的`style`替换如下: ```less ``` 完成 上面步骤可以得到搜索框的静态布局,下面我们来加入交互操作。 #### 2.tab栏操作 实现切换效果,并且判断如果切换的机票`tab`,那么直接跳转到机票首页 编辑`methods`下的`handleOption`方法 ```js // 省略其他代码 // 切换tab栏时候触发 handleOption(index){ // 设置当前tab this.currentOption = index; // 如果切换的机票tab,那么直接跳转到机票首页 const item = this.options[index]; if(item.name === "机票"){ return this.$router.push(item.pageUrl); } }, // 省略其他代码 ``` #### 搜索跳转 在输入时按下回车键,或者点击搜索放大镜图标, 触发搜索时候会跳转到当前`tab`的`pageUrl`页面路径,并且在`url`上携带上输入框的值 > 页面会根据参数进行搜索请求 ```js // 省略其他代码 // 搜索时候触发 handleSearch(){ const item = this.options[this.currentOption]; // 跳转时候给对应的页面url加上搜索内容参数 this.$router.push(item.pageUrl + this.searchValue); } // 省略其他代码 ``` ### 总结 1. 先把搜索框定位在轮播图上。 (css 的问题) 2. 给`tab`添加切换效果,并且判断如果是机票`tab`,直接跳转到机票首页。 最重要的一个变量是 currentOption 3. 实现搜索跳转,注意跳转的链接来自当前选中的`tab`的`url`属性,并且附带上参数 ![image-20200506160250475](img/image-20200506160250475.png) ## 登录注册页布局 首先添加 切换的 tab 分类部分 新建`pages/user/login.vue`的代码如下 ```vue ``` 在预留的位置中将会导入登录组件和注册组件。 ## 登录功能 ![1561362542495](img/1561362542495-1588890287280.png) ### 思路 1. 在`components/user`中新建`loginForm.vue`表单组件 2. 使用`Element-ui`的表单组件布局 3. 表单数据绑定 v-model 4. 表单验证 输入框失去焦点,需要验证 点击登录按钮, 发送登录请求前, 需要一次整个表格的验证 使用饿了么封装好的验证方法 5. 登录接口对接 ajax ### 实现步骤 #### 新建登录表单组件 在`components/user`中新建`loginForm.vue`组件,新增内容如下 ```vue ``` > 注意:新增了组件后在`pages/user/login.vue`中导入即可,导入位置,去除下面部分的组件的注释 ```vue ``` #### 表单数据绑定 修改`data`的`form`数据,然后使用`v-model`绑定到对应的表单字段。 编辑`components/user/loginForm.vue` ```js // 其他代码... data(){ return { // 表单数据 form: { username: "", // 登录用户名/手机 password: "" // 登录密码 }, // 其他代码... } }, // 其他代码... ``` 使用`v-model`绑定到对应的表单字段 ```vue ``` #### 表单验证 双向数据绑定到`form`字段后,我们现在可以来提交表单进行登录了,但是提交之前还需要验证表单字段是否合法,比如不能为空。 表单验证**3**个**重点的设定**, 这是用来做校验, 跟数据绑定有区别. - 表单model 绑定整个表单数据 - 表单rules 可以设定每个字段的校验规则 - 表单每一项对应的prop 设置在 el-form-item 上面 - 发送请求前的一次保底总校验 本来我们设定的 rules 规则都有触发条件, 但是我们在提交表单前, 希望无需触发器, 也想将整个表单全部验证一次 this.$refs.form.validate() 这个函数可以有两种可能的用法, 这是饿了么封装的时候已经做好处理的 1. 传入回调函数作为参数, 这个回调会得到两个形参, 1. 是否通过校验的布尔值, 2. 没通过的项组成的对象 2. 不传入回调, 自动变成一个 promise 返回, 成功逻辑写在 then 里面, 失败逻辑写在我们的catch 里面 `components/user/loginForm.vue` 的新增代码 ```js // 其他代码... data(){ return { // 其他代码... // 表单规则 rules: { username: [ { required: true, message: '请输入用户名', trigger: 'blur' }, ], password: [ { required: true, message: '请输入密码', trigger: 'blur' }, ], }, } }, // 其他代码... ``` 使用`el-form-item`添加`prop`属性 ```vue ``` > 现在可以尝试在把`input`输入框的值清空,会出现在`rules`中的定义的提示内容 #### 登录接口 接下来要调用登录的接口进行登录了,如果一切正常的话,我们可以看到后台返回的用户信息,如果登录失败,我们需要对统一对错误的返回进行处理,这个我们在最后再统一实现。 现在只关注成功部分 修改`components/user/loginForm.vue`的提交登录事件: ```js // 其他代码... // 提交登录 methods: { handleLoginSubmit(){ // 验证表单 this.$refs['form'].validate((valid) => { // 为true表示没有错误 if (valid) { this.$axios({ url: "/accounts/login", method: "POST", data: this.form }).then(res => { console.log(res.data); }) } }) } } // 其他代码... ``` > 现在登录接口可以开始访问了,服务器给我们提供了测试账号密码 > > 账号:`13800138000` > > 密码:`123456` 如果正常登录应该可以在控制看到打印出来的用户信息,则表示登录成功了。 ![image-20200303102101626](img/image-20200303102101626.png) ### 总结 1. 在`components/user`中新建`loginForm.vue`表单组件 2. 使用`Element-ui`的表单组件绑定数据和验证表单 3. 调用登录接口 ## 表单验证拓展 1. 怎么样使用正则表达式校验 ![image-20200508140825633](img/image-20200508140825633.png) 2. 校验顺序 - required 代表必填, 最优先的 - 如果没有 required 然后用户没有填数据, 那么默认校验通过, 其他的规则不生效 - 其他同级别的校验规则按照对象的先后顺序来 3. 如何在输入框聚焦时, 暂时清除错误提示 ![image-20200508142226372](img/image-20200508142226372.png) ## 使用store管理数据 (vuex) 什么是 vuex ? - Vuex 是一个专为 Vue.js 应用程序开发的**状态管理**模式。 - 它采用**集中式存储**管理应用的所有组件的状态, - 并以相应的规则保证状态以一种**可预测的方式**发生变化。 (必须按照规定的方式改变数据) 面对一个储存状态数据的工具时, 有几个方面的问题我们马上可以想到的 - 数据放在哪里 store state 里面 - 如何存入/更新数据 可以在 store state 里面初始化 / 调用 mutation 函数进行数据改造 - 如何获取数据 this.$store.state.模块名.字段名 ### 概念 在实际使用之前了解 vuex 三个概念 - **store** 仓库 - 创建仓库, 因为 nuxt 框架已经封装好各种引入, 所以可以直接使用, 只需要创建文件即可 ![image-20201107120252930](img/image-20201107120252930.png) - **state** 状态, 所有数据 ![image-20201107120617849](img/image-20201107120617849.png) 如何在组件当中显示一个 state 数据 this.$store.state.模块名.字段名 ![image-20201107121420564](img/image-20201107121420564.png) - mutation 规定对数据进行修改的方法 1. 创建 ```js export const mutations = { // mutations 是一个对象 // 里面的每一个属性都是一个函数 // 这里的函数专门用来修改这个仓库的数据 state setAbc(state, data) { // 所有 Mutation 函数都可以接受两个参数 // 第一是 state 对象本身 // 第二是外面调用时传入的数据 // 我们现在想修改 abc state.abc = data; } } ``` 2. 使用 ```js // this.$store.state.user.abc = 321 直接复制是不行的 // 需要使用 commit 的方式调用 mutation // 调用时需要两个参数,第一个是mutaion 的路径,包括文件名和函数名,中间用反斜杠隔开 // 第二个是你想要传进去的 data 数据 setTimeout(() => { this.$store.commit('user/setAbc', 321); }, 1000); ``` ### 优势 - js 原生的数据对象写法, 比起 localStorage 不需要做转换, 使用方便 - 属于 vue 生态一环, 能够触发响应式的渲染页面更新 (localStorage 就不会) - 限定了一种可预测的方式改变数据j, 避免大项目中, 数据不小心的污染 其实如果跟 localStorage 对比的话 localStorage.getItem == this.$store.state.xxx localStorage.setItem == this.$store.commit() ## vuex管理用户信息步骤(store) 1. 在`store`文件夹新建`user.js`, 配置 state 和 mutation 2. 在登录页面中实现登录,并保存数据到`store`的`state`中 使用 commit 方法 3. **在头部组件中显示用户信息** ![image-20201107145527829](img/image-20201107145527829.png) ### 新建状态文件 在`store`文件夹新建`user.js`,并添加以下代码 ```js // 用户管理 export const state = () => ({ // 采用接口返回的数据结构 userInfo: { token: "", user: {}, }, }) export const mutations = {}; export const actions = {}; ``` ### 登录成功将数据存入 vuex ![image-20200303144046010](img/image-20200303144046010.png) ### 顶部组件,展示用户信息 **判断 token的存在则将用户数据显示出来** 不断获取数据进行渲染, 其实跟我们之前的 localStorage.getItem('user') 很像 ![image-20200303145014710](img/image-20200303145014710.png) ### 展示用户信息 在头部组件展示`store`中保存的用户数据。 在`components/header.vue`中实现该功能: ```vue {{$store.state.user.userInfo.user.nickname}} ``` > 模板中使用`$store.state.user.userInfo`可以访问`store`的数据,虽然长了点,但是不难理解。 > > 注册功能也是同样的优化思路 ## 保存store到本地 localStorage ![image-20201107151943516](img/image-20201107151943516.png) 现在用户已经保存到`store`了,但是还有一个问题,数据是保存在变量缓存中的,如果页面一刷新,那么数据就会不见了,这样是不合理的。 所以我们需要使用`localStorage`实现**状态持久化**, - 概念就是每当 vuex 数据发生变化, 就存放到 localStorage 里面去 - 每当页面刷新的时候, localStorage 数据恢复到 vuex 里面 **另外由于`nuxtjs`是运行在服务器的,不能直接在`store`中使用浏览器的localStorage方法**, 所以我们需要依赖一些判断 但是不想自己写, 可以直接调用一个插件来实现以上的所有功能。 ### 思路 监听数据变化, 每当登录完毕, vuex 数据发生变化,就要将数据保存到浏览器 **本地** (指用户浏览器localStorage) 页面打开时, 会尝试将之前保存过的数据恢复到 vuex 当中即可 有点复杂, 找个插件帮忙 ### 实现缓存信息到本地 `nuxtjs`中`store`不能直接使用浏览器的`lcoalStorage`方法, 而且自己写数据同步功能比较麻烦, 所以我们需要依赖一个插件`vuex-persistedstate`,该插件会把本地存储的数据存储到`store`。 > 插件地址: 1. 安装插件 ``` npm install --save vuex-persistedstate ``` 2. 需要创建一个 localStorage 自定义插件用来引入第三方包 以前我们引入第三方的插件时, 可以直接在 main.js 入口文件 的 new Vue 根实例创建之前, 添加代码, nuxt 的机制是自定义插件,存放在 plugins 文件夹, 然后用配置进行引入 创建插件的两步 在根目录`plugins`中新建文件`localStorage.js`,加入以下代码 ```js import createPersistedState from 'vuex-persistedstate' export default ({store}) => { window.onNuxtReady(() => { createPersistedState({ key: "store", // 读取本地存储的数据到store })(store) }) } ``` ​ 3. 导入插件 修改`nuxt.config.js`配置文件,在`plugins`配置项中新增一条数据 ```js // 其他代码... plugins: [ // 其他代码... { src: '@/plugins/localStorage', ssr: false } ], // 其他代码... ``` > 修改完后重新启动项目即可。 ![image-20200303155917264](img/image-20200303155917264.png) ### 总结 使用`vuex-persistedstate`保存 vuex 到本地存储 1. 安装 2. 创建插件文件 plugins/ 文件夹下面 3. 在 nuxt.config.js 的 plugins 配置里面引入插件, 注意设置 ssr 为 false 4. 重启项目即可 # ==day03== # 每日反馈 ![image-20201107213901491](img/image-20201107213901491.png) **vue cli 以前的项目中能不能用 vuex ** 能, 直接按照 vuex 官方文档, 安装引入注册, 创建实例, 挂载到 new Vue() 当中即可 **vuex 持久化** 如果数据有变化, 存放到 localStorage 如果页面进入, 将 localStorage 数据重新拿回来 ![image-20201107215547776](img/image-20201107215547776.png) 数据获取为什么用 computed - 封装的好处, 可以偷懒 - 为什么用 computed ( data 呢? methods 呢?) data 不行, 因为他没有声明式渲染的作用, 他不会跟随别的数据更新 methods 可以, 也能实现 method 每次调用显示时都会执行一次 computed 只要获取过一次, 下次即使是重复使用, 也不会重新执行(数据没有变化), 因为内部有一个缓存机制 # 复习回顾 - 首页搜索跳转功能 布局 tab 切换 搜索跳转 - 登录表单布局 页面外围容器 tab 切换 登录注册 登录页的表单 - 实现登录功能 - 数据的绑定 - 数据校验 model / rules / prop - 请求发送 - 掌握使用store管理用户登录数据 (vuex) - 掌握本地存储保存和同步store vuex 数据 # 开发任务 - [x] 退出登录功能 - [x] 注册功能 表单布局 数据绑定 数据校验 更灵活的函数式校验 自定义一个校验函数 validator 提交请求 (验证码, 注册请求) form数据跟 api 要求不一致, 多了一个, 使用 ... 剩余参数语法进行提取 - [x] axios的响应错误拦截 - nuxt 封装的 axios 利用 nuxt 插件机制, 在 plugins 下面创建文件, 在 nuxt.config.js 里面配置 - 如果我们自己封装了一个原版的 axios 直接使用以前黑马头条用过的 http.interceptors.response.use - [x] 机票页的基本布局 - [x] 搜索框组件布局 只讲了 自动补全输入框, 和日期选择器的使用 # 一. 登录注册(第二部分) ## 1.退出登录 **思路: 我们现在的主要目标:设置我们的 vuex 用户信息为空** 最终只需要一行代码就可以实现 之前我们已经做过一次设置用户信息的动作 登录成功那里 使用 commit 将获取回来的数据设定到 userInfo 里面 如果我们强制设置一个空对象, 其实就可以实现退出登录了 ```js //UserHeader.vue //🚩 第二种方法: this.$store.commit('user/setUserInfo', {}) //------------------------------------ //🚩 第一种方法: // const {commit}=this.$store; // commit("userstore/cleanUserInfo") //🚩------------------------------------ //userstore.js //清除用户数据 export const mutations = { setUserInfo(state, data) { state.userInfo = data }, //清除用户数据 // cleanUserInfo(state, info) { // // 是否存在浏览器环境 // if (process.browser) { // localStorage.removeItem('userInfo') // } // state.userInfo = {} // } }; ``` ## 2.注册功能 ![image-20191203050759212](img/image-20191203050759212.png) ### 思路: 1. **在`components/user`中新建`RegisterForm.vue`表单组件** 2. **使用`Element-ui`的表单组件布局排版** 3. **表单数据绑定 v-model** > 1. 要看一共有多少数据 > 2. 参考最终 api 接口要求:username, nickname , captcha , password 4. **表单验证** > 表单 model / rules >表单项 prop > >第二次确认密码的验证方式(新方式) 5. **发送手机验证码** 6. **注册接口对接 ajax** ------------------------------------------------------------------------------------------------------------------------------------- ### 实现步骤 #### 新建注册表单组件 在`components/user`中新建`RegisterForm.vue`组件,新增内容如下 ```vue ``` > > >注意:新增了组件后在`pages/user/login.vue`中导入即可,导入位置,去除下面部分的组件的注释 ```vue ``` #### 表单数据绑定 修改`data`的`form`数据,然后使用`v-model`绑定到对应的表单字段。 编辑`components/user/RegisterForm.vue` ```js // 其他代码... data(){ return { // 表单数据 form: { username: "", // 登录用户名/手机 password: "", // 登录密码 checkPassword: "", // 确认密码 nickname: "", // 昵称 captcha: "" // 手机验证码 }, // 其他代码... } }, // 其他代码... ``` 使用`v-model`绑定到对应的表单字段 ```vue ``` #### 表单验证 双向数据绑定到`form`字段后,我们现在可以来提交表单了,但是提交之前还需要验证下表单字段是否合法,比如不能为空。 继续编辑`components/user/RegisterForm.vue` ```js // 其他代码... data(){ //由于校验函数只是在data使用,可以直接写在data中 // value 输入的内容 // 校验完成就需要执行 // 1. 如果校验合法, 直接执行, 没有参数 // 2. 如果不合法, 就要带上一个错误对象参数 new Error('两次密码必须相同') // 确认密码 const validatePass = (rule, value, callback) => { if (value === '') { callback(new Error('请再次输入密码')); } else if (value !== this.form.password) { callback(new Error('两次输入密码不一致!')); } else { callback(); } } return { // 其他代码... // 表单规则 rules: { username: [{ required: true, message: '请输入用户名', trigger: 'blur' }], password: [{ required: true, message: '请输入密码', trigger: 'blur' }], checkPassword: [{ validator: validatePass, trigger: 'blur' }], nickname: [{ required: true, message: '请输入昵称', trigger: 'blur' }], captcha: [{ required: true, message: '请输入验证码', trigger: 'blur' }], }, } }, // 其他代码... ``` > 和登录组件一样,给`el-form-item`添加对应的`prop`属性。 #### 发送手机验证码 ![image-20201108104842532](img/image-20201108104842532.png) 根据手机号码发送手机验证码,默认返回`6`个`0`。 其实我们只是 发送了一个 ajax 请求而已 `components/user/RegisterForm.vue` ```js // 2.发送验证码 async handleSendCaptcha(){ // 正则规则语法:规则.test(需要验证的字符) let regexp= /^1[3456789]\d{9}$/ if(!regexp.test(this.form.username)){ return this.$message.error('请输入正确的手机号') } console.log(this.form.username); let res=await captchas(this.form.username) console.log(res); if (res.data.code) { this.$message.success('成功获取验证码:' + res.data.code) } }, //------------------------------ //封装手机验证码api export const captchas = (tel) => { return axios({ method: 'post', url: '/captchas', // 注意data传参的格式,不然会报400 data: { tel } }) } ``` #### 注册接口 提交数据到注册接口,返回数据和登录接口是一样的。 下面的示例代码当中 使用了**剩余参数/属性**语法 `components/user/RegisterForm.vue` ```js // 3.点击注册 handleRegSubmit(){ console.log(this.form); // 再次验证表单 this.$refs.ruleForm.validate(async(isvalidate)=>{ let res if(isvalidate){ // 注册请求的参数不需要确认密码,所以使用剩余运算符去掉 let {checkPassword,...data}=this.form res=await register(data) console.log(res); if (res.data.token) { this.$message.success('注册成功' ) this.$emit('toLogin') //触发自定义事件,告诉父组件可以做某件页面跳转的事件了 } } }) --------------------------------------- // 注册api export const register = (data) => { return axios({ method: 'post', url: 'accounts/register', data }) } --------------------------------------- ``` 剩下的无非就是弹窗提示, **注册成功后续操作** - 可以自动直接登录 ![image-20201108115008010](img/image-20201108115008010.png) - ==或者跳转到登录页让用户登录 (推荐)== ![image-20201108115616929](img/image-20201108115616929.png) ### 总结 注册和登录的步骤大致上是一样的,只是接口和参数的区别, 注册表单功能实现中的重点, **自定义表单校验函数** 发送请求时**剩余参数**用来筛选需要提交的数据 # 二. 错误拦截 目标: 使用`axios`的拦截器拦截页面的**所有请求错误**,并弹出对应的提示。 **==原版的axios写法: axios.interceptors.response.us(回调函数)==** ## nuxt自带的 ### 1.nuxt 的插件机制 以前的拦截器都放在 main.js 入口文件当中 引入 axios 之后, 创建 vue 实例之前 nuxt 当中没有 main.js, 如何在 创建 vue 根实例之前执行某些代码 靠插件机制 其实所有在 nuxt.config.js 的 plugins 数组里面配置了路径的文件都会在 vuejs 之前执行, 相当于以前 main.js 入口文件 建议这些插件都放在 plugins 文件夹下方 ### 2.新建axios拦截错误插件 > ==**这个拦截器是供nuxt封装的@nuxtjs/axios使用的**==。 新建文件`plugins/axios.js`,添加以下内容 ```js import {Message} from "element-ui"; //🚩🚩如果在插件里面需要获取nuxt本身 //需要export default export default function({$axios, redirect}){ $axios.onError(err => { // const {statusCode, message} = err.response.data; // 还未使用 // if(statusCode === 401 || statusCode === 403){ // Message.warning({message: "请先登录"}); // redirect("/user/login"); // } // if(statusCode === 400){ // Message.warning({message}); // } // }) //} // $axios.onError 是一个 nuxt 提供的辅助拦截器函数 // 里面可以拦截到错误 $axios.onError(err=>{ // 这里是每当出错的时候都会被拦截 console.log(err); // 这个 err 是一个错误对象, log 的时候看不出来有什么用 // 用 dir 可以看到里面的内容 console.dir(err); console.log('错误信息是: ' + err.response.data.message); // 弹出一个提示告诉用户 // this.$message.error('') 在这里由于不是组件内部, 没有 this 可以用 // 可以单独引入element ui 当中的 Message 组件 Message.error(err.response.data.message || '系统错误') // 如果需要跳转页面, 比如说需要跳转到登录页 // 可以从 nuxt 中拿到 redirect 函数即可 // redirect('/user/login') }) } ``` ### 3.调用插件 只是一个配置而已 `nuxt.config.js` ```js // 其他代码... plugins: [ '@/plugins/element-ui', { src: '@/plugins/localStorage', ssr: false }, '@/plugins/axios' // 调用插件 ], // 其他代码... ``` **==注意:这是nuxt自己封装的,所以自己安装的axios是不会经过它的,安装的axios的拦截器在下面==** ## 小结 创建 axios 响应拦截插件的步骤 1. 创建文件 2. 在nuxt.config.js 配置引入文件 3. 在插件文件当中 暴露一个函数, 这个函数会被 nuxt 自动触发, 并且可以获得 整个nuxt 对象作为参数 4. 取得 $axios 之后就可以创建拦截器了 5. nuxt axios 插件 提供了一个方便的函数进行拦截器创建 **onError** 6. 在里面进行判断并且弹窗等操作 ## 原版的 axios ### 1.使用原版 axios 封装时的拦截器写法 ```js import { Message } from "element-ui"; // 远程服务器 // 创建一个 axios import axios from 'axios' const http = axios.create({ baseURL: 'http://157.122.54.189:9095' }) http.interceptors.response.use( res => { console.log('自己安装的axios拦截到正常响应状态码为200的请求'); // 如果是状态码就报错的请求, 这里无法拦截, 需要添加第二个函数作为回调 // 如果拦截了请求或者响应, 记得要 return return res }, err => { console.log('这里拦截到状态码本身出错'); // 这里可以拦截到状态码本身出错的请求 console.dir(err) Message.error(err.response.data.message || '系统错误') } ) export default http ``` ```vue //loginForm.vue子组件 if(res&&res.data.token){ this.$message.success("登录成功"); //将结果发送到刚刚新建的vuex中,存储,实现状态持久化,不然刷新之后数据就没了 this.$store.commit('userstore/setUserInfo',res.data) console.log(res.data); this.$router.push('/') } ``` ## 拦截器封装的作用 - 不封装, 功能依旧可以实现 - 如果需要错误弹窗每个页面请求都要做一遍 - 可以通过统一的拦截, 判断到有错误, 就弹窗 - 接口写的时候就只需要关注成功逻辑即可 # 三. 机票首页 ## 重要知识点 * 机票搜索的业务逻辑 * momentjs的使用 ![air](img/air.png) ## 机票首页布局 替换`pages/air/index.vue`的代码如下 ```vue ``` 在上面布局中继续完善机票搜索功能和特价机票列表。 ![image-20200304161943191](img/image-20200304161943191.png) --- ## 封装搜索组件 ​ 机票搜索表单的交互比较多,而且功能比较独立,所以封装成组件导入,后面我们会更多的模块封装成组件。 ### 组件布局 #### 1.新建机票搜索表单组件 `components/air/searchForm.vue`,并替换以下布局代码 ```vue ``` 创建完成后在`pages/air/index.vue`中导入组件 ```vue ``` ### 相关组件介绍 1.`el-autocomplete`远程搜索自动补全组件 组件文档: 2.`el-date-picker`日期选择组件 组件文档: # ==day04== # 每日反馈 拦截器是不是必须要放在插件系统里面 - nuxt 自带的 @nuxtjs/axos 是额外封装好的, 你想拿到这个实例, 就必须通过插件系统 nuxt 文档当中还有其他的扩展办法, 基本都是基于这个插件系统 ![image-20201110083415813](img/image-20201110083415813.png) - 我们自己另外封装的话, 那么就是自己引入的 axios , 就在引入的地方写即可 # 复习回顾 - 退出登录功能 - 注册功能 - 表单布局 - 数据绑定 - 数据校验 - 发送请求 (验证码 + 真正的注册请求) - axios的响应错误拦截 - 机票搜索页布局和相关组件 自动补全的输入框 日期选择器插件 # 开发任务 - [x] 搜索框组件布局 - [x] 搜索框自动补全 - [x] 搜索框日期组件 - [x] 带数据跳转结果页面并发送请求, **完成搜索业务** - [x] 机票搜索页特价机票部分渲染 获取数据, 遍历渲染模板 - [x] 跳转后机票列表渲染 页面布局, 分隔几个大框, 左边的筛选部分头部部分分页部分, 右边侧边栏部分 头部组件, 只是简单的四列 机票列表的遍历, 渲染 # 一. 封装搜索组件 ​ 机票搜索表单的交互比较多,而且功能比较独立,所以封装成组件导入,后面我们会更多的模块封装成组件。 ## 组件布局 新建机票搜索表单组件`components/air/searchForm.vue`,并替换以下布局代码 ```vue ``` 创建完成后在`pages/air/index.vue`中导入组件 ```vue ``` ## 目标思路 ![image-20200510103541578](img/image-20200510103541578.png) 通过用户输入数据, 组合成需要的参数 点击搜索, 跳转页面后, 传递到机票搜索结果页面,再发送请求 需要的参数包括 - 出发地 / 出发地代码 departCity /departCode - 到达地 / 到达地代码 destCity / destCode - 出发时间 departDate ### 相关组件介绍 1.`el-autocomplete`远程搜索组件 组件文档: 2.`el-date-picker`日期选择组件 组件文档: ## 步骤 ### 布局 先用最简单代码实现布局, 所有的数据和函数声明空的就好, 全部不管 #### 获取城市数据 **1.在data中新增变量存储表单数据** 在data中定义需要提交的是`5`个参数 ```vue ``` 注意在`template`的表单中使用`v-model`双向绑定数据到`form`的属性。 比如出发城市`v-model="form.departCity"`: ```vue ``` **2.调用城市查询接口** 使用实时查询的方式调用查询城市的接口,由于出发城市和到达城市都需要调用这个查询接口,所以把查询操作封装到一个独立函数来调用。 ```vue ``` **3.测试城市数据** 接下来我们测试下城市数据,为了查看查询结果,我们在提交方法中打印下表单数据 ```js // 提交表单是触发 handleSubmit(){ console.log(this.form) } ``` 输入: 广州 到 上海 ![1560701275060](img/1560701275060.png) 点击搜索查看打印信息 ![1560702530507](img/1560702530507.png) > 日期`departDate`只需要在`el-date-picker`组件中使用`v-model="form.departDate"`进行绑定即可获得了。 目前为止,我们可以获取到`5`个参数了,但是日期的格式好像有点问题,不过不着急,我们一个个问题来解决。 #### 下拉数据选中赋值 针对城市下拉框选项选中的事件处理,我们应该把`选中的选项`当做是当前的数据 ```vue ``` 注意上面代码中我们使用了`momentjs`,这是一个第三方的包,需要先下载并且导入。 下载命令: ``` npm install --save moment ``` 在组件中引入 ```js import moment from "moment"; ``` 可以重复测试表单数据,会发现`5`个参数都没问题了 #### 搜索跳转 下面来实现页面跳转,需要在`URL`中把`5`个参数都到带过去给搜索结果页`/air/flights` ```vue ``` > `/air/flights`页面暂时还没创建,不过只要能从`URL`中看到参数正确传递即可。 #### 城市切换 这是个把出发城市和到达城市对换位置的功能,实现起来非常简单,把`form`的数据对调换就可以了 ![1560704899767](img/1560704899767.png) 调换的事件函数 ```vue ``` #### 往返 目前接口不支持往返,需要添加一个提示 ````js // tab切换时触发 handleSearchTab(item, index){ if(index === 1){ this.$confirm("目前暂不支持往返,请使用单程选票!", '提示', { confirmButtonText: '确定', showCancelButton: false, type: 'warning' }) } }, ```` ### 总结 1. 关键点在于如何获取跳转链接需要的`5`个参数`!` 2. `el-autocomplete`组件的使用(查看文档) 3. 使用`momentjs`进行时间转换 4. 表单自定义验证 ## 二. 特价机票 ![1605244892385](img/1605244892385.png) ### 思路 1. 特价机票布局 2. 请求数据接口 ### 实现步骤 #### 特价机票布局 `pages/air/index.vue` ```vue ``` > 注意跳转链接的`nuxt-link :to="/air/flights?xxxx`的参数拼接 #### 请求数据接口 ```vue ``` # ==day05== # 复习回顾 - 搜索框组件布局 - 搜索框自动补全 - 搜索框日期组件 - 带数据跳转结果页面并发送请求, 完成搜索业务 - 机票搜索页特价机票部分渲染 - 跳转后机票列表页渲染 # 开发任务 - [x] 航班时间的计算 - [x] 航班座位信息的显示和隐藏功能 - 航班数据的分页 - 航班头部数据的条件筛选 - 本地存储搜索历史记录 # 一. 机票列表页 ![机票搜索列表页](img/%E6%9C%BA%E7%A5%A8%E6%90%9C%E7%B4%A2%E5%88%97%E8%A1%A8%E9%A1%B5.png) ## 重要知识点 * 页面渲染 * 搜索条件过滤 * 本地存储搜索记录 ## 基本布局 新建机票列表页`pages/air/flights.vue`,代码如下 ```vue ``` ## 航班列表 ### 思路 1. 列表头部组件 2. 列表组件布局 3. 无数据提示 ### 实现步骤 #### 1.列表头部组件 预览效果: ![1560756900783](img/1560756900783.png) 1.新建列表头部组件`components/air/flightsListHead.vue`,并添加以下内容: ```vue ``` 2.在`pages/air/fights.vue`中引入组件 ```vue ``` #### 2.机票列表组件 #### 3.侧边栏组件: 内容今天最后面。 预览效果: ![1560758294093](img/1560758294093.png) 1.新建机票列表组件`components/air/flightsItem.vue`,并添加以下内容: ```vue ``` 组件接收一个名为`data`的机票数据,但是目前还没请求后台接口,所以预留到后面再使用。 2.在`pages/air/fights.vue`中引入组件 ```vue ``` ## 航班列表渲染 ![1561172786454](img/1561172786454.png) ### 思路 1. 渲染列表数据 2. 计算相差时间 3. 控制列表展开 4. 分页 ### 实现步骤 #### 渲染列表数据 **1.请求接口数据** `pages/air/flights.vue` ```vue ``` **2.渲染列表组件** `components/air/flightsItem.vue` ```vue ``` 代码虽然很多,但都是很简单的字段替换,其中有一个`起飞时间`到`到达时间`的时间间隔是接口没有返回的,需要我们在前端自己计算,这是一道数学题,和`Vue`无关. #### 计算相差时间 计算`起飞时间`到`到达时间`的时间间隔。 `components/air/flightsItem.vue` ```vue ``` #### 控制列表展开 给组件添加一个开关控制展开收起,比较通用的解决办法。 `components/air/flightsItem.vue` ```vue ``` #### 分页 处理分页的两个关键元素,`pageIndex`和`pageSize`。 `pages/air/flights.vue` ```vue ``` ==因为分页的数据是手算的,所以这里将分页数据计算函数封装成一个计算属性,== 整改数据: ```js data(){ return { flightsData: {}, // 航班总数据 // dataList: [] // 航班列表数据,用于循环flightsItem组件,单独出来是因为要分页 } computed:{ // 因为分页的数据是手算的, // 所以这里将分页数据计算函数封装成一个计算属性 dataList(){ // 因为这里面是页面进入时就执行, 不像之前可以在 // 获取数据 .then 之后执行z // 加一个判断, 有数据,就切割, 没数据就返回默认空数组即可 if(!this.filterList){ return [] } let start=(this.pageIndex-1)*this.pageSize let end=this.pageSize+start return this.filterList.slice(start,end) } }, ``` ### 总结 1. 请求机票列表接口,获取到数据 2. 循环机票列表数据,并且把每一项数据传给`flightsItem.vue`组件 3. 计算出发时间和到达时间的相差值 4. 使用变量控制列表的展开收起 5. 分页数据的切换 ## 条件过滤 ### 思路 1. 条件过滤的布局 2. 渲染过滤数据 3. 过滤列表 4. 撤销条件 ### 实现步骤 #### 条件过滤的布局 页面效果: ![1560767899094](img/1560767899094-1605244555446.png) 1.新建过滤组件`components/air/flightsFilters.vue`,并添加以下内容: ```vue ``` 2.在`pages/air/fights.vue`中引入组件 ```vue ``` #### 渲染过滤数据 在请求列表数据时列表接口已经把过滤的数据信息带过来了,现在只需要把值传给组件就可以了 ![1560770172921](img/1560770172921-1605244555446.png) 1.先在`pages/air/flights.vue`中初始化数据,并传值 ```vue ``` 2.在`components/air/flightsFilters.vue`中渲染过滤数据 ```vue ``` #### 过滤列表`!` 1.过滤条件触发时候需要修改数组列表`flightsData.flights`,这样原来的列表就会被覆盖了,所以需要缓存一份列表用于`根据条件提取数据`。 修改`pages/air/flights.vue`代码如下: ```vue ``` 分页的总页修改 ``` :total="filterList.length" ``` 2.在过滤事件中修改列表数据 `components/air/flightsFilters.vue` ```vue ``` #### 撤销条件 初始化所有条件,还原数据列表。 ```js // 撤销条件时候触发 handleFiltersCancel(){ this.airport = ""; this.flightTimes = ""; this.company = ""; this.airSize = ""; this.$emit("setDataList", this.data.flights); }, ``` #### 封装一下过滤代码:把过滤放到data里面变成过滤属性 ```js rules:{ // 纯函数, 不依赖当前环境 // 不对当前数据造成副作用 // 相同的输入, 每次都能够得到相同的输出 // 🚩🚩🚩📦📦定义每个选项的过滤方法 // 参数:参数要过滤的方法,这里只是定义数据,方法,用于下面的方法 airport:(flights,userOption)=>{ return flights.filter(v=>{ return v.org_airport_name===userOption }) }, flightTimes:(flights,userOption)=>{ // 分割选出来的时间:0-6 const from = Number(userOption.split(',')[0]) const to = Number(userOption.split(',')[1]) console.log(from,to); return flights.filter(v=>{ // 过滤出 在上面分割时间内的数据:0<=time<=6 let time=Number(v.dep_time.split(':')[0]) console.log(time,typeof time); return time >= from && time < to }) }, company:(flights,userOption)=>{ return flights.filter(v=>{ return v.airline_name===userOption }) }, airSize:(flights,userOption)=>{ return flights.filter(v=>{ return v.plane_size===userOption }) }, } ``` ```js methods: { // 解决每次过滤都会使用原始数据, 每一个过滤器都会覆盖上一个过滤器的结果 runFilters(){ let flights=[...this.data.flights] console.log(flights); for(let item in this.rules){ // item要加this let userOption=this[item] let handleFn=this.rules[item] if(userOption){ // 带上参数执行过滤方法 flights=handleFn(flights,userOption) } } this.$emit('setDAataList',flights) }, ``` ### 总结 本节重点在`过滤列表`的实现,但是实现并非只有这一种方法,目的是想在缓存数据的方法中能达到举一反三,这也是编程开发时常见的手段。 ## 跳转到创建订单页 ![1560773545973](img/1560773545973-1605244555446.png) `选定`按钮跳转到订单页,订单页需要的参数`id`和`seat_xid`: ![1560773748576](img/1560773748576-1605244555446.png) 给`选定`按钮添加点击事件实现跳转,即使目前还没订单页,但不影响查看`url`的参数。 `components/air/flightsItem.vue` ```vue ``` ## 历史查询记录 ### 思路 1. 侧边栏布局 2. 添加记录到本地 3. 读取本地数据 4. 监听`url`的变化 ### 实现步骤 #### 3.侧边栏组件 页面效果: ![1560774193680](img/1560774193680-1605244555447.png) 1.新建侧边栏组件`components/air/flightsAside.vue`,并添加以下内容: ```vue ``` 2.在`pages/air/fights.vue`中引入组件 ```vue ``` #### 添加记录到本地 添加的操作需要回到机票首页的搜索表单中,搜索跳转时把搜索的记录添加到本地。 `components/air/searchForm.vue` ```js // 提交表单是触发 handleSubmit(){ // 其他代码... // 添加到本地存储 const airs = JSON.parse(localStorage.getItem('airs') || `[]`); airs.push(this.form); localStorage.setItem("airs", JSON.stringify(airs)); this.$router.push({ path: "/air/flights", query: this.form }) } ``` #### 读取本地数据 从本地读取记录列表并且渲染到页面。 ``components/air/flightsAside.vue`` ```vue ``` 但是现在点击历史记录并不会刷新页面,需要监听`url`的变化,重新发起请求。 #### 监听`url`的变化 `pages/air/flights.vue` ```vue ``` #### 方法2:利用vuex存储数据 > 步骤: > > 新建一个新的仓库history设置state和mutations用于存储表单信息在**机票搜索页**: > > 点击搜索时提交表单时,this.$store.commit('history/addHistory',this.form)存储表单信息, > > 在组件中遍历数组$store.state.history.historyList渲染到侧边栏组件中。 ```js //新建一个新的仓库history.js export const state = () => ({ historyList: [] }) export const mutations = { addHistory(state, data) { state.historyList.unshift(data) } }; //-------------------------------------- //点击搜索时提交表单时,存储数据 this.$store.commit('history/addHistory',this.form) //-------------flightsAside.vue-------------------------
历史查询
{{item.departCity}} - {{item.destCity}}

{{item.departDate}}

选择
``` ### 总结 1.主要功能在于如何存储数据到本地,和读取本地的数据 2.监听`url`的变化 # ==day06== ## 每日反馈答疑 ### **vue 生命周期** > 程序运行过程中, 封装框架的作者, 设计了一些可以供使用开发者后续添加函数的地方 一般是以程序生命时间节点为标准 ### **条件过滤** ![image-20201112090926093](img/image-20201112090926093.png) 创建前 **创建后** ​ created 这个时候 vue 实例当中数据已经准备完毕可以通过 this.xxx 获取或修改, 可以开始 ajax 发请求拿数据 挂载前 **挂载后** ​ mounted 这个时候页面dom 被挂载到了 vue 当中渲染出来, 这个时候开始才能操作dom 更新前 更新后 销毁前 **销毁后** 这是一个组件销毁的时候, 通常担心遗留的 定时器 或者 全局监听等内容会在这里解绑 如 clearTimeout(id) ## 复习回顾 - 跳转后机票列表页渲染 - 航班时间的计算 - 航班座位信息的显示和隐藏 - 航班数据的分页 - 航班数据的条件筛选 ## 开发任务 - # 机票订单页 - 页面布局 - 添加删除乘机人 - 保险数据请求和处理 (渲染和选中时数据的管理) - 获取手机验证码 - 机票订单数据的拼接和提交(今天只开发到发送请求获取订单信息) - 计算机票订单总价格(响应式) ![机票订单页](img/%E6%9C%BA%E7%A5%A8%E8%AE%A2%E5%8D%95%E9%A1%B5.png) ## 重要知识点 * 表单实现逻辑 * 手机验证码 * 计算总价格 ## 订单首页布局 新建`pages/air/order.vue`的代码如下 ```vue ``` ## 订单表单组件 ### 思路 1. 新建表单组件 2. 添加乘机人 3. 移除乘机人 4. 渲染保险数据 5. 拼接订单数据 6. 发送手机验证码 ### 实现步骤 #### 新建表单组件 1.新建订单表单组件`components/air/orderForm.vue`,添加以下内容 ```vue ``` 2.把组件引入到`pages/air/order.vue`中 ```vue ``` #### 添加乘机人 点击该按钮添加一个新的乘机人 ![1560785602730](img/1560785602730.png) 说明乘机人是一个列表数据,我们使用数组保存起来,但是一开始需要显示一位乘机人的位置,所以在数组初始化时候添加一位乘机人。 `components/air/orderForm.vue` ```vue ``` 这样点击添加按钮时候往`users`数组中添加数据即可,然后再把表单数据双向绑定到`users` ```js // 添加乘机人 handleAddUsers(){ console.log(this.users); // this.users.push({ // username:'', // id:'' // }) this.users=[ ...this.users, { username:'', id:'' } ] }, ``` 把表单字段双向绑定到`users` ```vue ``` #### 移除乘机人 根据下标移除`users`列表的元素 ```js // 移除乘机人 handleDeleteUser(index){ this.users.splice(index, 1); }, ``` #### 渲染保险数据 保险的数据来自于后台接口,这也是我们在`URL`中传递过来的两个参数的作用了,需要他们来请求机票相关的信息。 1.由于数据在表单组件和以后的侧边栏组件都要使用,所以在父组件中调用接口(`通常情况下都是父级调用接口比较合理`) `components/air/order.vue` ```vue ``` 2.在表单组件中渲染保险数据:利用`el-checkbox-group`组件 `components/air/orderForm.vue` ```vue ``` #### 拼接订单数据 这是一份提交机票订单的参数文档,我们需要拼接以下的参数 ![1560788151576](img/1560788151576.png) 拼接完后在提交事件`handleSubmit`中先打印出来。 以下代码都在`components/air/orderForm.vue`中编辑。 新增表单的字段到`data` ```js data () { return { users:[ // 乘机人信息 // 乘机人应该是一个数组 // 里面的每个对象都是一个乘机人 // 添加删除的实话, 只需要 push / splice { username:'xiaobai', id:'441611111111111111' } ], insurances:[], //保险id contactName:'xiaobai', //联系人名字 contactPhone:'13722222222', //电话 invoice:false, //是否需要发票 captcha:'000000' } }, ``` > 注意`seat_xid`和`air`两个字段可以从`props`的`data`中获得 **1.用户数据** 用户数据已经保存在`users`中了 **2.联系人名字/联系人手机/验证码** ```vue ``` **4.测试创建订单的参数** ```js // 提交订单 handleSubmit(){ const data = { users: this.users, insurances: this.insurances, contactName: this.contactName, contactPhone: this.contactPhone, invoice: this.invoice, captcha: this.captcha, seat_xid: this.data.seat_infos.seat_xid, air: this.data.id } } ``` > 手机验证码应该是`点击发送验证码后发送到手机`的,这里我们模拟手机请求 #### 发送手机验证码 `compnents/air/orderForm.vue` ```js // 发送手机验证码 handleSendCaptcha(){ if(!this.contactPhone){ this.$confirm('手机号码不能为空', '提示', { confirmButtonText: '确定', showCancelButton: false, type: 'warning' }) return; } if(this.contactPhone.length !== 11){ this.$confirm('手机号码格式错误', '提示', { confirmButtonText: '确定', showCancelButton: false, type: 'warning' }) return; } this.$axios({ url: `/captchas`, method: "POST", data: { tel: this.contactPhone } }).then(res => { const {code} = res.data; this.$confirm(`模拟手机验证码为:${code}`, '提示', { confirmButtonText: '确定', showCancelButton: false, type: 'warning' }) }) }, ``` > 注意目前手机验证码永远都是返回`6`个`0` ## 提交订单 上面已经准备好创建订单的数据了,接下来就提交订单了,订单提交完后会跳转到微信付款页。 `compnents/air/orderForm.vue` ```js // 提交订单 handleSubmit(){ const orderData = { users: this.users, insurances: this.insurances, contactName: this.contactName, contactPhone: this.contactPhone, invoice: this.invoice, captcha: this.captcha, seat_xid: this.data.seat_infos.seat_xid, air: this.data.id } const {user: {userInfo}} = this.$store.state; this.$message({ message: "正在生成订单!请稍等", type: "success" }) this.$axios({ url: `/airorders`, method: "POST", data: orderData, headers: { Authorization: `Bearer ${userInfo.token || 'NO TOKEN'}` } }).then(res => { // 跳转到付款页 this.$router.push({ path: "/air/pay" }); }).catch(err => { const {message} = err.response.data; // 警告提示 this.$confirm(message, '提示', { confirmButtonText: '确定', showCancelButton: false, type: 'warning' }) }) } ``` ## 侧边栏组件 侧边栏组件是一个负责展示的组件,其中总价需要在订单表单组件中计算得出,再传递过来。 ![1560790950171](img/1560790950171.png) ### 思路 1. 新建侧边栏组件 2. 计算总额 ### 实现步骤 #### 新建侧边栏组件 1.新建订单表单组件`components/air/orderAside.vue`,添加以下内容 ```vue ``` > 这里的数据展示和功能几乎都是和`机票列表`是重复的 2.把组件引入到`pages/air/order.vue`中 ```vue ``` #### 计算总额 总金额要使用兄弟组件传值的方式。 在订单表单组件中计算总金额,并传递给父组件 **1.父组件** `pages/air/order.vue` ```vue ``` **2.订单表单组件** `components/air/orderForm.vue` ```js computed:{ totalPrice(){ let res=0 // 机票价格=机票*人数 res=this.data.seat_infos.org_settle_price*this.users.length console.log(res); //保险价格=保险*人数 //几个id就是几份保险,人数是绑定式的,一般要买都买 不买都不买 this.insurances.forEach(id=>{ // 第一层取到选中的保险id this.data.insurances.forEach(dataId=>{ // 遍历原始数据的id if(dataId.id==id){ res+=dataId.price*this.users.length } }) }) console.log(res); this.$emit('setAllPrice',res) return res } } ``` `computed`计算属性的值如果页面中没引用的话函数是不会执行的,所以需要在页面中调用下`allPrice`. 在页面的`template`中任意位置加以下代码 ```vue ``` **3.订单侧边栏组件** ```vue ``` # ==day07== ## **getters** - 用途像 计算属性, 如果储存的数据不符合最终输出的结果, 又不想在获取数据的时候做改变, 就可以通过 getter 的方式做一个中间层计算出来 - 创建办法, 就是在以前的 state / mutations 另外创建一个对象叫 getters 里面的每个属性都是一个函数, 可以接受到 state 作为形参 - 取值办法 $store.getters['userstore/username'] ## **actions** - 有一些异步操作, 不止一个地方需要用到, 如登录之类, 可以考虑封装到 vuex actions 里面 - 封装办法, 就是在以前 state / mutations 额外多一个 actions 对象 里面的每个属性都是一个函数, 可以接受两个参数, (store, data) - 使用方式 this.$store.dispatch(actions路径名字符串, 额外数据), 注意调用完这个 actions 他 # 复习回顾 - 本地存储搜索历史记录 布局和样式 数据的存放(vuex) 如何记录(每当搜索按钮点击的时候) this.commit 如何获取记录渲染 $store.state.history **拓展**,如何避免添加重复记录(遍历历史, 如果有完全一样的则不作处理) - 机票订单页布局 - 添加删除乘机人 - 保险数据请求和处理 (渲染和选中时数据的管理) - 获取手机验证码 - 机票订单数据的拼接和提交 文档少了验证码 - 计算机票订单总价 # 开发任务 - [x] 支付订单的数据校验 - [x] 支付订单的未登录状态处理 - 付款页面 基本布局和数据渲染 - 前端生成付款二维码 - **轮询**查询付款结果 ## 一:订单页 ### 表单的数据校验 (进阶) #### 思路 - **==基本校验的三个基本设置 model / rules / prop==** :model='{}' v-model 1.:rules='rules' (在data定义) 2.:rules='[{ key: value }]' prop='定义rules的键' - **==对于动态增减表单的特殊处理==** :rules='[{ key: value }]' :prop='动态定义rules的键' #### 步骤 - ==校验**联系人**信息(**基础**)== - model 只要是包裹着表单当中数据的**对象**即可 - prop 以**字符串**的形式表明当前表单项在 model 里面的路径 ( 不包括 model 本身) - rules 对应 model 里面每个字段需要进行判断的标准 - ==校验**乘机人**信息(**进阶**)== - 饿了么校验的本质 1. model: 包含表单数据的对象, 即使里面只有一个属性 users 是一个随时可能增加减少的数组也无所谓 ![image-20200517094507072](img/image-20200517094507072.png) 2. prop 当前项在 model 当中数据为位置路径的字符串形式, 如果涉及v-for 遍历, 那么需要使用 index 变量来实际生成每一项的 prop ![image-20200517094454351](img/image-20200517094454351.png) 3. rules 是以 key: value 的形式声明规则, 其中 key 是 prop一模一样的字符串作为校验的标准, prop 会根据自己的 字符串值查找对应 的 rule ![image-20200517094911724](img/image-20200517094911724.png) 4. 因为这种写法在不确定长度的动态数组当中不好应用, 可以使用另一个 rules 的 声明方式 不再声明表单全局的 rules , 单独声明每个表单项的 rules 即可 ![image-20200517095322819](img/image-20200517095322819.png) - **小结** 对于动态增减表单项的校验方式 依然是三个重要部分 model 将 users 用花括号包裹, 硬造一个对象::model='{users}' prop 字符串拼接, 把每个遍历到的表单 index 放入 字符串中 :prop='字符串' rules 没办法 form 上面全局设置, 就变成了表单项单独自己设置 - 发送之前进行总的校验 两种方式, 用 async / await 调用 promise 比较简单 ![image-20200517102152182](img/image-20200517102152182.png) ## 二:支付订单的未登录状态处理 ### 思路 可以处理的办法有很多 - 最粗暴, 任何页面如果没有登录都跳到登录页 - 点击提交的时候, 直接跳到登录页 - 弹出窗口提示登录 - 直接显示登录表单替换按钮供用户使用 怎么让在尽量不打断用户支付的情况下进行登录和注册的校验和提醒? 其实要求用户注册的时间点值得思考的 根据你的目标决定 - 对于客户流量不愁的app 或者网站, 有可能目标就是尽快收集用户信息, 所以就把注册放在很前面 - 如果是一些小网站,每个访客都很珍贵, 尽量让访客在上面花费自己的时间, 进行各种产品挑选, 表单填写, 最后一刻才叫你注册登录,增加留存率 ### 目标 在用户填写机票订单表单的页面 判断是否有登陆 如果已经登录, 就显示提交订单 如果没有登录, 就显示也登陆表单 登陆过后直接显示提交订单按钮 ### 方法 - 复用登录注册页的表单布局 注意样式影响 - 利用当前 token 状态决定显示提交订单还是登录表单 - 如果登录页有做登录后跳转到首页的体验优化, 在订单页注意登录后无需跳转 - 如果在登录或者注册页面, 我们可以设置登录完毕就自动跳转到首页 - 现在我们想要可以实现不自动跳转, 一直留在订单表单页 ![image-20200517112303604](img/image-20200517112303604.png) ## 三:机票付款页 ### 重要知识点 * 前端生成二维码 * 付款结果轮询 ![机票付款页](img/%E6%9C%BA%E7%A5%A8%E4%BB%98%E6%AC%BE%E9%A1%B5.png) ### 基本布局 新建机票付款页`pages/air/pay.vue`,代码如下 ```vue ``` ### 生成二维码 ### 思路 1. 获取订单`id` 2. 获取订单详情 3. 生成二维码 ### 实现步骤 #### 获取订单`id` 获取订单详情的接口需要使用订单`id`。 订单`id`是在提交订单成功时返回的,所以要回到`components/air/orderForm.vue`中处理下跳转,给参数带上订单`id` `components/air/orderForm.vue` ```js // 提交订单 handleSubmit(){ // 其他代码... this.$axios({ // 其他代码... }).then(res => { const {data: {id}} = res.data; // 跳转到付款页 this.$router.push({ path: "/air/pay", query: { id } }); }) // 其他代码... } ``` #### 获取订单详情 `pages/air/pay.vue` ```js ``` 结果中的`payInfo`字段就是付款链接,需要根据链接生成二维码 ![1560832524390](img/1560832524390.png) > 注意:把价格展示到页面上,这里不添加详细步骤了。 #### 生成二维码 下载第三方的生成二维码包 ``` npm install --save qrcode ``` 导入到页面中并使用 `pages/air/pay.vue` ```vue ``` 现在页面应该能看到支付二维码了,请用微信扫扫并付款。 ### 总结 掌握使用`qrcode`前端生成二维码。 ## 支付结果轮询 在付款页需要服务器实时推送信息给客户端,检查是否付款成功然后执行对应的操作,这里我们采用`轮询的方式`调用查询订单状态接口。 `pages/air/pay.vue` ```VUE ``` 查询接口`3`秒查询一次,如果已付款会弹窗提示并停止继续查询,页面销毁也会停止查询。 # ==day08== ## 开始6天的项目实战: ### 目标 小组形式完成,酒店首页,酒店详情页,旅游攻略首页,旅游攻略详情页,文章发布页 小组gitee地址:https://gitee.com/avglee/xian-yun12 ![1606210669306](img/1606210669306.png) 1606210802557 ![1606210826678](img/1606210826678.png) ![1606210842914](img/1606210842914.png) ![1606210877506](img/1606210877506.png) ![1606210886528](img/1606210886528.png) ![1606210913513](img/1606210913513.png)