GPS定位系統(四)——Vue前端 前言 正題 總結 關於作者

前言

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

前面已經學習了Android、Java端的代碼實現,現在開始介紹網站前端vue的管理框架。

文中也會有大量代碼,對於admin管理框架,我是模仿iview-amin,然後新建一個項目,手敲下來的,只取了自己所需的模塊,目的就是爲了練手,期間也遇到了很多問題,建議大家也可以自己模仿者手敲一遍。也可以使用elementUI,這個框架整體而言比iview更好一些。

GPS定位系統系列

GPS定位系統(一)——介紹

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

GPS定位系統(三)——Java後端

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

GPS定位系統(五)——Docker

收穫

學習完這篇文章你將收穫:

  • Vue + Vue-cli + iview + axios + vue-router + vuex 的實踐
  • 高德地圖 js api的使用
  • axios restful接口的異常處理封裝
  • 上傳頭像
  • modal彈框編輯個人信息template
  • admin管理框架

[TOC]

正題

一、admin框架介紹

框架搭建了整體架構,單頁面的web應用。通用縮放式菜單欄,選項卡式管理網頁、麪包屑導航、bug日誌管理、全屏等功能。

框架使用了通用熱門的 VUE一套框架,包括Vue + Vue-cli + vuex + vue-router+iview,具體還請參見源碼。

單頁面大致結構

<template>
  <Layout style="height: 100%" class="main">
    <Sider ref="sider" class="sider" hide-trigger collapsible :collapsed-width="78" v-model="isCollapsed">
      <div class="logo-con">
        <img v-show="!isCollapsed" :src="maxLogo" class="max-logo" key="max-logo"/>
        <img v-show="isCollapsed" :src="minLogo" class="min-logo" key="min-logo"/>
      </div>
      <!--      展開狀態-->
      <Menu class="open-menu" ref="menu" :active-name="$route.name" :open-names="openedNames" theme="dark" width="auto"
            v-show="!isCollapsed"
            @on-select="turnToPage">
        <template v-for="item in menuList">
          <!--          有children且只有1個-->
          <template v-if="item.children && item.children.length===1">
            <MenuItem :name='item.children[0].name'>
              <Icon :type="item.children[0].meta.icon"></Icon>
              <span>{{showTitle(item.children[0])}}</span>
            </MenuItem>
          </template>
          <template v-else>
            <!--            有children 大於1個嵌套-->
            <template v-if="item.children && item.children.length>1">
              <Submenu :name='item.name'>
                <template slot="title">
                  <Icon :type="item.meta.icon || ''"/>
                  <Span>{{showTitle(item) }}</Span>
                </template>
                <template v-for="subitem in item.children">
                  <MenuItem :name="subitem.name">
                    <Icon :type="subitem.meta.icon"></Icon>
                    <Span>{{showTitle(subitem)}}</Span>
                  </MenuItem>
                </template>
              </Submenu>
            </template>
            <!--            沒有children-->
            <template v-else>
              <MenuItem :name='item.name'>
                <Icon :type="item.meta.icon"></Icon>
                <Span>{{showTitle(item)}}</Span>
              </MenuItem>
            </template>

          </template>

        </template>
      </Menu>
      <!--      收縮狀態-->
      <div v-show="isCollapsed" class="close-menu">
        <template v-for="item in menuList">
          <template v-if="item.children && item.children.length>0">
            <Dropdown placement="right-start" @on-click="turnToPage" class='dropdown'>
              <a type="text" class="drop-menu-a">
                <Icon :type="item.meta.icon"></Icon>
              </a>
              <template v-for="subitem in item.children">
                <DropdownMenu slot="list">
                  <DropdownItem :name="subitem.name">
                    <a type="text" class="drop-item-a">
                      <Icon :type="subitem.meta.icon"></Icon>
                      <span>{{showTitle(subitem)}}</span>
                    </a>
                  </DropdownItem>
                </DropdownMenu>
              </template>
            </Dropdown>
          </template>
          <template v-else>
            <Tooltip transfer placement="right" :content="showTitle(item)">
              <a @click="turnToPage(item.name)" type="text" class="drop-menu-a">
                <Icon :type="item.meta.icon"></Icon>
              </a>
            </Tooltip>
          </template>
        </template>
      </div>
    </Sider>
    <layout>
      <Header class="header" :style="{padding:0}">

        <Icon @click.native="collapsedSider" :class="rotateIcon" :style="{margin:'0 20px'}" type='md-menu'
              size="24"></Icon>
        <custom-bread-crumb show-icon style="margin-left: 30px;" :list="breadCrumbList"></custom-bread-crumb>
        <div class="header-right">
          <user :message-unread-count="0" :user-avatar="userAvatar" :user-name="userName"/>
          <error-store v-if="$config.plugin['error-store'] && $config.plugin['error-store'].showInHeader"
                       :has-read="hasReadErrorPage" :count="errorCount"></error-store>
          <fullscreen v-model="isFullscreen" style="margin-right: 10px;"/>
        </div>
      </Header>
      <Content class="main-content-con">
        <Layout class="main-layout-con">
          <div class="tag-nav-wrapper">
            <tags-nav :value="$route" @input="handleClick" :list="tagNavList" @on-close="handleCloseTag"></tags-nav>
          </div>
          <Content class="content-wrapper">
            <keep-alive>
              <router-view/>
            </keep-alive>
          </Content>
        </Layout>
      </Content>
      <!--      <Footer class="footer">Footer</Footer>-->
    </layout>
  </Layout>
