利用vue+vue-router+elementUI實現簡易通信錄

一個具備基本增刪改查功能的通信錄,數據保存在本地的localStorage中。css

 demo地址: https://junjunhuahua.github.iohtml

1. 所用技術vue

js框架: vue2  https://cn.vuejs.org/node

ui框架: elementUI  http://element.eleme.io/#/zh-CNwebpack

腳手架: vue-cligit

單頁: vue-router  https://router.vuejs.org/zh-cn/github

模塊打包: webpackweb

 

2. 腳手架搭建vue-router

# 全局安裝 vue-cli
$ npm install -g vue-cli
# 建立一個基於 webpack 模板的新項目
$ vue init webpack contact
$ cd contact
# 安裝依賴
$ npm install
$ npm run dev
這是vue官方基於webpack的腳手架,run dev後瀏覽器會自動打開localhost:8080,也可使用run build命令,執行build命令後會自動將src目錄中的內容進行編譯打包壓縮,而後在dist目錄中能夠看到這些文件
 
3. 目錄結構
項目根目錄: 
 

build爲構建項目所用的node代碼,config爲構建時的一些配置項,dist爲打包後(npm run build 用於發佈)的代碼,node_modules爲node模塊,src爲開發時所用的代碼。mongodb

src目錄:

assets爲全局css,圖片,以及一些工具類的js,components爲vue的組件,router爲路由配置,app.vue爲主頁面的組件,config.js爲目錄配置項,main.js爲入口js

 
4. main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import ElementUI from 'element-ui'
import utils from './assets/utils.js'
import 'element-ui/lib/theme-chalk/index.css'
import './assets/normalize.css'

Vue.use(ElementUI)
Vue.use(utils)

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  ElementUI,
  template: '<App/>',
  components: { App }
})

main.js的主要工做是引入一些框架,全局css,以及工具函數,還會處理vue組件的加載,最後實例化vue。

 
5. App.vue
App.vue能夠認爲是應用最外層的一個容器。
<template>
  <div id="app">
    <div class="app-left">
      <el-row class="tac">
        <el-col>
          <el-menu :default-active="menuIndex" class="el-menu-vertical-demo"
                   background-color="#545c64" text-color="#fff" :unique-opened="menuUniqueOpen" :router="menuRouter"
                   active-text-color="#ffd04b">
            <h3>個人應用</h3>
            <template v-for="(item, index) in menuData">
              <!-- 此處的index需顯示轉換爲string,不然會報warn -->
              <el-submenu :index="'' + (index + 1)">
                <template slot="title">{{ item.name }}</template>
                <template v-for="(subItem, i) in item.value">
                  <!-- 此處index格式爲父級的index加上下劃線加上當前的index,index都需加1 -->
                  <router-link tag="span" :to="subItem.path">
                    <el-menu-item :index="subItem.name">{{ subItem.title }}</el-menu-item>
                  </router-link>
                </template>
              </el-submenu>
            </template>
          </el-menu>
        </el-col>
      </el-row>
    </div>
    <div class="app-right">
      <router-view></router-view>
    </div>
  </div>
</template>

<script>
  import menuData from './config'

  export default {
    name: 'app',
    data () {
      return {
        menuData,
        menuIndex: '', // 菜單當前所在位置
        menuUniqueOpen: true, // 菜單項是否惟一開啓
        menuRouter: true // 是否開啓路由模式
      }
    },
    mounted: function () {
      ...
    },
    watch: {
      '$route' (to) {
        this.menuIndex = to.name
      }
    }
  }
</script>

 

這邊偷了一個懶,沒有把左側的menu單獨作成一個vue而是混入App.vue中。
 
6. 路由
在正式寫代碼以前,首先要肯定要項目的結構,模塊如何劃分,哪一個模塊對應哪一個路由。
由於整個項目如今就劃分出兩個大板塊,通信錄與記帳本,因此路由第一級就只有contact和account兩種。
Vue.use(VueRouter)
let myRouter = new VueRouter({
  routes: [
    {
      path: '*',
      component: () => import('../components/NotFoundComponent.vue')
    },
    {
      path: '/',
      redirect: '/contact'
    },
    {
      path: '/contact',
      name: 'Contact',
      component: () => import('../components/contact/List.vue')
    },
    {
      path: '/contact/edit',
      name: 'Contact',
      component: () => import('../components/contact/Edit.vue')
    },
    {
      path: '/account',
      name: 'Account',
      component: () => import('../components/account/list.vue')
    }
  ]
})

