GPS定位系统(四)——Vue前端

前言

GPS系列——Vue前端,github项目地址

前面已经学习了Android、Java端的代码实现,现在开始介绍网站前端vue的管理框架。

文中也会有大量代码,对于admin管理框架,我是模仿iview-amin,然后新建一个项目,手敲下来的,只取了自己所需的模块,目的就是为了练手,期间也遇到了很多问题,建议大家也可以自己模仿者手敲一遍。也可以使用elementUI,这个框架整体而言比iview更好一些。

GPS定位系统系列

GPS定位系统(一)——介绍

GPS定位系统(二)——Android端

GPS定位系统(三)——Java后端

GPS定位系统(四)——Vue前端

GPS定位系统(五)——Docker

  • Docker nginx 二级域名无端口访问多个web项目
  • Docker nginx https二级域名无端口访问多个web项目
  • 持续部署——Travis+Docker+阿里云容器镜像

收获

学习完这篇文章你将收获:

  • Vue + Vue-cli + iview + axios + vue-router + vuex 的实践
  • 高德地图 js api的使用
  • axios restful接口的异常处理封装
  • 上传头像
  • modal弹框编辑个人信息template
  • admin管理框架

[TOC]

正题

一、admin框架介绍

主页

框架搭建了整体架构,单页面的web应用。通用缩放式菜单栏,选项卡式管理网页、面包屑导航、bug日志管理、全屏等功能。

GPS定位系统(四)——Vue前端_第1张图片
image-20200710144618941

框架使用了通用热门的 VUE一套框架,包括Vue + Vue-cli + vuex + vue-router+iview,具体还请参见源码。

单页面大致结构


对于缩放菜单的功能较为复杂,可以细品一下,能收获许多。

二、axios封装

我们的java后台的接口统一数据为restful结构的

{code:xxx,msg:xxx,data:xxx}

对于axios而言封装上面要注意其返回的respon的结构,以及异常response的结构的处理。

这里先放整体axios封装代码:

axios.js

import axios from 'axios'
import store from '@/store'
import {Message} from 'iview'

// import { Spin } from 'iview'
const addErrorLog = errorInfo => {
  const {statusText, status, request: {responseURL}} = errorInfo
  let info = {
    type: 'ajax',
    code: status,
    mes: statusText,
    url: responseURL
  }
  console.log("addErr:" + JSON.stringify(info))
  if (!responseURL.includes('save_error_logger')) store.dispatch('addErrorLog', info)
}

class HttpRequest {
  constructor(baseUrl = baseURL) {
    this.baseUrl = baseUrl
    this.queue = {}
  }

  getInsideConfig() {
    const config = {
      baseURL: this.baseUrl,
      // headers: {
      //   'Content-Type': "application/json;charset=utf-8"
      // }
    }
    return config
  }

  destroy(url) {
    delete this.queue[url]
    if (!Object.keys(this.queue).length) {
      // Spin.hide()
    }
  }


  interceptors(instance, url) {
    // 请求拦截
    instance.interceptors.request.use(config => {
      // 添加全局的loading...
      if (!Object.keys(this.queue).length) {
        // Spin.show() // 不建议开启,因为界面不友好
      }
      this.queue[url] = true
      return config
    }, error => {
      return Promise.reject(error)
    })
    // 响应拦截
    instance.interceptors.response.use(res => {
      console.log("res:" + JSON.stringify(res))
      this.destroy(url)
      const {data: {code, data, msg}, config} = res
      if (code == 200) {
        return data;
      } else {
        this.dealErr(code, msg)
        let errorInfo = {
          statusText: msg,
          status: code,
          request: {responseURL: config.url}
        }
        addErrorLog(errorInfo)
        return Promise.reject(res.data)
      }
    }, error => {
      console.log("error:" + JSON.stringify(error))
      this.destroy(url)
      let errorInfo = error.response
      if (!typeof(errorInfo) === undefined && !errorInfo) {
        const {request: {statusText, status}, config} = JSON.parse(JSON.stringify(error))
        errorInfo = {
          statusText,
          status,
          request: {responseURL: config.url}
        }
        addErrorLog(errorInfo)
        const data = {code: status, msg: statusText}
        this.dealErr(data.code, data.msg)
      } else {
        Message.error('网络出现问题,请稍后再试')
      }
      return Promise.reject(error)
    })
  }


  dealErr(c, msg) {
    console.log("code:" + c)
    console.log("msg:" + msg)
    switch (c) {
      case 400:
        Message.error(msg)
        break;
      case 401:
        Message.error('登录过期,请重新登录')
        break;
      // 404请求不存在
      case 404:
        Message.error('网络请求不存在')
        break;
      // 其他错误,直接抛出错误提示
      default:
        Message.error("系统错误")
    }
  }

  request(options) {
    const instance = axios.create()
    options = Object.assign(this.getInsideConfig(), options)
    this.interceptors(instance, options.url)
    return instance(options)
  }


}