</template>

對於縮放菜單的功能較爲複雜,可以細品一下,能收穫許多。

二、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的<head>中加入 <script type="text/javascript" src="https://webapi.amap.com/maps?v=1.4.15&key=你的key"></script>

注意這裏需要在body前面,不然有時候地圖加載不出來

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

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

2)vue文件的template中加入``<div id="map"></div>

注意map需要設置寬高

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

文件中引入mapUI

<script src="//webapi.amap.com/ui/1.1/main.js"></script>
<script>
 import AMap from 'AMap'

3)使用

 const map = new AMap.Map('map', {
          resizeEnable: true,
          zoom: 11
        })
        this.map = map
        map.plugin(['AMap.ToolBar', 'AMap.MapType'], function () {
          map.addControl(new AMap.ToolBar())
          map.addControl(new AMap.MapType({showTraffic: false, showRoad: false}))
        })

注意:如果同一頁面使用多個地圖,需要div的id設置爲不同,比如,map1、map2等,Map()構造也要傳入相應的map id

不出意外,地圖就能加載出來了。

2、實時定位

實時定位功能主要邏輯爲:

1、從接口獲取所有人的最新的定位信息,然後生成marker標註在地圖上

2、並且給每個標註都注入個人信息,如頭像、名字、時間等信息

3、可以下拉選擇用戶,定位到地圖中心,且打開infoWindow展示個人信息

4、由於接口是沒有存具體地址的,需要利用經緯度座標,轉爲具體地址展示在infowindow上

詳情,請參看源碼。

獲取接口數據,添加markers和dropdown

 allNowGps() {
        this.getAllNowGps().then(res => {
          this.data = res;
          this.addMarkers(res)
          this.addDropDown(res)
          console.log("res", JSON.stringify(res))
        })
      },
 addMarkers(data) {
        this.infoWindow = new AMap.InfoWindow({offset: new AMap.Pixel(0, -70)});
        data.forEach((item, index) => {
          var marker = new AMap.Marker({
            position: [item.lng, item.lat],
            icon: personLogo,
            offset: new AMap.Pixel(-15, -66),
            map: this.map,
            extData: item,
          });
          var info = this.getContentByItem(item);
          marker.content = info.join("<br/>") //使用默認信息窗體框樣式,顯示信息內容
          marker.on('mouseover', this.markerHover);
          // marker.emit('mouseover', {target: marker});
          // 真正的content的使用,是在hover打開infowindow的時候,傳給indowindow進行展示的
        })
        this.map.setFitView();
      },
addDropDown(res) {
        if (res && res.length > 0) {
          res.forEach(item => {
            //默認獲取自己的
            //注意從cookie裏面拿出來默認是string
            if (item.uid == this.$store.state.user.userId) {
              this.currentUser = item
              //設爲中心 顯示信息
              this.map.setCenter([item.lng, item.lat])
            }
          })
        }
      },

