Vue全家桶+MongoDB+Koa2全栈开发网站

github网址MT-PC

实战准备

  1. 项目安装:
    • npm install -g npx
    • npx create-nuxt-app project-name
    • npm install --update-binary
    • 出现问题:当更改server下面的index.js文件的时候,就是将require改为import后会报错
    • 原因:因为node本身不支持import这个指令,
    • 解决:使用babel
      1. 在package.json文件中更改dev和start,都在配置的末尾加上--exec babel-node
      2. 建立.babelrc文件,文件内容为
          {
              "presets": ["es2015"]
          }
      3. 安装插件:npm install babel-preset-es2015
      4. 重启服务 npm run dev
      
            
            
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
  2. 支持sass语法,安装插件:npm i sass-loader node-sass eslint@^3.18
  3. 支持axios npm install @nuxtjs/axios
    nuxt.config.js:
        modules: [
            '@nuxtjs/axios',
        ],
        axios: {
    
    },
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 版本介绍:
    • Node v10.15.0
    • Vue 2.9.6
    • NPM 6.4.1
    • Webpack 4.1.0
    • Nuxt 2.0.0
  • 新知识点的网址:
  • 项目目录
    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文件,还可以配置很多其他文件

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
  • 逻辑目录:
    layouts/default.vue
        header组件
            topBar 
                Geo
                User
                navBar
            searchBar
        content:按需要加载
        footer组件
    
      
      
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
  • 首页开发

    需求分析(模板设计)在这里插入图片描述

    在这里插入图片描述

    1. 思考
      • 如何节省网络请求
        • 有时候可以考虑将 部分的内容直接写死
      • 如何语义化
      • DOM最简化
    城市服务组件

    在这里插入图片描述

    1. 业务逻辑:
      首先浏览器发出request请求,建立http连接,服务器端可以拿到request.ip,也就是浏览器端向我发起请求的时候,根据http协议,我就可以知道ip地址,然后我拿到ip地址去数据中心做映射,这个ip对应哪个城市,然后就可拿到城市名称,服务器拿到city之后下发给浏览器
    2. 思考:如何节省网络请求?
      • 传统方法:发送两次请求
        • 当页面渲染完了,我向服务器发一个请求(可以是空的内容,因为空的内容也会建立链接),建立链接,拿到ip,然后…,最后拿到city,也就是组件是在mounted事件之后发送一个请求,然后服务器给你这个城市的名称,再渲染到组件上去
        • 弊端:拿到页面,获取城市,一共发了两次请求,除了浪费请求,还有体验问题,就是闪了一下
      • 节省网络请求:发送一次请求
        在请求文档的时候,那个时候服务器已经知道你的ip了,在那个时候,完全可以拿到ip对应的城市,这个数据是可以当时返回给你的,不需要额外再建立一次连接,利用vuex同步状态,再利用ssr,就可以做到一次请求就可以拿到数据
    用户数据&状态

    在这里插入图片描述

    1. 业务逻辑:首先浏览器发一个request请求,然后服务器根据passport来验证当前是否是登录用户,passport会查当前redis,因为你发这个请求的时候,它会带着cookie过来,服务器的passport会用你的cookie再和redis去做认证,如果是登录状态的话,它会返回你的用户名
    2. 网络请求和上面城市组件一样

    组件设计:

    默认模板配置layouts/default.vue
    1. 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>

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
  • header部分:mt-app/components/public/header/index.vue
  • 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%,就整个背景都是灰色的了
      
          
          
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
  • 定位:geo组件s
    1. 位置、引入
      • 位置:components/public/header/geo.vue
      • 在components/public/header/topBar.vue中被引入
    用户登录:user组件
    1. 位置、引入
      • 位置:components/public/header/user.vue
      • 在components/public/header/topBar.vue中被引入
    2. 登录部分:登录或者未登录两种状态
      <!-- 登录 -->
      <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>
      
         
         
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
    右上角->我的美团 手机app 商家中心 网址导航:nav组件

    在这里插入图片描述

    1. nav.vue位置、引入:
      • 位置:components/public/header/nav.vue
      • 在components/public/header/topBar.vue中被引入
    2. 我的美团部分
      用最简单的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>
          ...
          ...
      
         
         
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
    3. 网址导航部分
      官网上这部分的列表结构是有标题有内容
          所以我们采取利用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
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
    搜索框部分:searchBar.vue
    1. 位置、引入
      • 位置:components/public/header/searchbar.vue
      • 在components/public/header/index.vue中被引入
    2. 搜索相关逻辑:
      • 热门搜索:聚焦 没有内容的时候显示热门搜索
        在这里插入图片描述
      • 相关搜索:聚焦 有内容时显示相关搜索
        在这里插入图片描述
      • 这两个彼此独立,放在平行结构中,具体实现如下:
        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
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
        • 19
        • 20
        • 21
        • 22
        • 23
        • 24
        • 25
    3. 问题1:当我聚焦后想点击推荐中的链接的时候,会先触发input事件的blur事件,才能点击,所以在点它(链接)之前,已经触发了blur事件,导致你点击这个链接,没有生效
      • 解决:就是我在失去焦点的时候,把isFocus的变化做个延时的处理
        blur: function(){
                //setInterval和setTimeout中传入函数时,函数中的this会指向window对象,所以用self现将this存起来
                let self = this;
                setTimeout(function(){
                    self.focus = false
                },200)
            }
        
             
             
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
    4. 问题2:我怎么让推荐的内容随着我的输入内容改变,怎么更改数据发出去
      • 方法1:监听v-model内容,也就是search
      • 方法2: 直接观察input事件,在input标签中增加
        <el-input placeholder="搜索商家或地点"@input="input"/>
    全部分类部分menu.vue

    在这里插入图片描述

    1. 位置、引入:
      • 位置:components/index/menu.vue
      • 在pages/index.vue中被引入
    2. 逻辑:
      • 结构拆分:一级标题 —>全部分类
        数据结构:
            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>
        
             
             
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
        • 19
      • 结构拆分:二级标题 —>子分类(美食,外卖,酒店等)
        在这里插入图片描述
        逻辑:每个标题下面对应的内容都不一样,我怎么确定当鼠标划过,我应该显示哪个内容呢
        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)
            },
        
             
             
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
        • 19
        • 20
        • 21
        • 22
        • 23
        • 24
        • 25
        • 26
        • 27
        • 28
        • 29
        • 30
        • 31
        • 32
        • 33
        • 34
        • 35
        • 36
      • 关于鼠标滑动事件的处理:
         因为全部分类下的分类项和分类项下的组件是并行结构
         也就是我要是鼠标移入到分类项下的组件部分,就算做成移出了全部分类
         这样的话,依照之前的原理,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 = ''
             }
        
             
             
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
    休闲生活、住酒店、我是商家,登录,二维码部分:life.vue

    在这里插入图片描述

    1. 位置、引入:
      • 位置:components/index/life.vue
      • 在pages/index.vue中引入
    2. 中间轮播图部分:
      • 位置:components/index/silder.vue
      • 在components/index/life.vue中被引入
      • 写法:参照Element-UIhttps://element.eleme.cn/#/zh-CN/component/carousel
    注册组件:register

    在这里插入图片描述

    1. 位置、引入
      • 位置:pages/register.vue
      • 访问 localhost:3000/register
    2. 编写组件
      • 创建组件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'
        
             
             
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
      • 创建模板:layouts/blank.vue
      • 使用模板
        export default {
            layout:'blank',
        }
        
             
             
        • 1
        • 2
        • 3
      • 创建模板原因:
        因为这个注册组件样式上并不需要header和footer,所以不能使用我们配置好的默认模板:default.vue,要新建一个blank.vue的空模板
    数据结构设计

    在这里插入图片描述

    用户:数据库设计,接口设计,用户注册、登录逻辑
    1. 数据库设计:
      server
          dbs
              models              -->放置数据库数据
                  user.js         -->users表,包括usename,password,email
              config.js           -->数据库配置文件(smtp服务, redis连接, mongodb连接)      
      
         
         
      • 1
      • 2
      • 3
      • 4
      • 5
    2. 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

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
  • 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

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
  • 简要接口介绍,具体见代码:server/interface/users.js
    • 接口
    /users/signup           注册接口
    /users/signin           登陆接口
    /users/verify           发送验证码接口     
    /users/exit             退出
    /users/getUser          登陆状态获取用户名
    
      
      
    • 1
    • 2
    • 3
    • 4
    • 5
    • 在server/index.js中引入路由:
      import users from './interface/users'
      app.use(users.routes()).use(users.allowedMethods())
      
          
          
      • 1
      • 2
  • 将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())
        ...
        ...
    
      
      
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
  • 在上述后台配置结束后,在pages/register组件中定义方法,实现注册逻辑
    • 发送验证码:sendMsg
      1. 先验证用户名,密码是否符合规则
      2. 如果符合规则,将用户输入的用户名(username)和密码(email)作为参数,请求/users/verify接口
      
          
          
      • 1
      • 2
    • 注册: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)
      
          
          
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
  • 实现登录逻辑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="/" 
      
          
          
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
    • 跳转到主页面后,实现 左上角"立即登录" -> “用户名”
      users/components/public/header/user.vue
      1. 我们已经定义了接口/users/getUser,通过请求这个接口就能获取到用户的用户名
      2. 但是我们用什么时候请求接口呢,有两种方式:
        (1) 在vuex中同步这种状态,
        (2) 不增加SSR负担,在组件中页面渲染完毕之后
            我们再去获取接口,我们这里用异步获取
            在mounted生命周期:组件挂载到页面,渲染完毕再去请求,达到异步获取的效果
            请求接口,我们可以用promise.then,也可以用async和await,我们这里用async,await
      
          
          
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
  • 退出逻辑pages/exit.vue
    • 利用中间件
      问:退出(exit.vue)组件中,为什么用中间件来实现退出操作呢,
      答: 因为,我们点击users/components/public/header/user.vue文件中的退出后
          跳转到 退出页面(page/exit.vue)之后,自动的去执行退出操作
          所以利用middleware机制,触发这个获取退出的接口,让这个接口响应完之后,
          我们再做自动化的执行动作        
      
          
          
      • 1
      • 2
      • 3
      • 4
      • 5
  • 补充:开启SMTP服务
  • 关于数据
    1. 获取数据获取有两种方式:
      • 数据库:
        • 数据库数据的导入
          1. 进入到mongodb数据库安装位置
          2. 执行:mongoimport d student -c areas areas.dat
          
                 
                 
          • 1
          • 2
        • 举个栗子:使用数据库中的数据
          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)
          
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
  • 通过别人的接口获取所有城市数据
    • 接口:
      http://cp-tools.cn/sign
      sign = 7296092/4224626
      
          
          
      • 1
      • 2
    • 举个栗子
      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 : []
          }
      })
      
          
          
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
  • 我们这里所有数据获取都主要用接口的方式,可以自己练习一下数据库的方式
  • 城市服务等:接口设计,显示当前城市逻辑等
    1. 简要接口介绍,具体见代码:server/interface/geo.js
      • 简要接口介绍:
        /geo/getPosition       在接口发出请求到服务端,服务端根据当前的ip来查库,给出你当前城市的名称
        /geo/province          获取省份的接口
        /geo/province/:id      给出你指定的id的省份,每一个省份都有一个对应的id,根据id可以查询到这个省份下面所有管辖的城市
        /geo/city              获取所有城市(不是按省份分类的城市)
        /geo/hotCity           获取热门城市
        /geo/menu              获取全部分类下的菜单数据
        接口测试工具:postman
        
             
             
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
      • 在server/index.js中引入路由
        import geo from './interface/geo'
        app.use(geo.routes()).use(geo.allowedMethods())
        
             
             
        • 1
        • 2
      • 如何将接口反映到城市上去
        两种办法:
        (1)直接在组件中请求接口,通过异步的方式,然后更改dom
        (2)用SSR方式,在服务端渲染的时候,拿到接口的值,返回页面,用户体验更高,因为过来的时候已经带来了结果
        
             
             
        • 1
        • 2
        • 3
    2. 获取当前城市,通过SSR方式渲染在初始页面的左上角:
      在这里插入图片描述
      • 创建文件:
        store 
            modules                     -->vuex子模块
                geo.js                  -->当前城市
            index.js                    -->vuex模块(汇总子模块并且定义一些操作)
        
             
             
        • 1
        • 2
        • 3
        • 4
      • 逻辑
        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}}
        
             
             
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
    3. 获取全部分类下的子类,通过SSR方式渲染到components/index/menu.vue
      • 创建文件:
        store 
            modules                     -->vuex子模块
                geo.js                  -->当前城市
                home.js                 -->全部分类下的子类,和热门城市
            index.js                    -->vuex模块(汇总子模块并且定义一些操作)
        
             
             
        • 1
        • 2
        • 3
        • 4
        • 5
      • 逻辑
        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]
        
             
             
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
    4. 其他需要了解知识点
      • 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:    对于请求这个路由的浏览器,服务端返回给浏览器的数据
        
             
             
        • 1
        • 2
        • 3
      • 如何判断SSR效果是不是正确:通过查看源码,因为这个是服务端打回给自己的模板
    搜索相关:接口,搜索逻辑等
    1. 简要接口介绍,具体见代码:server/interface/search.js
      • 接口
        /search/top
        /search/resultsByKeysWords      根据任何一个关键词可以查出来所有相关的列表
        /search/hotPlace                热门景点/热门搜索
        /search/products                查询列表,我们点击某一个关键词并进入后,它会在产品列表页推荐所有的产品
        /search/products/:id            根据每个产品的id查询这个产品的详情
        
             
             
        • 1
        • 2
        • 3
        • 4
        • 5
      • 在server/index.js中引入路由
        import geo from './interface/geo'
        app.use(geo.routes()).use(geo.allowedMethods())
        
             
             
        • 1
        • 2
    2. 搜索:通过调用接口直接返回数据
      • 注意:每输入一个字母都进行一次请求,显然浪费性能,所以引入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)
        
             
             
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
      • lodash详解:https://segmentfault.com/a/1190000015312430
      • 问题:Error: timeout of 1000ms exceeded
        在axios.js配置文件中timeout改为2000
        
             
             
        • 1
    3. 热门城市推荐,通过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 : []
            }
        })
        
             
             
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
      • 将热门城市数据存到vuex中
        • 创建文件:
          store 
              modules                     -->vuex子模块
                  geo.js                  -->当前城市
                  home.js                 -->全部分类下的子类,和热门城市
              index.js                    -->vuex模块(汇总子模块并且定义一些操作)
          
                 
                 
          • 1
          • 2
          • 3
          • 4
          • 5
        • 存储步骤:
          1. 在store/modules/home.js中定义 actions和mutations
              setHotPlace         热门推荐
          2. 在store/index.js中引入home.js
          3. store/index.js中 请求接口/search/hotPlace  --->  得到所有热门城市
          4. 将得到的子类数据 提交到vuex
          
                 
                 
          • 1
          • 2
          • 3
          • 4
          • 5
      • 用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>
            </p>
        
             
             
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
    4. 有格调部分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是我们自己设定的默认显示数据
        
             
             
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
    城市选择页面:changeCity

    在这里插入图片描述

    1. 位置、引入
      • 位置:pages/changeCity.vue
      • 访问:localhost:3000/changeCity
      • 模板:使用默认default.vue模板
      • changeCity中组件
        components:{
            iSelect,    
            Hot,
            Categroy
        }
        
             
             
        • 1
        • 2
        • 3
        • 4
        • 5
      • 这个页面的难点
        • 拼音首字母怎么写,如果写26个英文字母标签再插入,是很失败的
        • 如何通过后端给定接口,返回城市后,根据字母来分类
          • 一个字母对应城市的显示
          • 点击字母,快速定位到该字母对应的所有城市
    按省份选择iselect.vue 那栏

    在这里插入图片描述

    1. 位置、引入:
      • 位置:components/changeCity/iselect.vue
      • 在pages/changeCity.vue中被引入
    2. 逻辑:
      • 搜索框参见Elmement-UI:https://element.eleme.cn/#/zh-CN/component/input
      • 确定需要哪些数据province,city…
      • 将省份和城市做关联(利用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='';
                }
            }
        }
        
             
             
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
        • 19
        • 20
        • 21
        • 22
        • 23
      • 在页面被加载之前将所有省份获取过来,(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
                 }
             })
        },
        
             
             
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
      • 直接搜索部分,数据的处理,利用延时处理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值
            4. 将数据进行过滤,就是城市中包含 我搜索关键字的才留下
        }, 200),
        handleSelect:function(item){
            1. 将当前城市设置为item
            2. 跳转页面,回到初始页
        }
        
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
  • 注意:直接搜索 范围是全国
  • 热门城市hot.vue 那栏

    在这里插入图片描述

    1. 位置、引入:
      • 位置:components/changeCity/hot.vue
      • 在pages/changeCity.vue中被引入
    2. 逻辑:
      • 结构采用dl dt dd,因为是一个标题,很多内容
      • 在mounted声明周期函数中获取数据渲染
        async mounted(){
            let {status, data:{hots}} = await this.$axios.get(`/geo/hotCity`)
            if(status == 200){
                this.list = hots;
            }
        }
        
             
             
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
    按拼音首字母选择categroy.vue 那栏

    在这里插入图片描述

    1. 位置、引入:
      • 位置:components/changeCity/categroy.vue
      • 在pages/changeCity.vue中被引入
    2. 逻辑:
      • 确定显示字母用的节点,利用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>
        
             
             
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
      • 点击字母,快速定位到该字母对应的所有城市->利用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实现定位
        
             
             
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
      • 左侧字母,右侧城市部分,选择合适的数据格式,有利于dom结点的减少
        data(){
            return{
                list:'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''),
                // block用来存储 后面用字母 分类城市部分数据,title代表字母,city代表该字母对应城市
                // block:[title, city:[]]
                block:[],
            }
        },
        
             
             
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
      • 所有城市获取利用接口/geo/city
        let {status, data:{city}} = await self.$axios.get('/geo/city');
        
             
             
        • 1
      • 将每个字母对应的城市选择出来, 将数据改为需要的格式,将字母连带着城市进行排序显示
        • 汉语和拼音的转化:利用库
          1. 引入:
              import pyjs from 'js-pinyin'
          
                 
                 
          • 1
          • 2
        • 将字母对应城市选择出来
          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);
          })
          
                 
                 
          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7
          • 8
          • 9
          • 10
          • 11
          • 12
        • 将得到的数据由对象格式变为数组
          for(let [k, v] of Object.entries(d)){
              // 这个k和v就是[key, value]
              // for of 上网查
              blocks.push({
                  title: k.toUpperCase(),
                  city: v,
              })
          }
          
                 
                 
          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7
          • 8
        • 将字母排序显示
          blocks.sort((a, b)=>a.title.charCodeAt(0) - b.title.charCodeAt(0))
          self.block = blocks;
          
                 
                 
          • 1
          • 2
    产品列表页(products),就是点击搜索出来的页面:products

    在这里插入图片描述

    1. 位置、引入
      • 位置:pages/products.vue中被引入
      • 访问:localhost:3000/products
      • 模板:使用默认default.vue模板
      • products中组件
        components:{
            Crumbs,         ->哈尔滨美团哈尔滨哈尔滨融创乐园
            Categroy,       ->分类,区域的部分
            List,           -> 智能排序,景点详情部分
            Amap            ->地图
        }
        
             
             
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
    2. 逻辑: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,
              }
            })
        }
        
             
             
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
      • 将获取到的数据进行
        • 过滤:有图片的数据
        • 格式化:只取我们需要的数据,并格式化成我们需要的数据格式
      • 没有通往这个页面的入口,就是能触发 访问localhost:3000/products 操作的地方
        在components/public/header/searchbar.vue中更改
        两个热门搜索,一个搜索列表,添加 类似如下语句
        <a :href="'/products?keyword='+encodeURIComponent(item.name)">{{item.name}}</a>
        
             
             
        • 1
        • 2
      • 待实现功能:
        • 点击排序:点击按价格排序或者按人气排序可以实现下面列表的排序
        • 鼠标划过景点简介时候,地图可以定位到相应位置
    3. 注意decode和encode的问题:query的很多插件在源码中进行了decode,所以用的时候,有的已decode了,自己就没必要再写一遍了,会报错
    哈尔滨美团哈尔滨哈尔滨融创乐园:Crumbs

    在这里插入图片描述

    1. 位置、引入

      • 位置:components/products/crumbs.vue
      • 在pages/products.vue中被引入
    2. 逻辑:

      • 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) }}
        
             
             
        • 1
        • 2
    分类,区域的部分:categroy

    在这里插入图片描述

    1. 位置、引入
      • 位置:components/products/categroy.vue
      • 在pages/products.vue中被引入
      • categroy中组件
        components:{
            iselect        ->下拉框(划过酒店住宿,周边游出现的下拉框)
        }
        
             
             
        • 1
        • 2
        • 3
    2. 逻辑:
      • DOM结构:
        • 利用dl里面两个dt(分类和全部)和一个dd,dd里面循环引入组件iselect.vue,展示分类右边的数据,像什么酒店住宿,周边游之类的
        • 将每一项都用一个公共的组件iselect.vue来实现,通过组件中数据的改变来实现页面的布局
        <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>
        
             
             
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
      • 下拉框 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>
          
                 
                 
          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7
          • 8
          • 9
          • 10
          • 11
          • 12
    智能排序,景点详情部分:list

    在这里插入图片描述

    1. 位置、引入
      • 位置:components/products/list.vue
      • 在pages/products.vue中被引入
      • list中组件
        import Item from './product.vue'
        components:{
            Item            ->每个景点的简要介绍:像几颗星,门票价格,位置等
        }
        
             
             
        • 1
        • 2
        • 3
        • 4
    2. 逻辑:
      • DOM结构
        • 采用dl和dd,将智能排序,价格排序,人气等放在一个数组中,利用v-for循环输出数据
        • 每个景点的信息利用组件(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"
              :meta="item"/>
          
                 
                 
          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7
          • 8
          • 9
          • 10
          • 11
          • 12
      • 景点的简要介绍:components/products/product.vue
        在这里插入图片描述
        • DOM结构:参见Element-UI:
          https://element.eleme.cn/#/zh-CN/component/rate
        • 数据:父组件传递
    地图控件Amap

    在这里插入图片描述

    1. 位置、引入
      • 位置:components/public/map.vue
      • 在pages/products.vue中被引入
    2. https://lbs.amap.com/api/javascript-api/guide/overlays/toolbar
    详情页开发 detail.vue

    在这里插入图片描述

    1. 需求分析
      在这里插入图片描述

    2. 位置、引入

      • 位置:pages/detail.vue
      • 访问 localhost:3000/detail.vue
      • 模板:使用默认default.vue模板
      • detail.vue中组件
        components:{
            Crumbs,         ->哈尔滨美团 > 哈尔滨美食 > 哈尔滨火锅
            Summa,          ->商品详情
            List            ->商家团购及优惠下的列表
        }
        
             
             
        • 1
        • 2
        • 3
        • 4
        • 5
      • 跳转到该路由的链接:components/products/product.vue
        <h3><nuxt-link :to="{path:'detail',query:{keyword:meta.name,type:meta.module}}">{{ meta.name }}</nuxt-link></h3>
        
             
             
        • 1
    3. 逻辑:

      • 判断是否显示:商家团购及优惠,显示的条件是登录或者有数据,利用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>
        
             
             
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
      • 思考:访问(详情页)localhost:3000/detail.vue时的请求参数:keyword,type,
        为什么不在data中获取,而是asyncData中
        在访问localhost:3000/detail.vue时的请求参数keyword,type
        只能通过:let {keyword,type}=ctx.query,在服务器端获取到
        而asyncData中正好是在服务器端执行的,
        所以写在asyncData中
        

    代码见:pages/detail.vue中

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
  • 在detail.vue中请求/search/products后
    (请求回来的数据传递路线: detail.vue->list.vue->item.vue)
    返回数据格式如下原因:和data关联,所以,返回数据后,data就不用同样再写一次了
    return {
        keyword,
        product,
        type,
        list,
        login
      }
    
      
      
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
  • 商家团购及优惠下的列表 List

    在这里插入图片描述

    1. 位置、引入
      • 位置:components/details/list.vue
      • 在pages/detail.vue中被引入
      • list.vue中的组件
        components:{
            item        ->每条数据
        }
        
             
             
        • 1
        • 2
        • 3
    2. 逻辑:
      • DOM结构:
        <ul>
          <li>{{ list.filter(item=>item.photos.length).length }}款套餐</li>
          <item
            v-for="(item,idx) in list"
            :key="idx"
            :meta="item" />
        </ul>
        
             
             
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
      • 数据的获取:两种方式
        • SSR:我在页面下发的时候就把数据塞进去了
          • SSR方式,用户体验好,用户直接拿到信息,
          • 连 接口都保护起来了,比如说我创建购物车接口,我根本就暴露不出来,因为这个动作是在服务端执行的,客户端看不到创建购物车
        • 拿到空页面之后额外请求数据
      • item组件(components/details/item.vue)
        在这里插入图片描述
        • 用于渲染DOM结构的数据获取:
          pages/detail.vue请求接口/search/products
          将数据传递给components/details/list.vue
          list.vue将数据传递给item组件
          
                 
                 
          • 1
          • 2
          • 3
        • 点击抢购商品,创建购物车
          1. 请求接口/cart/create:创建购物车,将刚创建的购物车id返回
          2. 创建成功后,根据购物车id跳转到购物车页面->pages/cart.vue
          3. 补充: 实际应用中,浏览器传给服务端一个产品的id
                  然后这个id对应产品库中的某个商品
                  然后再将该商品的名称,价钱等信息传给服务端,
                  但是我们这里没有真正的产品库,所以
                  只能通过 直接传给服务端商品的名称,价钱等信息
                  来获取服务器端对应的数据 这样的方式
          
                 
                 
          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7
          • 8
        • 创建购物车接口::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())
          
                 
                 
          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7
          • 8
    购物车:cart

    在这里插入图片描述

    1. 位置、引入
      • 位置:pages/cart.vue
      • 访问 localhost:3000/cart.vue
      • 模板:使用默认default.vue模板
      • cart.vue中组件
        components:{
            list            ->订单列表
        }
        
             
             
        • 1
        • 2
        • 3
      • 跳转到该路由的链接:components/details/item.vue
        window.location.href=`/cart/?id=${id}`
        
             
             
        • 1
    2. 逻辑
      • 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> 
        
             
             
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
      • 订单列表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 所有订单数据,由子组件全部渲染出来
          
                 
                 
          • 1
          • 2
        • 逻辑:
          父组件通过接口获取数据,传入子组件数组,存储在cartData中,
          子组件通过Element-UI结构渲染数据,
          如果我在子组件中更改了购买商品的数量,也就是cartData中的值被更改了,
          那么,我们在父组件监听的total(所有订单总价),也就会重新计算
          然后重新渲染父组件中 下面这个结构中的数据
             <p>
                  应付金额:<em class="money">¥{{ total }}</em>
             </p>
          
                 
                 
          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7
          • 8
        • 注意:仔细看一下list.vue的数据计算和DOM结构!有一部分需要好好理解
      • 提交订单:点击"提交订单",请求/order/createOrder接口,如果请求成功,跳转页面至全部订单页
    全部订单页:order

    在这里插入图片描述

    1. 需求分析
      在这里插入图片描述
    2. 位置、引入
      • 位置:pages/order.vue
      • 访问 localhost:3000/order.vue
      • 模板:使用默认default.vue模板
      • detail.vue中组件
        components:{
            List            ->订单列表
        }
        
             
             
        • 1
        • 2
        • 3
      • 跳转到该路由的链接:pages/cart.vue
        this.$alert(`恭喜您,已成功下单,订单号:${id}`, '下单成功', {
            confirmButtonText: '确定',
            callback: action => {
                location.href = '/order'
                }
            })
        }
        
             
             
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
      • 创建订单和返回全部订单接口: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())
        
             
             
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
    3. 逻辑
      • 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 ? '待付款' : '已付款'
                  }
                }),
              }
            }
          }
        
             
             
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
      • 点击"全部订单"或者"待付款"或者"待使用"等,样式和数据对应改变
        点击元素,触发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
                }
              })
            },
        
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    问题
    1. 搜索失去焦点,热门推荐还在
    2. 还有莫名其妙会报错,会出现什么靓丽什么的搜索结果
    3. 注册时候同一个验证码也可以注册
                                    </div>
    
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值