export default HttpRequest

api.request.js:

import HttpRequest from '@/libs/axios'
import config from '@/config'
const baseUrl = config.baseUrl

const axios = new HttpRequest(baseUrl)
export default axios

调用:

import axios from '@/libs/api.request'
import Qs from 'qs'

export const login = ({username, password}) => {
  const data = {
    username,
    password
  }
  return axios.request({
    url: 'login',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    data: Qs.stringify(data),
    method: 'post'
  })
}

注意:如果要使用form表单的形式,需要做转化,这里可以简单方便的使用Qs库来直接stringify,也别忘了设置headers的'Content-Type': 'application/x-www-form-urlencoded',因为axios默认的是json格式。

1、interceptors的response编写

res => {
      console.log("res:" + JSON.stringify(res))
      this.destroy(url)
      const {data: {code, data, msg}, config} = res
      if (code == 200) {
        return data;
      } else {
        this.dealErr(code, msg)
        let errorInfo = {
          statusText: msg,
          status: code,
          request: {responseURL: config.url}
        }
        //添加到日志
        addErrorLog(errorInfo)
        return Promise.reject(res.data)
      }
    }
{
    "data":{
        "code":200,
        "data":{
            xxxx
        },
        "msg":"请求成功"
    },
    "status":200,
    "statusText":"",
    "headers":{
        "content-length":"487",
        "content-type":"application/json;charset=UTF-8"
    },
    "config":{
        "url":"http://127.0.0.1:9090/get_info",
        "method":"post",
        "headers":{
            "Accept":"application/json, text/plain, */*",
            "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbk5hbWUiOiJrayIsImV4cCI6MTU5Njk1Nzg1MSwidXNlcklkIjoiMTMifQ.ChaBg4n5KKsF7ISj8uzHV0eh_JKadoVIBtNG4oUtp8U"
        },
        "baseURL":"http://127.0.0.1:9090/",
        "transformRequest":[
            null
        ],
        "transformResponse":[
            null
        ],
        "timeout":0,
        "xsrfCookieName":"XSRF-TOKEN",
        "xsrfHeaderName":"X-XSRF-TOKEN",
        "maxContentLength":-1
    },
    "request":{

    }
}

注意,axios接口请求的response的数据结构如上。

可以看到获取数据需要res.data.data,我们这里用解构 const {data: {code, data, msg}, config} = res 一下,code==200(其实是res.data.code)的时候返回data(其实就是返回res.data.data)

2、error处理

这里的error可以分为3类:

  • 服务器端的业务的http error
  • 前端的http error
  • 前端http error
dealErr(c, msg) {
    console.log("code:" + c)
    console.log("msg:" + msg)
    switch (c) {
      case 400:
        Message.error(msg)
        break;
      case 401:
        Message.error('登录过期,请重新登录')
        break;
      // 404请求不存在
      case 404:
        Message.error('网络请求不存在')
        break;
      // 其他错误,直接抛出错误提示
      default:
        Message.error("系统错误")
    }
  }

这里封装一个方法,用于处理服务器端的业务的http error前端的https error,因为他们结构都是相同的。

error => {
      console.log("error:" + JSON.stringify(error))
      this.destroy(url)
      let errorInfo = error.response
      if (!typeof(errorInfo) === undefined && !errorInfo) {
        const {request: {statusText, status}, config} = JSON.parse(JSON.stringify(error))
        errorInfo = {
          statusText,
          status,
          request: {responseURL: config.url}
        }
        addErrorLog(errorInfo)
        const data = {code: status, msg: statusText}
        this.dealErr(data.code, data.msg)
      } else {
        Message.error('网络出现问题,请稍后再试')
      }
      return Promise.reject(error)
    }

但是还有一种error也要处理,这里判断如果error.response不为空,则为http类型的error,使用dealErr,如果为空,则说明是非http类型的err,直接toast 网络出现问题,请稍后再试(比如,前端跨域报错,请求不符合规范等错误,就是非http类型的err)

结构如下

{
    "message":"Network Error",
    "name":"Error",
    "stack":"createError handleError",
    "config":{
        "url":"http://127.0.0.1:9090/login",
        "method":"post",
        "data":"username=kk&password=kk",
        "headers":{
            "Accept":"application/json, text/plain, */*",
            "Content-Type":"application/x-www-form-urlencoded"
        },
        "baseURL":"http://127.0.0.1:9090/",
        "transformRequest":[
            null
        ],
        "transformResponse":[
            null
        ],
        "timeout":0,
        "xsrfCookieName":"XSRF-TOKEN",
        "xsrfHeaderName":"X-XSRF-TOKEN",
        "maxContentLength":-1
    }
}

三、高德地图相关功能

1、引入sdk使用

1)在public文件夹下的index.html的中加入

注意这里需要在body前面,不然有时候地图加载不出来

vue.config.js配置文件中加入