hover之後展示infowindow

 markerHover(e) {
        var _this = this
        //轉換經緯度爲具體地址
        this.geocoder.getAddress(e.target.getPosition(), function (status, result) {
          if (status === 'complete' && result.info === 'OK') {
            var address = result.regeocode.formattedAddress;
            console.dir(address);
            const content = e.target.content + '<br/>地址:' + address;
            const marker = e.target
            const item = e.target.getExtData()
            console.log('extData', e.target.getExtData());
            _this.infoWindow.setContent(content)
            _this.infoWindow.open(_this.map, e.target.getPosition());
          }
        });
      },

這裏e.target其實就是marker,然後marker的extData可以裝載 item的數據,這裏也取出來展示

注意,這裏使用了geocoder來把經緯度座標轉爲具體地址

3、歷史軌跡

歷史軌跡功能主要邏輯爲:

1、默認獲取當前用戶的歷史軌跡數據,可以通過日期篩選,並生成軌跡和marker

2、獲取所有的用戶,生成選擇下拉列表,選擇下拉,可以獲取對應用戶的歷史軌跡數據

3、利用高德地圖的繪製軌跡

4、開始marker沿着軌跡移動,模擬移動行爲

 allUsers() {
        this.handleGetAllUsers().then(res => {
          console.log("users", JSON.stringify(res))
          let users = [];
          users = res;
          this.users = users
          if (res && users.length > 0) {
            users.forEach(item => {
              //默認獲取自己的
              //注意從cookie裏面拿出來默認是string   == 就可以比較
              if (item.uid == this.$store.state.user.userId) {
                this.currentUser = item
                this.selectGpsHis(item.uid)
              }
            })
          }
        })
      },

獲取所有用戶

selectGpsHis(uid, from, to) {
        this.getGpsHis({uid, from, to}).then(data => {
          console.log("getGpsHis", JSON.stringify(data))
          this.showGpsHis(data)
        })
      },

獲取對應用戶的歷史軌跡數據

showGpsHis(data) {
        this.map.clearMap()
        this.followPath = []

        data.forEach((item, index) => {
          const gps = [item.lng, item.lat]
          this.followPath.push(gps)
        })
        //重組數據爲 [[lng,lat],[lng2,lat2]]

        if (this.followPath.length === 0) {
          Message.warning('無歷史數據')
          return
        }

        this.marker = new AMap.Marker({
          map: this.map,
          position: this.followPath[0],
          icon: personLogo,
          offset: new AMap.Pixel(-15, -66),
        });

        // 繪製軌跡
        var polyline = new AMap.Polyline({
          map: this.map,
          path: this.followPath,
          showDir: true,
          strokeColor: "#28F",  //線顏色
          // strokeOpacity: 1,     //線透明度
          strokeWeight: 6,      //線寬
          // strokeStyle: "solid"  //線樣式
        });

        var passedPolyline = new AMap.Polyline({
          map: this.map,
          // path: this.followPath,
          strokeColor: "#AF5",  //線顏色
          // strokeOpacity: 1,     //線透明度
          strokeWeight: 6,      //線寬
          // strokeStyle: "solid"  //線樣式
        });

        this.marker.on('moving', function (e) {
          passedPolyline.setPath(e.passedPath);
        });
        this.map.setFitView()
      },

繪製軌跡並且生成marker

 startAnimation() {
        this.marker.moveAlong(this.followPath, 5000);
      },
stopAnimation() {
          this.marker.stopMove()
        },

開始軌跡、停止軌跡,軌跡速度是按照地圖的每小時多少千米的速度來設置的。

注意:拿到的數據,需要重組成 軌跡所需的數據結構。

四、modal彈框template自定義

對於用戶管理,我們有創建和編輯用戶資料,兩個格式相同,可以複用。這裏自定義的modal模板,可以參考一下。

代碼較長:

<template>
  <Modal :value="isShow" :title="title" @on-visible-change="handleVisible">
    <Upload
      ref="upload"
      :show-upload-list="false"
      :on-success="handleUploadSuccess"
      :format="['jpg','jpeg','png']"
      :max-size="2048"
      :on-format-error="handleUploadFormatError"
      :on-exceeded-size="handleUploadMaxSize"
      :headers="header"
      type="drag"
      :action="uploadUrl"
      style="display: inline-block;width:50px;height:50px;margin-bottom: 50px">
      <Avatar :src='user.avatar' style="width:50px;height: 50px"/>
    </Upload>
    <Form ref="user" :model="user" :rules="ruleValidate">
      <FormItem label="用戶名" prop="username">
        <Input v-model="user.username"/>
      </FormItem>
      <FormItem label="密碼" prop="password">
        <Input v-model="user.password"/>
      </FormItem>
      <FormItem label="姓名">
        <Input v-model="user.name"/>
      </FormItem>
      <FormItem label="手機" prop="mobile">
        <Input v-model="user.mobile"/>
      </FormItem>
    </Form>
    <div slot="footer">
      <Button size="large" @click="handleCancel">取消</Button>
      <Button type="primary" size="large" @click="handleConfirm('user')">確定</Button>
    </div>
  </Modal>
