vue + iview 項目實踐總結 【完】

一直想把一大篇的總結寫完、寫好,感受本身拖延太嚴重還總想寫完美,而後好多筆記都死在編輯器裏了,之後還按照一個小節一個小節的更新吧,小步快跑😂,先發出來,之後再迭代吧。javascript

最近咱們參與開發了一個(年前了)BI項目,前端使用vue全家桶,項目功能基本開發完成,剩下的修修補補,開發過程還算順暢,期間遇到好多問題,也記錄了一下,發出來一塊兒交流,主要是思路,怎麼利用vue給的API實現功能,避免你們在一樣的坑裏待太長時間,若是有更好實現思路能夠一塊兒交流討論😎🤗。css

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

前端其實不太複雜,可是隻要用vue開發基本上都會遇到的幾個問題,好比菜單組件多級嵌套、刷新後選中當前項、前端

涉及幾個點,表格表頭表體合併、文件上傳、富文本編輯器、權限樹等等。vue

項目介紹

系統的主要功能就是面向各個部門查看報表數據,後端同窗們很厲害,能彙總到一個集團的全部數據,各類炫酷大數據技術;java

菜單功能:node

  • 數據看板: 篩選、展現日期和表格分頁
  • 業務報表: 報表類型,日期篩選、表格分頁
  • 數據檢索: 篩選項聯動、表格分頁
  • 損耗地圖: 篩選項、關係圖插件
  • 展開分析: 篩選項、分類、卡片、表格
  • 系統信息: 版本發佈、步驟條、富文本編輯
  • 數據源上傳: 手動上傳、表格展現
  • 權限管理: 用戶管理、角色管理(權限菜單配置)

項目預覽圖:jquery

對勾爲已更新。ios

  • 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,咱們只須要在最初建立的時候獲取一次就夠了。ajax

以下gif例子,點擊上方TAB後從新刷新折線組件:

<!--模板-->
<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

效果圖:

大體思路就是先建立一個子組件,而後再建立一個父組件,循環引用,拿左側菜單說明,代碼以下,數據結構也在父組件中。

<!--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中獲取一遍菜單數據,而後到菜單組件又獲取一次數據,請求兩次接口。

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監聽路由參數從新獲取數據」中提到過,還有一部分功能的接口數據返回的特別慢,會出現切換菜單後,數據才加載出來,須要增長切換菜單後取消原來的請求,代碼註釋中 setp一、二、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組件,純數據展現,支持表頭和標題的跨行跨列。

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爲二、copSpan爲2,在cellMerge方法中,若是座標爲A的單元格,return ropSpan和copSpan,若是座標爲B、C、D則要return [0,0]隱藏,不然會出現表格錯亂

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組件緩存

產品的需求是從列表頁面點擊查看按鈕進入詳情頁面,詳情頁面再點擊返回,列表頁面要不能刷新,就須要把組件緩存起來。

組件緩存直接加 keep-live就能夠了,比較麻煩的是咱們在這個組件裏判斷三種狀況,1.第一次進入 2.從其餘欄目進入 3.從詳情頁進入,若是從爲一、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. 不要在子組件中操做父組件的數據

確實能夠在子組件中修改父組件的數據,但強烈建議不要在子組件中操做父組件數據,期間我接手過一個功能,梳理了半天邏輯,沒找到觸發點在哪裏,原來是在子組件中操做了父組件的數據,不利於維護,我本身起了個名字,讓數據保持單向流動,不知道是否是能夠定義爲單項數據了原則😂。

在開發的過程當中咱們發現,每一個人寫的業務組件代碼風格都不一致,怎樣是一致,關於業務組件,有沒有好的規範或者原則呢?還但願你們給點資料和建議很是感謝。

相關文章
相關標籤/搜索