module.exports = {
  configureWebpack: {
    externals: {
      'AMap': 'AMap',
      'AMapUI': 'AMapUI'
    },
  },
}

2)vue文件的template中加入``

注意map需要设置宽高

#map{
  width: 100%;
  height: 100%;
}

文件中引入mapUI




使用:

通过isEdit去判别是编辑还是新建

这里有一点比较重要:

isShowEdit是用于我们动态去展示和隐藏modal弹框,使用的是v-if。网上很多人,是使用modal的v-model或者value来控制modal的显示和隐藏的,原本我也是那样做的,但是后来发现,那样做非常不稳定和可靠,有时候弹框弹出来,其双向绑定的is-show,并未能和modal的状态统一。所以,最终使用的是div+v-if的方式来控制。

还有就是,关于父组件和子组件之间传值的问题:

父给子,一般是通过props来接收,并且,我们希望的是单向的,父可以改变控制子,但是,子不能改变去控制父,不然会报错。而需要使用,子发事件回调给父,来改变父的状态的方式来实现。

//处理确定
      handleConfirm(name) {
        this.$refs[name].validate((valid) => {
          console.log('handleConfirm validate',valid)
          if (valid) {
            //发送ok事件
            this.$emit('ok', {user: this.user, isEdit: this.isEdit})
            //关闭弹框
            this.$emit('visible', false)
          }
        })
      },
      
 handleVisible(visible) {
        //每次都清空验证信息 因为编辑和创建不一样
        this.$refs['user'].resetFields();
        //发送事件给父组件 修改自己的visible状态(注意这里 不能用v-model数据绑定 子组件不能修改父组件传来的prop的对象状态)
        this.$emit('visible', visible)
      }

this.$emit('visible', false) 使用这个来改变其父的isShowEdit的值,从而隐藏或者显示自身modal。

上传头像


      
    

上传头像注意下,如果我们上传图标需要header,比如传token的话,需要把header作为参数传进来。

五、登录验证

对于登录验证,我们这边是使用vue-router的beforeEach统一处理页面的跳转来实现的。

const router = new Router({
  routes: routers,
  mode: 'history',
})
const turnTo = (to, access, next) => {
  // if (canTurnTo(to.name, access, routes)) next() // 有权限,可访问
  // else next({replace: true, name: 'error_401'}) // 无权限,重定向到401页面
  next()
}
router.beforeEach((to, from, next) => {
  iView.LoadingBar.start()
  const token = getToken()
  if (!token && to.name !== LOGIN_PAGE_NAME) {
    // 未登录且要跳转的页面不是登录页
    next({
      name: LOGIN_PAGE_NAME // 跳转到登录页
    })
  } else if (!token && to.name === LOGIN_PAGE_NAME) {
    // 未登陆且要跳转的页面是登录页
    next() // 跳转
  } else if (token && to.name === LOGIN_PAGE_NAME) {
    // 已登录且要跳转的页面是登录页
    next({
      name: homeName // 跳转到homeName页
    })
  } else {
    //这由于暂时没有权限系统 直接 跳转即可
    // if (store.state.user.hasGetInfo) {
      turnTo(to, store.state.user.access, next)
    // } else {
    //   store.dispatch('getUserInfo').then(user => {
    //     // 拉取用户信息,通过用户权限和跳转的页面的name来判断是否有权限访问;access必须是一个数组,如:['super_admin'] ['super_admin', 'admin']
    //     turnTo(to, user.access, next)
    //   }).catch(() => {
    //     setToken('')
    //     next({
    //       name: 'login'
    //     })
    //   })
    // }
  }
})

router.afterEach(to => {
  //设置标题
  setTitle(to, router.app)
    //隐藏进度条
  iView.LoadingBar.finish()
  window.scrollTo(0, 0)
})

总结

源码很多,所以其实很多东西都在源码里面了,可能内容篇幅较长,很少有人能够完整看完,但是,写在这里只为某些时候可能遇到类似问题,有一个借鉴参考的地方即可。就如同,我自己手敲admin框架的时候,很多时候iview-amin就是我的一个可以借鉴和参考的项目,遇到有不会的或者没有思路的,就可以参考借鉴一下,这样会好很多。

整个系列,前端、移动端、后端,都有了,打通了,接下来就是要学习一下,怎么打包,部署到服务器那些东西了。

服务器呢,我打算再使用docker,学习一番docker+nginx+mysql等实现前端和后端的线上部署,具体请参看

GPS定位系统(五)——Docker

关于作者

作者是一个热爱学习、开源、分享,传播正能量,喜欢打篮球、头发还很多的程序员-。-

热烈欢迎大家关注、点赞、评论交流!

:https://www.jianshu.com/u/d234d1569eed

github:https://github.com/fly7632785

CSDN:https://blog.csdn.net/fly7632785

掘金:https://juejin.im/user/5efd8d205188252e58582dc7/posts

你可能感兴趣的:(GPS定位系统(四)——Vue前端)