vue + iview 專案實踐總結
一直想把一大篇的總結寫完、寫好,感覺自己拖延太嚴重還總想寫完美,然後好多筆記都死在編輯器裡了,以後還按照一個小節一個小節的更新吧,小步快跑:joy:,先發出來,以後再迭代吧。
最近我們參與開發了一個(年前了)BI專案,前端使用vue全家桶,專案功能基本開發完成,剩下的修修補補,開發過程還算順暢,期間遇到好多問題,也記錄了一下,發出來一起交流,主要是思路,怎麼利用vue給的API實現功能,避免大家在同樣的坑裡待太長時間,如果有更好實現思路可以一起交流討論:sunglasses: 。
前後端分離形式開發, vue+vueRouter+vueX+iviewUI+elementUI
,大部分功能我們都用的iviewUI,有部分元件我們用了elementUI,比如表格、日曆外掛,我們沒接mock工具,介面用文件的形式交流,團隊氛圍比較和諧,三個PHP三個前端,效率還可以,兩個前端夥伴比較厲害,第一次使用vue,就承擔了90%的開發工作任務,我沒到上線就跑回家休陪產假了,特別感謝同事們的支援,我才能回家看娃。
前端其實不太複雜,但是隻要用vue開發基本上都會遇到的幾個問題,比如選單元件多級巢狀、重新整理後選中當前項、
涉及幾個點,表格表頭表體合併、檔案上傳、富文字編輯器、許可權樹等等。
專案介紹
系統的主要功能就是面向各個部門檢視報表資料,後端同學們很厲害,能彙總到一個集團的所有資料,各種炫酷大資料技術;
選單功能:
- 資料看板: 篩選、展示日期和表格分頁
- 業務報表: 報表型別,日期篩選、表格分頁
- 資料檢索: 篩選項聯動、表格分頁
- 損耗地圖: 篩選項、關係圖外掛
- 展開分析: 篩選項、分類、卡片、表格
- 系統資訊: 版本釋出、步驟條、富文字編輯
- 資料來源上傳: 手動上傳、表格展示
- 許可權管理: 使用者管理、角色管理(許可權選單配置)
專案預覽圖:



對勾為已更新。
- 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. 先準備資料,再指向
this.data
- 15. 不要操作單向資料流
- 16. 禁止瀏覽器儲存密碼 回車登入
- 17. 水印效果
1. 使用v-if解決非同步傳參元件重繪
大部分的互動的流程都是 “ajax請求資料=>傳入元件渲染”,很多屬性需要非同步傳入子元件然後進行相關的計算,如果繫結很多computed或者watch,效能開銷會很大,而且有些場景並不需要使用computed和watch,我們只需要在最賤建立的時候獲取一次就夠了。
如下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監聽路由引數重新獲取資料”中提到過,還有一部分功能的介面資料返回的特別慢,會出現切換選單後,資料才加載出來,需要增加切換選單後取消原來的請求,程式碼註釋中 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元件,純資料展示,支援表頭和標題的跨行跨列。



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]隱藏,否則會出現表格錯亂 。

// 表格合併主方法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" > <tableItemv-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"}); //全文匹配 剔除" This.message = $("body").html().replace(/"/g,""); } }) 複製程式碼