外卖webAPP(一)

一,首页页面情况

 

1.底部四个切换栏路由组件路由配置

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)
// 引入路由组件
import Msite from '@/views/Msite'
import Order from '@/views/Order'
import Profile from '@/views/Profile'
import Search from '@/views/Search'

const routes = [
  {
    path: '/msite',
    component: Msite
  },
  {
    path: '/order',
    component: Order
  },
  {
    path: '/profile',
    component: Profile
  },
  {
    path: '/search',
    component: Search
  },
  {
    path: '/',
    redirect: '/msite'
  }
]

const router = new VueRouter({
  routes
})

export default router

在app.vue入口组件引入路由

<template>
  <div>
    <h3>app</h3>
    <router-view></router-view>
    <FooterGuide></FooterGuide>
  </div>
</template>

二,在footerGuider组件中

2.1,在底部的切换栏标签中有个on类名,渲染颜色,我们需要点击底部的四个切换栏(导航式路由),然后颜色也会跟着变化(根据路由的路径来判断)

    <footer class="footer_guide border-1px">
    <a href="javascript:;" class="guide_item " :class="{on:$route.path ==='/msite'}" @click="goTo('/msite')">
      <span class="item_icon">
        <i class="iconfont icon-waimai"></i>
      </span>
      <span>外卖</span>
    </a>
    <a href="javascript:;" class="guide_item" :class="{on:$route.path ==='/search'}" @click="goTo('/search')">
      <span class="item_icon">
        <i class="iconfont icon-search"></i>
      </span>
      <span>搜索</span>
    </a>
    <a href="javascript:;" class="guide_item"  :class="{on:$route.path ==='/order'}"   @click="goTo('/order')">
      <span class="item_icon">
        <i class="iconfont icon-dingdan"></i>
      </span>
      <span>订单</span>
    </a>
    <a href="javascript:;" class="guide_item"  :class="{on:$route.path ==='/profile'}"   @click="goTo('/profile')">
      <span class="item_icon">
        <i class="iconfont icon-geren"></i>
      </span>
      <span>我的</span>
    </a>
  </footer>
  .guide_item
      display flex
      flex 1
      text-align center
      flex-direction column
      align-items center
      margin 5px
      color #999999
      &.on
        color blue
 // 点击底部切换栏,跳转到对应的路由组件 
    goTo(path){
      this.$router.replace(path)
    }

三,定义公共headerTop组件

首页,搜索,订单,我的这四个路由组件页面顶部是一样的,我们需要将header抽离出来定义公共组件,但是我们发现首页组件顶部左边多了一个搜索图标,右边多个登录|注册

此时我们需要用到作用域插槽,子组件headerTop利用slot组件占位,父组件传入结构

首页组件(miste)顶部样式

搜索组件(search)顶部样式

订单(order)顶部样式

我的(profile)顶部样式

3.1,定义公共组件HeaderTop,利用具名插槽

<template>
  <header class="header">
    <!--插槽作用域,子组件占位,父组件传入结构 -->
    <slot name="left"></slot>
    <span class="header_title">
      <span class="header_title_text ellipsis"
        >{{title}}</span
      >
    </span>
    <!--插槽作用域,子组件占位,父组件传入结构 -->
    <slot name="right"></slot>

  </header>
</template>

接收父组件传递过来的属性

