vue + iview 專案實踐總結 【完】

愚坤發表於2019-03-08

一直想把一大篇的總結寫完、寫好,感覺自己拖延太嚴重還總想寫完美,然後好多筆記都死在編輯器裡了,以後還按照一個小節一個小節的更新吧,小步快跑?,先發出來,以後再迭代吧。

最近我們參與開發了一個(年前了)BI專案,前端使用vue全家桶,專案功能基本開發完成,剩下的修修補補,開發過程還算順暢,期間遇到好多問題,也記錄了一下,發出來一起交流,主要是思路,怎麼利用vue給的API實現功能,避免大家在同樣的坑裡待太長時間,如果有更好實現思路可以一起交流討論??。

前後端分離形式開發,vue+vueRouter+vueX+iviewUI+elementUI,大部分功能我們都用的iviewUI,有部分元件我們用了elementUI,比如表格、日曆外掛,我們沒接mock工具,介面用文件的形式交流,團隊氛圍比較和諧,三個PHP三個前端,效率還可以,兩個前端夥伴比較厲害,第一次使用vue,就承擔了90%的開發工作任務,我沒到上線就跑回家休陪產假了,特別感謝同事們的支援,我才能回家看娃。

前端其實不太複雜,但是隻要用vue開發基本上都會遇到的幾個問題,比如選單元件多級巢狀、重新整理後選中當前項、

涉及幾個點,表格表頭表體合併、檔案上傳、富文字編輯器、許可權樹等等。

專案介紹

系統的主要功能就是面向各個部門檢視報表資料,後端同學們很厲害,能彙總到一個集團的所有資料,各種炫酷大資料技術;

選單功能:

  • 資料看板: 篩選、展示日期和表格分頁
  • 業務報表: 報表型別,日期篩選、表格分頁
  • 資料檢索: 篩選項聯動、表格分頁
  • 損耗地圖: 篩選項、關係圖外掛
  • 展開分析: 篩選項、分類、卡片、表格
  • 系統資訊: 版本釋出、步驟條、富文字編輯
  • 資料來源上傳: 手動上傳、表格展示
  • 許可權管理: 使用者管理、角色管理(許可權選單配置)

專案預覽圖:

vue + iview 專案實踐總結 【完】

vue + iview 專案實踐總結 【完】

vue + iview 專案實踐總結 【完】

對勾為已更新。

  • 1. 使用v-if解決非同步傳參
  • 2. 使用$refs呼叫子元件方法
  • 3. 元件遞迴實現多級選單
  • 4. 使用watch監聽路由引數重新獲取資料
  • 5. 頁面重新整理後Menu根據地址選中當前選單項
  • 6. 使用Axios統一狀態碼判斷、統一增加token欄位
  • 7. 點選左側選單選中項點選重新整理頁面
  • 8. 使用Axios.CancelToken切換路由取消請求
  • 9. 使用element的table元件實現 表頭表體合併
  • 10. iview的Menu元件+vuex實現麵包屑導航
  • 11. iview上傳元件手動上傳與富文字編輯器接入
  • 12. 使用cheerio獲取表格資料
  • 13. keep-live元件快取
  • 14. 讓資料保持單向流動(不要在子元件中操作父元件的資料)

1. 使用v-if解決非同步傳參元件重繪

大部分的互動的流程都是 “ajax請求資料=>傳入元件渲染”,很多屬性需要非同步傳入子元件然後進行相關的計算,如果繫結很多computed或者watch,效能開銷會很大,而且有些場景並不需要使用computed和watch,我們只需要在最初建立的時候獲取一次就夠了。

如下gif例子,點選上方TAB後重新重新整理折線元件:

vue + iview 專案實踐總結 【完】

<!--模板-->
<mapBox v-if="mapData" :data="mapData"></mapBox>
複製程式碼
<!--點選搜尋後執行-->
let This = this
// setp1 重點
this.mapData = false

this.$http
.post('/api/show/mapcondition',{key:key,type:type})
.then(function(response){
// setp2 重點
    this.mapData = response.data
})
複製程式碼

有時候會出現DOM元素與資料不同步,可以使用使用其他方式讓DOM強刷

- setTimeou
- $forceUpdate()
- $nextTick()
- $set()
複製程式碼

2. 使用$refs呼叫子元件方法

有時候會涉及到父元件呼叫子元件方法的情況,例如,iview的Tree元件暴露出來的getCheckedAndIndeterminateNodes方法,詳見官網文件link