</template>
<script>
  export default {
    name: 'edit-user',
    props: {
      //姓名、頭像、手機、用戶名、密碼、
      user: {
        uid: '',
        avatar:'',
        name: '',
        username: '',
        password:null,
        mobile: '',
      },
      //是否顯示彈框
      isShow: false,
      //上傳所需的token
      header: {},
      //上傳地址
      uploadUrl: {},
      isEdit: {
        type: Boolean
      },
    },
    computed: {
      title() {
        return this.isEdit === true ? "修改用戶信息" : "新建用戶信息"
      },
      ruleValidate() {
        console.log('ruleValidate', this.isEdit)
        const rule = this.isEdit === true ? this.editRuleValidate : this.createRuleValidate
        console.log('ruleValidate', rule)
        return rule
      }
    },
    watch:{
      isShow(val){
        console.log('isShow',val)
      }
    },
    data() {
      return {
        editRuleValidate: {
          username: [
            {required: true, message: 'The name cannot be empty', trigger: 'blur'}
          ],
          mobile:[
            { required: false, message: "請輸入手機號碼", trigger: "blur" },
            { pattern: /^1[3456789]\d{9}$/, message: "手機號碼格式不正確", trigger: "blur"}
          ]
        },
        createRuleValidate: {
          username: [
            {required: true, message: 'The name cannot be empty', trigger: 'blur'}
          ],
          password: [
            {required: true, message: 'The password cannot be empty', trigger: 'blur'}
          ],
          mobile:[
            { required: false, message: "請輸入手機號碼", trigger: "blur" },
            { pattern: /^1[3456789]\d{9}$/, message: "手機號碼格式不正確", trigger: "blur"}
          ]
        }
      }
    },
    methods: {
      handleUploadSuccess(res, file) {
        console.log(file.response)
        this.user.avatar = file.response
      },
      handleUploadFormatError(file) {
        this.$Notice.warning({
          title: 'The file format is incorrect',
          desc: 'File format of ' + file.name + ' is incorrect, please select jpg or png.'
        });
      },
      handleUploadMaxSize(file) {
        this.$Notice.warning({
          title: 'Exceeding file size limit',
          desc: 'File  ' + file.name + ' is too large, no more than 2M.'
        });
      },
      //處理確定
      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)
          }
        })
      },
      handleCancel() {
        //關閉彈框
        this.$emit('visible', false)
      },
      handleVisible(visible) {
        //每次都清空驗證信息 因爲編輯和創建不一樣
        this.$refs['user'].resetFields();
        //發送事件給父組件 修改自己的visible狀態(注意這裏 不能用v-model數據綁定 子組件不能修改父組件傳來的prop的對象狀態)
        this.$emit('visible', visible)
      }
    },
  };
</script>

使用:

<div v-if="isShowEdit">
      <EditUser :user="newUser"
                :isEdit="isEdit"
                @visible="this.handleEditVisible"
                                :is-show="true"
                :header="{'token': this.$store.state.user.token}"
                :upload-url="this.$config.baseUrl + 'upload'"
                @ok="handleEditOk"
      ></EditUser>
    </div>

通過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。

上傳頭像

<Upload
      ref="upload"
      :show-upload-list="false"
      :on-success="handleUploadSuccess"
      :format="['jpg','jpeg','png']"
      :max-size="2048"
      :on-format-error="handleUploadFormatError"
      :on-exceeded-size="handleUploadMaxSize"
      :headers="header"
      type="drag"
      :action="uploadUrl"
      style="display: inline-block;width:50px;height:50px;margin-bottom: 50px">
      <Avatar :src='user.avatar' style="width:50px;height: 50px"/>
    </Upload>

上傳頭像注意下,如果我們上傳圖標需要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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章