能夠看到上面/contact和/contact/edit的name是相同的,這是爲了讓在新增或者編輯聯繫人頁面下,還能讓active狀態停留在左側個人聯繫人上,能夠看到App.vue中的代碼this.menuIndex = to.name就是進行的該操做,

雖然這樣vue會報一個warn告訴我別重名[捂臉],暫時能想到的就是這樣的操做方式了,有考慮過依靠判斷path來肯定是否顯示高亮狀態,可是當目錄層級較深且較複雜的狀況下,這樣就不是很靠譜了。

component這裏爲何是這種形式,而不是直接用一個組件名呢,由於當路由開始多起來的時候,一下把全部的組件都加載進來會很是很是慢且會加載到許多當時並無用到的組件,經過import這種形式,可讓webpack將路由變換時用到的組件分開打包,網頁會根據使用狀況再進行

因爲router是vue的組件,因此使用時記得要Vue.use一下。

 

 7. 聯繫人列表頁 --- contact/list.vue

<template>
  <div class="contact-list">
    <div class="contact-list-header">
      <el-button @click="goToNew" type="primary">新增聯繫人</el-button>
    </div>
    <div class="contact-list-content">
      <template>
        <div class="contact-list-wrap">
          <h3>高級檢索</h3>
          <el-form ref="contactSearch" :model="searchParams" :inline=true>
            <el-form-item label="姓名">
              <el-input v-model="searchParams.name" placeholder="請輸入須要檢索的姓名"></el-input>
            </el-form-item>
          </el-form>
          <el-button type="primary" size="mini" round @click="contactSearch('contactSearch')">搜索</el-button>
        </div>
        <div class="contact-list-wrap">
          <h3>聯繫人列表</h3>
          <el-table
            :data="listNewData"
            style="width: 100%"
            @row-click="viewContact"
            :default-sort="{prop: 'name', order: 'descending'}"
          >
            <el-table-column
              label="姓名"
              prop="name"
              sortable
              width="180">
            </el-table-column>
        ...
            <el-table-column
              label="功能">
              <template scope="scope">
                <el-button size="mini" type="primary" @click.stop="editContact(scope)">編輯</el-button>
                <el-button size="mini" @click.stop="deleteContact(scope)">刪除</el-button>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </template>
    </div>
    <contact-view ref="contactView" :viewData="curData" :viewShow.sync="viewShow"></contact-view>
  </div>
</template>