<!--模板-->
<Tree v-if="menu" :data="menu" show-checkbox multiple ref="Tree"></Tree>
複製程式碼
let rules = this.$refs.Tree.getCheckedAndIndeterminateNodes();
複製程式碼

3. 元件遞迴實現多級選單

遞迴元件用的很多,我們的左側選單還有無限拆分的表格合併,都用到了遞迴元件,詳見官網連結link

效果圖:

vue + iview 專案實踐總結 【完】

大致思路就是先建立一個子元件,然後再建立一個父元件,迴圈引用,拿左側選單說明,程式碼如下,資料結構也在父元件中。

<!--index.vue  父元件 資料介面在default中-->
<template>
    <Menu width="auto"
        theme="dark"
        :active-name="activeName"
        :open-names="openNames"
        @on-select="handleSelect"
        :accordion="true"
    >

      <template v-for="(item,index) in items">
        <side-menu-item
            v-if="item.children&&item.children.length!==0"
            :parent-item="item"
            :name="index+''"
            :index="index"
        >
        </side-menu-item>
        <menu-item v-else
            :name="index+''"
            :to="item.path"
        >
            <Icon :type="item.icon" :size="15"/>
            <span>{{ item.title }}</span>
        </menu-item>
      </template>
    </Menu>
</template>

<script>
import sideMenuItem from '@/components/Menu/side-menu-item.vue'
export default {
    name: 'sideMenu',
    props: {
        activeName: {
            type: String,
            default: 'auth'
        },
        openNames: {
            type: Array,
            default: () => [
                'other',
                'role',
                'auth'
            ]
        },
        items: {
            type: Array,
            default: () => [
                {
                    name : 'system',
                    title : '資料看板',
                    icon : 'ios-analytics',
                    children: [
                        { name : 'user', title : '使用者管理', icon : 'outlet',
                          children : [
                                { name : 'auth', title : '許可權管理1', icon : 'outlet' },
                                { name : 'auth', title : '許可權管理', icon : 'outlet',
                                  children:[
                                    { name : '334', title : '子選單', icon : 'outlet' },
                                    { name : '453', title : '子選單', icon : 'outlet' }
                                  ]
                                }
                            ]
                         }
                    ]
                },
                {
                    name : 'other',
                    title: '其他管理',
                    icon : 'outlet',
                }
            ]
        }
    },
    components: {
        sideMenuItem
    },
    methods: {
        handleSelect(name) {
           this.$emit('on-select', name)
        }
    }
}
</script>
複製程式碼
<!--side-menu-item.vue  子元件-->
<template>
    <Submenu :name="index+''">
        <template slot="title" >
            <Icon :type="parentItem.icon" :size="10"/>
            <span>{{ parentItem.title }}</span>
        </template>
        <template v-for="(item,i) in parentItem.children">
            <side-menu-item
                v-if="item.children&&item.children.length!==0"
                :parent-item="item"
                :to="item.path"
                :name="index+'-'+i"
                :index="index+'-'+i"
            >
            </side-menu-item>
            <menu-item v-else
                :name="index+'-'+i"  :to="item.path">
                <Icon :type="item.icon" :size="15" />
                <span>{{ item.title }}</span>
            </menu-item>
        </template>
    </Submenu>
</template>

<script>
export default {
    name: 'sideMenuItem',
    props: {
        parentItem: {
            type: Object,
              default: () => {}
        },
        index:{}
    },
    created:function(){
    }
}
</script>
複製程式碼

4. 使用watch監聽路由引數重新獲取資料

很多選單項都只是入參不一樣,是不會重新走業務邏輯的,我們就用watch監聽$router,如果改變就重新請求新的資料。

export default {
    watch: {
    '$route':'isChange'
    },
    methods:{
        getData(){
            // Do something
        },
        isChange(){
            this.getData()
        },
    }
}
複製程式碼

5. 重新整理:根據地址選中當前選單項

頁面重新整理後左側選單的預設選中項就和頁面對應不上了,我們用$router的beforeEnter方法做判斷,根據地址獲得路由的key(每一個路由都有一個key的引數),儲存到localStorage中,然後選單元件再從localStorage中取出key,再遍歷匹配到當前選專案,比較冗餘的是我們要在beforeEnter中獲取一遍選單資料,然後到選單元件又獲取一次資料,請求兩次介面。

vue + iview 專案實踐總結 【完】

step1 router.js中設定beforeEnter方法,獲得位址列中的key 儲存到localStorage

step2 選單元件取出localStorage中key,遞迴匹配
複製程式碼