export default {
  name:'HeaderTop',
  props:['title'],

3.2,在首页msite组价,插入子组件,需要两个结构给子组件

    <HeaderTop title="昌平区北七家宏福科技园(337省道北)">
      <template>
        <span class="header_search" slot="left">
          <i class="iconfont icon-sousuo"></i>
        </span>

        <span class="header_login" slot="right">
          <span class="header_login_text">登录|注册</span>
        </span>
      </template>
    </HeaderTop>

 在搜索组件(search)插入子组件,不需要结构给子组件

<HeaderTop title="搜索"></HeaderTop>

在订单组件(order)插入子组件,不需要结构给子组件

 <HeaderTop title="订单列表"></HeaderTop>

3.3,在msite首页组件添加轮播图插件swiper,

1.安装5版本的swiper,  npm  i swiper@5  -S     目前最新是6,不稳定

//swiper引入js和css
// import Swiper from "swiper";
// import "swiper/css/swiper.min.css";
<div class="swiper-container">
        <div class="swiper-wrapper">
          
          <div class="swiper-slide">
            <a href="javascript:" class="link_to_food">
              <div class="food_container">
                <img src="./images/nav/9.jpg" />
              </div>
              <span>甜品饮品</span>
            </a>
            <a href="javascript:" class="link_to_food">
              <div class="food_container">
                <img src="./images/nav/10.jpg" />
              </div>
              <span>商超便利</span>
            </a>
            <a href="javascript:" class="link_to_food">
              <div class="food_container">
                <img src="./images/nav/11.jpg" />
              </div>
              <span>美食</span>
            </a>
            <a href="javascript:" class="link_to_food">
              <div class="food_container">
                <img src="./images/nav/12.jpg" />
              </div>
              <span>简餐</span>
            </a>
            <a href="javascript:" class="link_to_food">
              <div class="food_container">
                <img src="./images/nav/13.jpg" />
              </div>
              <span>新店特惠</span>
            </a>
            <a href="javascript:" class="link_to_food">
              <div class="food_container">
                <img src="./images/nav/14.jpg" />
              </div>
              <span>准时达</span>
            </a>
            <a href="javascript:" class="link_to_food">
              <div class="food_container">
                <img src="./images/nav/1.jpg" />
              </div>
              <span>预订早餐</span>
            </a>
            <a href="javascript:" class="link_to_food">
              <div class="food_container">
                <img src="./images/nav/2.jpg" />
              </div>
              <span>土豪推荐</span>
            </a>
          </div>
        </div>
        <!-- Add Pagination -->
        <div class="swiper-pagination"></div>
      </div>
 mounted() {
    new Swiper('.swiper-container', {
      loop: true, // 可以循环轮播
      // 如果需要分页器
      pagination: {
        el: '.swiper-pagination'
      }
    })
  }

3.4,在msite首页组件拆分shoplist组件,html,css,图片拆分

3.5,在我的组件(profile),点击登录|注册链接,跳转到登录页面

 

配置登录路由组件对象

import Login from '@/views/Login'

const routes = [
  {
    path: '/msite',
    component: Msite,
    meta: {
      showFooterGuider: true
    }
  },
  {
    path: '/order',
    component: Order,
    meta: {
      showFooterGuider: true
    }
  },
  {
    path: '/profile',
    component: Profile,
    meta: {
      showFooterGuider: true
    }
  },
  {
    path: '/search',
    component: Search,
    meta: {
      showFooterGuider: true
    }
  },
  {
    path: '/',
    redirect: '/msite'
  },
  {
    path: '/login',
    component: Login,
    meta: {
      showFooterGuider: false
    }
  }
]
  <section class="profile-number">
        <!-- 路由链家登录 -->
        <router-link to="/login"  class="profile-link">
            <div class="profile_image">
            <i class="iconfont icon-person"></i>
          </div>
          <div class="user-info">
            <p class="user-info-top">登录/注册</p>
            <p>
              <span class="user-icon">
                <i class="iconfont icon-shouji icon-mobile"></i>
              </span>
              <span class="icon-mobile-number">暂无绑定手机号</span>
            </p>
          </div>
          <span class="arrow">
            <i class="iconfont icon-jiantou1"></i>
          </span>
        </router-link>

两个细节问题

1.在登录组件,点击左上角的返回图标,返回到我的组件,利用路由返回函数

       <button class="login_submit">登录</button>
          </form>
          <a href="javascript:;" class="about_us">关于我们</a>
        </div>
        <a href="javascript:" class="go_back" @click="$router.back()">
          <i class="iconfont icon-jiantou2"></i>
        </a>
      </div>

2.在登录页面底部的四个切换栏需要影藏,在路由对象中配置meta属性,布尔值,然后在app组件中v-if判断下

在app.vue中判断判断是否显示footer切换栏

<template>
  <div>
    <router-view></router-view>
    <FooterGuide v-if="$route.meta.showFooterGuider"></FooterGuide>
  </div>
</template>

3.6,axios二次封装,接口函数书写以及发送请求

新建api文件夹,新建ajax.js文件,axios二次封装 Ajax.js, 后台服务器地址http://localhost:4000

 
// 对axios的二次封装
import axios from 'axios'
const service = axios.create({
 
  baseURL: 'http://localhost:4000',
  timeout: 2000
})

// 请求拦截器
service.interceptors.request.use(config => {
  return config
})

// 响应拦截器
service.interceptors.response.use(
  response => {
    return response.data
  },
  error => {
    alert('请求出错' + error.message || '未知错误')
    //以后不允许用户继续处理: 中断promise链
    return new Promise(() => {}) //返回pending状态的promise 中断
  }
)

export default service

新建index.js文件,编写接口函数

import ajax from './ajax.js'

// 1、根据经纬度获取位置详情
export const reqAddress = (geohash) => ajax.get(`/position/${geohash}`)
// 2、获取食品分类列表
export const reqFoodCategorys = () => ajax.get('/index_category')
// 3、根据经纬度获取商铺列表
export const reqShops = (longitude, latitude) => ajax.post('/shops', {params:{longitude, latitude}})
// 4、根据经纬度和关键字搜索商铺列表
export const reqSearchShop = (geohash, keyword) => ajax.post('/search_shops', {params:{geohash, keyword}})
// 6、用户名密码登陆
export const reqPwdLogin = ({name, pwd, captcha}) => ajax.post('/login_pwd', {name, pwd, captcha})
// 7、发送短信验证码
export const reqSendCode = (phone) => ajax.get('/sendcode', {params:{phone}})
// 8、手机号验证码登陆
export const reqSmsLogin = (phone, code) => ajax.post('/login_sms', {phone, code})
// 9、根据会话获取用户信息
export const reqUserInfo = () => ajax.get('/userinfo')
// 10、用户登出
export const reqLogout = () => ajax.get('/logout')

/**
 * 获取商家信息
 */
export const reqShopInfo = () => ajax.get('/info')

/**
 * 获取商家评价数组
 */
export const reqShopRatings = () => ajax.get('/ratings')

/**
 * 获取商家商品数组
 */
export const reqShopGoods = () => ajax.get('/goods')

在main.js中测试接口数据

import * as API from '@/api'
API.reqAddress(('40.10038,116.36867')).then(r => {
  console.log(r)
})

此时,控制台报错了,请求跨域了,本机浏览器端口8080请求4000端口,我们需要在vue.config.js 配置代理proxy

module.exports = {
  lintOnSave: false,
  devServer: {
    //需要转发路由的路径
    proxy: {
      '/api': {
        target: 'http://localhost:4000',
        pathRewrite: { '^/api': '' },
        changeOrigin: true
      }
    }
  }
}

然后在封装的ajax中修改下路径,此时再次请求数据,是OK的

// 对axios的二次封装
import axios from 'axios'
const service = axios.create({
  baseURL: 'http://localhost:8080/api',
  // baseURL: 'http://localhost:4000',
  timeout: 2000
})

3.7,利用vuex,发送ajax,状态管理

1.新建store文件夹,新建index.js

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
// 引入接口函数
import {
  reqAddress,
  reqFoodCategorys,
  reqShops,
  reqUserInfo,
  reqLogout,
  reqShopRatings,
  reqShopGoods,
  reqShopInfo,
  reqSearchShop
} from '@/api'

const state = {
  latitude: 40.10038, // 纬度
  longitude: 116.36867, // 经度
  address: {}, //地址相关信息对象

}

const mutations = {
  RECEIVE_ADDRESS(state, address) {
    state.address = address
  },


}

const actions = {
  // 异步获取地址
  async getAddress({ commit, state }) {
    // 发送异步ajax请求
    const geohash = state.latitude + ',' + state.longitude
    const result = await reqAddress(geohash)
    // 提交一个mutation
    if (result.code === 0) {
      const address = result.data
      commit('RECEIVE_ADDRESS', address)
    }
  },



}

const getters = {}

export default new Vuex.Store({
  state,
  mutations,
  actions,
  getters
})

在main.js中引入store,并注册

import store from '@/store'

Vue.config.productionTip = false

new Vue({
  router,
  render: h => h(App),
  store
}).$mount('#app')

在msite首页组件中从store中获取数据

除了dispatch到store中actions的函数,还可以用mapActions中触发

  mounted(){
    this.$store.dispatch('getAddress')
  },

或者

import { mapActions, mapState } from 'vuex'
  methods:{
    ...mapActions(['getAddress'])
  },
 mounted(){
    // this.$store.dispatch('getAddress')
    this.getAddress()
  },

从store中的获取数据

computed:{

    // address(){
    //   return this.$store.state.address
    // }
    ...mapState(['address'])
  },

填充数据

<HeaderTop :title="address.name">
      <template>
        <span class="header_search" slot="left">
          <i class="iconfont icon-sousuo"></i>
        </span>

        <span class="header_login" slot="right">
          <span class="header_login_text">登录|注册</span>
        </span>
      </template>
    </HeaderTop>

3.8,在msite首页组件中的轮播图中获取动态数据,并填充数据

注:这个轮播图,应该是是一个双重数组遍历,一维数组是有几页,二维数组是有八个数据

然而我们后台返回的数据是一个数组数据,而不是一个二维数组。因此我们需要编写逻辑去实现两个数组嵌套

  mounted() {

    this.$store.dispatch('getCategorys')
  },
computed: {
 
    ...mapState(['address', 'categorys']),

返回的部分数据

{
  "code": 0,
  data: [
          {
            id: 1,
            is_in_serving: true,
            description: "0元早餐0起送,每天都有新花样。",
            title: "预订早餐",
            link: "",
            image_url: "/d/49/7757ff22e8ab28e7dfa5f7e2c2692jpeg.jpeg",
            icon_url: "",
            title_color: "",
            __v: 0
          },
          {
            id: 65,
            is_in_serving: true,
            description: "",
            title: "土豪推荐",
            image_url: "/d/49/7757ff22e8ab28e7dfa5f7e2c2692jpeg.jpeg",
            link: "",
            icon_url: "",
            title_color: "",
            __v: 0
          },
          ... 共n条数据
        ]
}
// 发送请求返回的数据categorys是一个数组,但是看html,应该要双重遍历,是一个二维数组,将categorys
    // 遍历下,需要将每一项对象添加到嵌套的小数组中
    // 思路;1.需要定义一个大数组,小数组,将小数组嵌套在大数组中,小数组添加categorys的每一个对象
    // 2.小数组只能有八项对象(页面只有八项标签),然后添加一个新的小数组

在computed中计算该数组,重要

categorysArr() {
      // 定义大数组
      let arr = []
      // 定义小数组
      let minArr = []
      this.categorys.forEach(item => {
        // 如果当前小数组已经满了, 创建一个新的
        if (minArr.length === 8) {
          minArr = []
        }

        // 如果minArr是空的, 将小数组保存到大数组中
        if (minArr.length === 0) {
          arr.push(minArr)
        }

        // 将每一项对象添加到小数组中
        minArr.push(item)
      })

      return arr
    }
  }

此时整理得到的数据

将数据填充到模板中

  data() {
    return {
      baseImageUrl: 'https://fuss10.elemecdn.com'
    }
  },
 <div class="swiper-container">
        <div class="swiper-wrapper">
          <div
            class="swiper-slide"
            v-for="(c1, index) in categorysArr"
            :key="index"
          >
            <a
              href="javascript:"
              class="link_to_food"
              v-for="(c2, index) in c1"
              :key="c2.id"
            >
              <div class="food_container">
                <img :src="baseImageUrl + c2.image_url" />
              </div>
              <span>{{ c2.title }}</span>
            </a>
          </div>
        </div>

此时有个bug,swiper轮播图并没有效果,如何有效果呢

1.监视异步请求获取的数据categorys,

2.此时数据回来了,但是页面还没有跟新,需要在下一次dom页面跟新完成执行

 watch: {
      categorys (value) { // categorys数组中有数据了, 在异步更新界面之前执行
        // 使用setTimeout可以实现效果, 但不是太好
        /*setTimeout(() => {
          // 创建一个Swiper实例对象, 来实现轮播
          new Swiper('.swiper-container', {
            loop: true, // 可以循环轮播
            // 如果需要分页器
            pagination: {
              el: '.swiper-pagination',
            },
          })
        }, 100)*/

        // 界面更新就立即创建Swiper对象
        this.$nextTick(() => {// 一旦完成界面更新, 立即调用(此条语句要写在数据更新之后)
          // 创建一个Swiper实例对象, 来实现轮播
          new Swiper('.swiper-container', {
            loop: true, // 可以循环轮播
            // 如果需要分页器
            pagination: {
              el: '.swiper-pagination',
            },
          })

        })

      }
    },
 
原文地址:https://www.cnblogs.com/fsg6/p/14302707.html