<script>
  import contactView from './View.vue'

  export default {
    data () { ... },
    components: {
      contactView
    },
    computed: {
      listNewData: function () { ... },
    mounted: function () {
      this.listData = this.utils.getLocalStorage('vueContact')
    },
    methods: {
      goToNew: function () {
        this.$router.push('/contact/edit')
      },
      sexFormatter: function (row) { ... },
      deleteContact: function (res) {
        let data = res.row
        this.$confirm('此操做將永久刪除該聯繫人, 是否繼續?', '提示', {
          confirmButtonText: '肯定',
          cancelButtonText: '取消',
          type: 'warning',
          callback: (action) => {
            if (action === 'confirm') {
              this.$delete(this.listData, data.id)
              this.utils.setLocalStorage('vueContact', this.listData)
            }
          }
        })
      },
      editContact: function (res) {
        let data = res.row
        this.$router.push({
          path: '/contact/edit', query: {id: data.id}
        })
      },
      viewContact: function (row) {
        this.viewShow = true
        this.curData = this.listData[row.id]
      },
      contactSearch: function () {
        let data = this.utils.getLocalStorage('vueContact')
        let newData = {}
        for (let item in data) {
          if (data[item].name.indexOf(this.searchParams.name) > -1) {
            newData[item] = data[item]
          }
        }
        this.listData = newData
      }
    }
  }
</script>

list.vue至關於該模塊的主頁,新增與編輯頁面經過右上角的新建按鈕或者列表中的編輯按鈕進入,查看頁面經過引入View.vue做爲一個彈窗放在列表頁中展現,不單獨設置路由。

列表展現所使用的是elementUI的table組件

刪除對象時必定要使用$delete,不然不會觸發視圖更新

view.vue代碼:

<template>
  <div class="contact-view">
    <el-dialog :before-close="closePop" ref="myDialog" :visible="viewShow">
      <el-form :model="viewData" label-width="60px">
        <el-form-item label="姓名" prop="name">
          <el-input :readonly="true" v-model="viewData.name"></el-input>
        </el-form-item>
        ...
        <el-form-item label="備註">
          <el-input :readonly="true" type="textarea" v-model="viewData.desc"></el-input>
        </el-form-item>
      </el-form>
    </el-dialog>
  </div>
</template>

<script>
  export default {
    props: ['viewShow', 'viewData'],
    methods: {
      closePop: function () {
        // 需手動關閉彈窗,找到父組件中調用的地方進行事件的觸發
        this.$parent.$refs.contactView.$emit('update:viewShow', false)
      }
    }

  }
</script>

這裏有個比較值得注意的點,就是關閉查看彈窗,彈窗的開啓關閉狀態經過list也就是父級中的viewShow來控制,viewShow經過view也就是子級中的props流入到子級中,可是vue中的數據流向是默認是單向的,想要子級中修改父級屬性必須使用emit,詳見上面代碼。

這裏原先使用elementUI的dialog組件的本身的關閉,會報錯,只能本身修改了。

ps: 爲何這裏不用vuex處理父子組件的通訊?由於若是是一個大型的後臺管理系統,像這樣的狀況會常常發生,若是都放在vuex中管理,那vuex的體積會很是龐大,反而不利於維護。

 

8. 聯繫人編輯(新增)頁 --- edit.vue

<template>
  <div class="contact-edit">
    <el-form ref="contactForm" :model="form" :rules="rules" label-width="80px">
      <el-form-item label="姓名" prop="name">
        <el-input v-model="form.name"></el-input>
      </el-form-item>
      <el-form-item label="性別">
        <el-select v-model="form.sex" placeholder="請選擇性別">
          <el-option label="男" value="male"></el-option>
          <el-option label="女" value="female"></el-option>
        </el-select>
      </el-form-item>
    ...
      <el-form-item label="備註">
        <el-input type="textarea" v-model="form.desc"></el-input>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="onSubmit('contactForm')">{{ btnName }}</el-button>
        <el-button @click="cancelForm">取消</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
  export default {
    data () {
      var nameValid = (rule, value, callback) => {
        if (!value) {
          callback(new Error('姓名不能爲空'))
        } else {
          callback()
        }
      }
      var mobileValid = (rule, value, callback) => {
        let phonePattern = /(^\s*$)|(^[1][3,4,5,7,8][0-9]{9}$)/
        if (value && !phonePattern.test(value)) {
          callback(new Error('手機號格式不正確'))
        } else {
          callback()
        }
      }
      return {
        type: '', // 控制是不是新建
        ...
        rules: {
          name: [{validator: nameValid, trigger: 'blur'}],
          mobile: [
//            {required: true, message: '手機號不能爲空', trigger: 'blur'},
            {validator: mobileValid, trigger: 'blur'}
          ]
        }
      }
    },
    // 組件加載後的鉤子
    mounted: function () {
      this.checkPageStatus(this.$route.query.id)
    },
    // 路由在組件中的鉤子
    beforeRouteUpdate: function (to, from, next) {
      this.checkPageStatus(to.query.id)
      next()
    },
    methods: {
      // 檢查頁面是新建仍是編輯
      checkPageStatus: function (id) { ... },
      cancelForm: function () {
        this.$router.push('/contact')
      },
      onSubmit: function (formName) { ... }
    }
  }
</script>

能夠看到mounted與beforeRouteUpdate中的代碼有些重合,那是由於vue在路由僅僅只是參數變換的時候,是不會從新從新加載組件的,因此須要在beforeRouteUpdate中處理初始的數據。

nameValid與mobileValid爲表單驗證的函數,el-form配置rules屬性名稱,而後data中相應的添加rules便可開啓表單驗證,可是有一點必定要注意el-form-item上必定要設置對應的prop屬性,rules纔會生效。

 

9. 總結

很是簡單的一個項目,可是有幾個點必定要關注好:

模塊的劃分,模塊劃分要合理,儘可能能保證模塊的複用性

狀態的管理,必定要明確什麼東西要放vuex中,什麼東西不用放,以避免使項目的維護反而變得更復雜

若是是大型項目,路由中必定要讓.vue文件在須要時再引入,不然會加劇初次加載的負擔

爲了減小篇幅,刪減了不少不重要的代碼,須要查看源碼請移步,項目地址: https://github.com/junjunhuahua/vue-basic-demo

github上的項目已改成後臺提供接口,再也不使用localStorage操做數據,後臺項目使用MongoDB+node實現,具體項目:https://github.com/junjunhuahua/mongodb-demo

相關文章
相關標籤/搜索