6. Axios統一狀態碼判斷、統一增加token欄位

Axios的interceptors方法有request和response兩個方法對請求的入參和返回結果做統一的處理。

<!--request 除登入請求外,其他均增加token欄位 -->
axios.interceptors.request.use(function (config) {
  let token = localStorage.getItem('token')
  if(token== null && router.currentRoute.path == '/login'){// 本地無token,未登入 跳轉至登入頁面
    router.push('/login')
  }else{
    if(config.data==undefined){
      config.data = {
        "token":token
      }
    }else{
      Object.assign(config.data,{"token":token})
    }
  }
  return config
}, function (error) {
  iView.Message.error('請求失敗')
  return Promise.reject(error)
})

<!--response 返回狀態統一處理 -->
axios.interceptors.response.use(function (response) {
  if(response.hasOwnProperty("data") && typeof response.data == "object"){
      if(response.data.code === 998){// 登入超時 跳轉至登入頁面
          iView.Message.error(response.data.msg)
          router.push('/login')
          return Promise.reject(response)
      }else if (response.data.code === 1000) {// 成功
        return Promise.resolve(response)
      } else if (response.data.code === 1060){ //資料定製中
         return Promise.resolve(response)
      }else {// 失敗
        iView.Message.error(response.data.msg)
        return Promise.reject(response)
      }
  } else {
    return Promise.resolve(response)
  }

}, function (error) {
  iView.Message.error('請求失敗')
  // 請求錯誤時做些事
  return Promise.reject(error)
})
複製程式碼

7. 點選左側選單選中項點選重新整理頁面

測試同學提出bug,左側選單選中後,再次點選選中項沒有重新整理,使用者體驗不好,產品同學一致通過,我們就用野路子來解決了。 給選單元件設定on-select事件,點選後儲存當前選中項的path,每次執行當前點選的path和儲存的path做對比,如果一致,跳轉到空白頁,空白頁再返回到當前頁,實現假重新整理,注:不知道是router.push有節流控制還是怎麼回事,不加setTimeout不管用。

<!--選單的handleSelect事件-->
handleSelect(name) {
    let This = this
    if((this.selectIndex == 'reset') || (name == this.selectIndex)){
        // 點選再次重新整理
        setTimeout(function function_name(argument) {
          This.$router.push({
              path: '/Main/about',
              query: {
                t: Date.now()
              }
            })
        },1)
    }
    this.selectIndex = name
    this.$emit('on-select', name)
},
複製程式碼
<!--空白頁-->
created(){
    let This = this
    setTimeout(function function_name(argument) {
      This.$router.go(-1);
    },1)
}
複製程式碼

8. 使用Axios.CancelToken切換路由取消請求

有一部分情況是切換路由時,只改變引數,在“4. 使用watch監聽路由引數重新獲取資料”中提到過,還有一部分功能的介面資料返回的特別慢,會出現切換選單後,資料才載入出來,需要增加切換選單後取消原來的請求,程式碼註釋中 setp1、2、3為順序

export default {
  data(){
    return {
      // setp1 建立data公共的source變數 
      source:''                
    }
  },
  created:function(){
    // 獲取搜尋資料
    this.getData()
  },
  watch:{
    '$route':'watchGetSearchData',
  },
  methods:{
    getData(){
      // setp2 請求時建立source例項 
      let CancelToken = this.$http.CancelToken
      this.source = CancelToken.source();
    },
    watchGetSearchData(){
      // setp3 切換路由時取消source例項 
      this.source.cancel('0000')
      this.getData()
      this.$http
        .post('/api/show/map',data,{cancelToken:this.source.token})
        .then(function(response){
            
        })
    }
  }
}
複製程式碼

9. element的table元件實現 表頭表體合併

我們專案用到的的元件表格有兩種,一種用iview的table,帶操作按鈕的表格,支援表頭跨行跨列,另一種element的table元件,純資料展示,支援表頭和標題的跨行跨列。

vue + iview 專案實踐總結 【完】

vue + iview 專案實踐總結 【完】

vue + iview 專案實踐總結 【完】

element的table元件支援表頭標題合併,我們定義資料結構包含三部分,表頭、表體、表體合併項。 表頭直接使用遞迴元件巢狀就可以了,表體資料直接扔給table元件,合併通過cellMerge方法遍歷合併項資料遍歷合併,程式碼如下。

資料結構

data:{
    historyColumns:[  // 表頭資料
        {
            "title": " ",
            "key": "column"
        },
        {
            "title": "指標",
            "key": "target"
        },
        {
            "title": "11/22",
            "key": "11/22"
        },
        {
            "title": "日環比",
            "key": "日環比"
        },
        {
            "title": "當週值",
            "key": "當週值"
        },
        {
            "title": "上週同期",
            "key": "上週同期"
        },
        {
            "title": "周環比",
            "key": "周環比"
        },
        {
            "title": "近7日累計",
            "key": "近7日累計"
        },
        {
            "title": "當月累計",
            "key": "當月累計"
        }
    ],
    histories:[  // 表體資料
        {
            "target": "在售量",
            "11/22": 912,
            "日環比": "-",
            "當週值": 912,
            "上週同期": 0,
            "周環比": "100%",
            "近7日累計": 912,
            "當月累計": 912,
            "column": "基礎指標"
        },
        {
            "target": "-在售外庫車量",
            "11/22": 29,
            "日環比": "-",
            "當週值": 29,
            "上週同期": 0,
            "周環比": "100%",
            "近7日累計": 29,
            "當月累計": 29,
            "column": "基礎指標"
        }
    ],
    merge:[  // 表體合併項
        {
            "rowNum": 0,
            "colNum": 0,
            "ropSpan": 1,
            "copSpan": 4
        },
        {
            "rowNum": 4,
            "colNum": 0,
            "ropSpan": 1,
            "copSpan": 27
        }
    ]
}
複製程式碼

表體合併說明: 表格有cellMerge方法,每一td在渲染時都會執行這個方法,在cellMerge裡遍歷merge資料,根據cellMerge的入參行、列定位到td,如果是要合併的表格,則return出要合併的行數和列數,如果在合併的範圍內,則要return [0,0],隱藏當前td。

比如要把A、B、C、D,merge的資料rowNum為A的行、colNum為A的列、ropSpan為2、copSpan為2,在cellMerge方法中,如果座標為A的單元格,return ropSpan和copSpan,如果座標為B、C、D則要return [0,0]隱藏,否則會出現表格錯亂

vue + iview 專案實踐總結 【完】
merge方法程式碼:

// 表格合併主方法  row:行陣列  column:列資料  rowIndex、columnIndex行列索引
cellMerge({ row, column, rowIndex, columnIndex }) {

  let This = this;
  if(This.configJson){
      for(let i = 0; i < This.configJson.length; i++){

      let rowNum = This.configJson[i].rowNum   // 行
      let colNum = This.configJson[i].colNum   // 列

      let ropSpan = This.configJson[i].ropSpan // 跨列數
      let copSpan = This.configJson[i].copSpan // 跨行數

      if(rowIndex == rowNum && columnIndex == colNum ){// 當前表格index 合併項
        return [copSpan,ropSpan]
      // 隱藏範圍內容的單元格
      // 行範圍 rowNum <= rowIndex && rowIndex < (rowNum+copSpan)
      // 列範圍 colNum <= columnIndex && columnIndex < (colNum+ropSpan)
      }else if( rowNum <= rowIndex && rowIndex < (rowNum+copSpan) && colNum <= columnIndex && columnIndex < (colNum+ropSpan) ){

        return [0,0]
      }

    }
  }

}

複製程式碼

**表頭合併說明:**element和iview的表頭合併資料格式可以一樣,都是遞迴形式,區別是iview的table元件直接把資料扔給元件就可以了,而element需要自己封裝一下表頭。

// 子元件
<template>
  <el-table-column :prop="thList.key" :label="thList.title" align="center">

    <template v-for="(item,i) in thList.children" >

        <tableItem  v-if="item.children&&item.children.length!==0"
        :thList="item" /></tableItem>

        <el-table-column align="center" v-else
              :prop="item.key"
              :label="item.title"
              :formatter="toThousands"
            >
            
        </el-table-column>
      </template>
  </el-table-column>
</template>
<script>
export default {
    name: 'tableItem',
    props: {
        thList: {
            type: Object,
              default: () => {}
        },
    },
}
</script>
複製程式碼

封裝後的table元件:

<template>
  <div>
    <el-table :data="Tbody" :stripe="stripe" :border="true" :span-method="cellMerge" align="center" :header-cell-style="tableHeaderColor"  height="600" >
      <template v-for="(item,i) in Thead">
          <template v-if="item.children&&item.children.length!==0" >

            <tableItem :thList="item" />

          </template>

          <template v-else >

            <el-table-column align="center"
              :prop="item.key"
              :label="item.title"
              :formatter="toThousands"
            >
            </el-table-column>

          </template>
        </template>
      </el-table>
  </div>
</template>
<script>
import tableItem from '@/components/table/tableHeader/table-Item.vue'
export default {
    name: 'table-header',
    props: {
        Thead: {
            type: Array,
            default: () => {}
        },
        Tbody:{
          type: Array,
          default: () => {}
        },
        stripe:{
          type:Boolean,
          default:false
        },
        cellMerge:Function,
          default:()=>{}
    },
    created:function(){
    },
    components:{
      tableItem
    },
    methods:{
      tableHeaderColor({ row, column, rowIndex, columnIndex }) {
        if (rowIndex === 0) {
          return 'background-color: #f8f8f9;'
        }
      }
    }
}
</script>
複製程式碼

其他頁面複用table

<!--引入-->
import TableList from '@/components/table/tableHeader/index.vue'
<!--呼叫-->
<TableList :Thead="historyColumns" :Tbody="historyData" :cellMerge="cellMerge" />
複製程式碼

10. iview的Menu元件+vuex實現麵包屑導航

iview的Menu元件有on-select方法,可以獲得當選選中項的name,我們的name按照資料索引來遍歷的,比如三級選單,選中後會返回2-0-1這樣的字串,表示樹選單第3個選單下的第1個子選單下的第2個選單項,通過這個字串再篩選出陣列['業務報表','B2C報表','成交明細']對應選單的title,然後發給vuex的Store.state,然後麵包屑元件通過計算資料屬性監聽Store.state拿屬性展示就可以了。

<!-- 根據字串篩出title陣列 發給$store -->
toBreadcrumb(arrIndex){

      let This = this;
      let mapIndex = arrIndex.split('-');
      // 獲取對應name
      let box={};

      let mapText = mapIndex.map(function(item,index){
          if(index == 0){
            box = This.MenuData[eval(item)];
          }else{
            box = box.children[eval(item)];
          }

          return box.title;
      });

      this.$store.commit('toBreadcrumb',mapText)
    }
複製程式碼

vueX程式碼

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
  state: {
    Breadcrumb:[],  // 麵包屑導航
    userName: '',
    readyData:""
  },
  mutations: {
    toBreadcrumb(state,arr){
      state.Breadcrumb = arr;
    }
  },
  getters: {
    getBreadcrumb: state => {
      return state.Breadcrumb
    }
  }
})
複製程式碼

麵包屑元件

<template>
  <Header style="background: #fff;">
  	<Row>
        <Col span="12">
          <!-- {{doneTodosCount}} -->
        	<Breadcrumb>
		        <BreadcrumbItem v-for="item in doneTodosCount">{{item}}</BreadcrumbItem>
		    </Breadcrumb>
        </Col>
        <Col span="12">
        	<Login />
        </Col>
    </Row>
  </Header>
</template>
<script>
import Login from '@/components/Login'
export default {
  data(){
    return {
    }
  },
  created:function(){
    this.$store.commit('toBreadcrumb',['首頁'])
  },
  computed: {
    doneTodosCount () {
      return this.$store.state.Breadcrumb
    }
  },
  components:{
    Login
  }
}
</script>
複製程式碼

11. iview上傳元件手動上傳,接入富文字編輯器

iview提供的元件特別豐富,我們在做圖片上傳的時候,需要手動上傳,需要呼叫子元件的file物件通過自己的post方法提交到服務端,actionDate為檔案資料,然後再通過on-success回撥反饋上傳成功或失敗。 手動上傳:

<Upload
     ref="upload"
    :data= "actionDate"
    :on-success="handleSuccess"
    :format="['png','jpg']"
    action="/api/upload/ccupload">
    <Button icon="ios-cloud-upload-outline">點選上傳檔案</Button>
</Upload>
 <div v-if="file !== null">
    上傳檔案: {{ file.name }} 
    <Button type="text" @click="upload" :loading="loadingStatus">{{ loadingStatus ? 'Uploading' : '上傳' }}</Button>
</div>
複製程式碼
// upload 方法
let uploadFile = this.$refs.upload.file
this.$refs.upload.data = this.actionDate;
this.$refs.upload.post(uploadFile);
this.loadingStatus = true;

// handleSuccess 方法
this.loadingStatus = false;
if(res.code == 1000){
    this.$Message.success('上傳成功')
}else{
    this.$Message.error('上傳失敗')
}
複製程式碼

我們在聯調的過程中後端說接收不到檔案,我們只能用node來驗證一下是不是元件有問題,於是用express寫了一下檔案上傳。

var express = require('express');
var router = express.Router();
let fs = require('fs')
var formidable = require('formidable');//表單控制元件
var path = require('path');
var app = express();
app.use(express.static('/public/'));

router.post('/test',(req,res)=>{
	var imgPath = path.dirname(__dirname) + '/public';
	var form = new formidable.IncomingForm();
	form.encoding = 'utf-8'; //設定編輯 
	form.uploadDir = imgPath; //設定上傳目錄
	form.keepExtensions = true; //保留字尾
	form.maxFieldsSize = 2 * 1024 * 1024; //檔案大小
	form.type = true;

	form.parse(req, function(err, fields, files){
		let src = files.img.path.split('/');
		let urlString = src[src.length-1]
	  	if (err) {
	      console.log(err);
	      req.flash('error','圖片上傳失敗');
	      return;
	  }
	  res.json({
	      code: '200',
	      type:'single',
	      url:'http://10.70.74.167:3000/'+urlString
	  })
	});
});

module.exports = router;
複製程式碼

我們在測試的時候增加了一個圖片test的轉發配置,然後把元件的action地址替換一下為/test/就可以了,親測無問題[陰險臉]。 vue.config.js

module.exports = {
  baseUrl: baseUrl,
  devServer: {
    proxy: {
        '/api': { // 開發伺服器
            target: ' http://*******',
            changeOrigin: true,
        },
        '/test': { // 圖片上傳測試
            target: ' http://10.70.74.167:3000',
            changeOrigin: true,
        }
    }
  },
  productionSourceMap: false,
}
複製程式碼

富文字編輯器的圖片上傳有兩種模式,一種是把圖片轉成base64,通過一個介面把html內容提交給服務端,另一種模式是兩個介面,分別把圖片上傳到伺服器,然後返回url字串到編輯器中,再把編輯器中的html儲存到伺服器上,我們用的編輯器是vue-quill-editor,使用第二種模式,藉助element的el-upload元件自動上傳圖片,然後返回地址插入到編輯器。

import {format} from '@/lib/js/utils.js'
	import {quillEditor} from 'vue-quill-editor'
	const toolbarOptions = [
	    ['bold', 'italic', 'underline', 'strike'],        // toggled buttons
	    [{'header': 1}, {'header': 2}],               // custom button values
	    [{'list': 'ordered'}, {'list': 'bullet'}],
	    [{'indent': '-1'}, {'indent': '+1'}],          // outdent/indent
	    [{'direction': 'rtl'}],                         // text direction
	    [{'size': ['small', false, 'large', 'huge']}],  // custom dropdown
	    [{'header': [1, 2, 3, 4, 5, 6, false]}],
	    [{'color': []}, {'background': []}],          // dropdown with defaults from theme
	    [{'font': []}],
	    [{'align': []}],
	    ['link', 'image'],
	    ['clean']
	  ]
    export default {
        data () {
            return {
            	options2:{},
            	quillUpdateImg: false, // 根據圖片上傳狀態來確定是否顯示loading動畫,剛開始是false,不顯示
            	content:'', // 富文字內容
            	title:'新建',
                editorOption:{
	            	placeholder: '',
	      			theme: 'snow',  // or 'bubble'
	      			modules:{
			            toolbar: {
			              container: toolbarOptions,
			              handlers: {
			                'image': function (value) {
			                  if (value) {
			                    // 觸發input框選擇圖片檔案
			                    document.querySelector('.avatar-uploader input').click()
			                  } else {
			                    this.quill.format('image', false);
			                  }
			                }
			              }
			            }
			        }
                },
		        serverUrl: '/api/add/upload?key='+this.$route.params.key,  // 這裡寫你要上傳的圖片伺服器地址
		        header: {
		          // token: sessionStorage.token
		        },
                current: 0,
                formValidate: {
                    device_name: '集團BI',
                    versions: '',
                    publish_time: '',
                    desc: '',
                },
                ruleValidate: {
                    device_name: [
                        { required: true, message: '請選擇系統名稱', trigger: 'change' }
                    ],
                    versions: [
                        { required: true, message: '請輸入版本資訊', trigger: 'blur' }
                    ],
                    publish_time: [
                        { required: true, type: 'date', message: '請選擇發版時間', trigger: 'change' }
                    ],
                    desc: [
                        { required: true, message: '請輸入對於該版本的總體描述', trigger: 'blur' },
                        { type: 'string', min: 20, message: '版本的總體描述不少於20個字', trigger: 'blur' }
                    ]
                },
                isFirst: true,
                isSecond: false,
                isThird: false,
                versionid:''
            }
        },
        created:function(){
        	this.limit();
        	this.initialization();
       	},
        methods: {
        	limit(){
                this.options2 =  {
                  disabledDate (date) {
                    return (date && date.valueOf() > new Date().getTime()) || (date && date.valueOf() < new Date("2017-12-31"))
                  }
                }
            },
        	//初始判定是新增/修改
        	initialization(){
        		let id = this.$route.params.id;
        		if(id !=0){
        			this.title = "編輯";
        			let obj = {};
        			obj.version_id = this.$route.params.id;
        			obj.key = this.$route.params.key;
        			this.$http
			            .post('/api/show/version',obj).then(response => (
			               this.formValidate.device_name = response.data.data.device_name,
			               this.formValidate.versions = response.data.data.versions,
			               this.formValidate.publish_time = response.data.data.publish_time,
			               this.formValidate.desc = response.data.data.desc,
			               this.content = response.data.data.pc_html
			        ))

        		}else{
        			this.title = "新建";
        		}
        	},
        	//第一步基本資訊(釋出)
        	firstSubmit(name){
        		this.$refs[name].validate((valid) => {
                    if (valid) {
                        this.$Message.success('資訊新增成功');
                        this.current += 1;
                        this.isFirst = !this.isFirst;
                        this.isSecond = !this.isSecond;
                    }else{
                        this.$Message.error('請完善必填資訊');
                    }
                })
        	},
        	//第二步的表單資料提交(釋出)
        	save(){
        		let id = this.$route.params.id;
        		let addObj = this.formValidate;
        		addObj.publish_time = format(this.formValidate.publish_time);
        		addObj.pc_html = this.content;
        		addObj.key = this.$route.params.key;
        		if(this.$route.params.id != 0){
        			addObj.version_id = id;
        		}
        		this.$http
		            .post('/api/add/version',addObj).then(response => (
		               this.secondSubmit(response.data.version_id)
		        ))
        	},
        	//第二步提交成功後轉至第三步(釋出)
        	secondSubmit(id){
        		this.current += 1;
                this.isSecond = false;
                this.isThird = !this.isThird;
                this.versionid = id;
        	},
        	//第三步跳轉至[預覽]
        	preview(){
        		this.$router.push({ path:"/Main/VersionManagementInfo/system_versions/"+this.versionid});
        	},
        	//第三步釋出
        	release(){
        		let status = this.$route.params.status;
        		if(status != 2){
        			let obj = {};
        			if(this.$route.params.id == 0){
        				obj.version_id = this.versionid;
        			}else{
        				obj.version_id = this.$route.params.id;
        			}
	        		obj.key = this.$route.params.key;
	        		this.$http
			            .post('/api/edit/publish/version',obj).then(response => (
			               this.releaseLink()
			        ))
        		}else{
        			this.releaseLink()
        		}

        	},
        	//第三步釋出跳轉
        	releaseLink(){
        		this.$router.push({ path:"/Main/VersionManagement/system_versions"});
        	},
        	//上一步操作
            returns () {
                if (this.current != 0) {
                    this.current -= 1;
                    this.isFirst = true;
                    this.isSecond = false;
                }
            },
            //富文字內容改變事件
            onEditorChange({editor, html, text}) {
		        this.content = html
		     },
		    //富文字圖片上傳前
		    beforeUpload() {
		        // 顯示loading動畫
		        this.quillUpdateImg = true
		    },
		    //富文字圖片上傳成功
		    uploadSuccess(res, file) {
		        // res為圖片伺服器返回的資料
		        // 獲取富文字元件例項
		        console.log(res,file);
		        let quill = this.$refs.myQuillEditor.quill
		        // 如果上傳成功
		        if (res.code == 1000 ) {
		          // 獲取游標所在位置
		          let length = quill.getSelection().index;
		          // 插入圖片  res.url為伺服器返回的圖片地址
		          quill.insertEmbed(length, 'image', res.data)
		          // 調整游標到最後
		          quill.setSelection(length + 1)
		        } else {
		          this.$message.error('圖片插入失敗')
		        }
		        // loading動畫消失
		        this.quillUpdateImg = false
	      	},
		    // 富文字圖片上傳失敗
		    uploadError() {
		        // loading動畫消失
		        this.quillUpdateImg = false
		        this.$message.error('圖片插入失敗')
		    },
		 
        }
    }

複製程式碼

12. 使用cheerio展示字串表格

有一部分表格資料比較難處理,是後端直接把xlsx檔案轉成字串發給前端,cheerio可以把字串轉為類似jquery物件的虛擬DOM,然後用jquery的api操作這個虛擬DOM。

import cheerio from "cheerio"
this.$http.post('/api/list/statement-table',p).then(function(response){
               if(response.data==""){
                  This.isShow=false;This.content=true;This.title=false//無資料時資料載入中和標題資料的盒子隱藏
                  This.message="<div style='text-align:center'>暫無資料</div>"
               }else{
                    //console.log(response)
                    This.isShow=false;
                    This.content=true;//有資料時 資料載入中隱藏 標題和表體顯示
                    let $ = cheerio.load(response.data);
                    //刪除自帶的行內樣式
                    $("body style").remove();
                    $("body table").css({"border":"1px solid #e8eaec" });
                    $("body table td").css({"border":"1px solid #e8eaec","padding":"10px","color":"#515a6e"});
                    //全文匹配 剔除&amp;quot;
                    This.message = $("body").html().replace(/&amp;quot;/g,"");
               }
           })


複製程式碼

13. keep-live元件快取

產品的需求是從列表頁面點選檢視按鈕進入詳情頁面,詳情頁面再點選返回,列表頁面要不能重新整理,就需要把元件快取起來。

vue + iview 專案實踐總結 【完】
元件快取直接加keep-live就可以了,比較麻煩的是我們在這個元件裡判斷三種情況,1.第一次進入 2.從其他欄目進入 3.從詳情頁進入,如果從為1、2這兩種情況,我們需要重新整理頁面,如果是3,則不重新整理。

思路是: created鉤子中著增加isFirstEnter標識,beforeRouteEnter鉤子中判斷是否為詳情頁面返回,如果是則加上meta.isBack的標識,在activated鉤子裡判斷是第幾種情況,如果為1或2,則重新請求列表頁資料,如果是3就不用動管了。

router.js增加標識meta的keepAliveisBack

/******** 業務報表 Start ********/
{
    path: '/Main/BusinessReport/:key', // 業務報表-列表
    name: 'BusinessReport',
    meta: { keepAlive: true,isBack:false},
    component: () => import('./pages/BusinessReport/index.vue'),
},
{
    path: '/Main/BusinessReportInfo/:sn/:is_check/:key/:type/:cmd5/:time/:is_down', // 業務報表-詳情
    name: 'BusinessReportInfo',
    component: () => import('./pages/BusinessReportInfo/index.vue'),
},
/******** 業務報表 End ********/
複製程式碼

根據mate.keepAlive渲染不同的router-view(忘記為什麼是這麼寫的了,感覺很low)。

<keep-alive>
    <router-view v-if="$route.meta.keepAlive" ></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive" >
    <!-- 這裡是不被快取的檢視元件,比如 Edit! -->
</router-view>
複製程式碼

元件程式碼鉤子事件createdbeforeRouteEnteractivated方法

data(){
    return{
        isFirstEnter:false
    }
},
created:function(){
    this.isFirstEnter = true;
},
beforeRouteEnter(to, from, next) {
    if(from.name === 'BusinessReportInfo') { //判斷是從哪個路由過來的,若是BusinessReportInfo頁面不需要重新整理獲取新資料,直接用之前快取的資料即可
      to.meta.isBack = true
    }
    next();
},
activated() {
    if(!this.$route.meta.isBack || this.isFirstEnter) {
        this.data=""
        //如果isBack是false,表明需要獲取新資料,否則就不再請求,直接使用快取的資料
        this.getPath(); // ajax獲取資料方法
    }
    this.$route.meta.isBack = false;
    this.isFirstEnter=false;
    //恢復成預設的false,避免isBack一直是true,導致下次無法獲取資料
}

複製程式碼

14. 不要在子元件中操作父元件的資料

確實可以在子元件中修改父元件的資料,但強烈建議不要在子元件中操作父元件資料,期間我接手過一個功能,梳理了半天邏輯,沒找到觸發點在哪裡,原來是在子元件中操作了父元件的資料,不利於維護,我自己起了個名字,讓資料保持單向流動,不知道是不是可以定義為單項資料了原則?。

在開發的過程中我們發現,每個人寫的業務元件程式碼風格都不一致,怎樣是一致,關於業務元件,有沒有好的規範或者原則呢?還希望大家給點資料和建議非常感謝。

相關文章