前言
vue-manage-system,一個基於 Vue.js 和 element-ui 的後臺管理系統模板,從2016年年底第一個commit,到現在差不多兩年了,GitHub上也有了 5k star,也是這些讓我有了持續更新的動力,其中也踩了很多坑,在這總結一下。
github地址:vue-manage-system
線上地址:blog.gdfengshuo.com/example/wor…
自定義圖示
element-ui 自帶的字型圖示比較少,而且許多比較常見的都沒有,因此需要自己引入自己想要的字型圖示。最受歡迎的圖示庫 Font Awesome,足足有 675 個圖示,但也因此導致字型檔案比較大,而專案中又不需要用到這麼多圖示。那麼這時候,阿里圖示庫就是一個非常不錯的選擇。
首先在阿里圖示上建立一個專案,設定圖示字首,比如 el-icon-lx,設定Font Family,比如 lx-iconfont,新增需要用到的圖示到專案中,我這邊選擇 Font class 生成線上連結,因為所有頁面都需要用到圖示,就直接在 index.html 中引入該css連結就行了
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>vue-manage-system</title>
<!-- 這裡引入阿里圖示樣式 -->
<link rel="stylesheet" href="//at.alicdn.com/t/font_830376_qzecyukz0s.css">
</head>
<body>
<div id="app"></div>
</body>
</html>
複製程式碼
然後需要設定字首為 el-icon-lx 的圖示類名使用 lx-iconfont 字型。
[class*="el-icon-lx"], [class^=el-icon-lx] {
font-family: lx-iconfont!important;
}
複製程式碼
但是這個樣式要放在哪裡才可以呢?這可不是隨便放就行的。在 main.js 中,引入了 element-ui 的樣式,而樣式中有這樣的一段css:
[class*=" el-icon-"], [class^=el-icon-]{
font-family: element-icons!important;
speak: none;
font-style: normal;
font-weight: 400;
font-variant: normal;
text-transform: none;
line-height: 1;
vertical-align: baseline;
display: inline-block;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
複製程式碼
很明顯,如果這段 css 在我們自定義樣式後面才執行,就會覆蓋了我們的樣式,那自定義的圖示就顯示不了。而在 build 專案的時候,會把 APP.vue 中的的樣式打包進 app.css 中,然後再把 main.js 中引用到的樣式追加到後面。那麼我們可以把自定義樣式放到一個css檔案中,然後在 main.js 引入 element-ui css 的後面引入,那就可以覆蓋掉預設字型了,然後便可以在專案中通過 <i class="el-icon-lx-people"></i>
使用圖示了。
那機智的人就發現了,我自定義圖示的字首不要含 el-icon- 就不會有這樣的問題了。是的,那麼為了和原有字型保持一樣的樣式,需要複製它的整段css
/* 假設字首為 el-lx */
[class*="el-lx-"], [class^=el-lx-]{
font-family: lx-iconfont!important;
speak: none;
font-style: normal;
font-weight: 400;
font-variant: normal;
text-transform: none;
line-height: 1;
vertical-align: baseline;
display: inline-block;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
複製程式碼
導航選單
element-ui 關於導航選單的文件也是非常詳細了,但是還是有人提 issue 或者加 QQ 問我:三級選單怎麼弄等等。而且具體的選單項可能是伺服器端根據許可權而返回特定的資料項,因此不能寫死在模板中。
首先定好選單資料的格式如下,即使伺服器端返回的格式不是這樣,也需要前端處理成下面的格式:
export default {
data() {
return {
items: [{
icon: 'el-icon-lx-home',
index: 'dashboard',
title: '系統首頁'
},{
icon: 'el-icon-lx-calendar',
index: '1',
title: '表單相關',
subs: [{
index: '1-1',
title: '三級選單',
subs: [{
index: 'editor',
title: '富文字編輯器'
}]
}]
},{
icon: 'el-icon-lx-warn',
index: '2',
title: '錯誤處理',
subs: [{
index: '404',
title: '404頁面'
}]
}]
}
}
}
複製程式碼
icon 就是選單圖示,就可以用到我們上面自定義的圖示了;index 就是路由地址;title 就是選單名稱;subs 就是子選單了。而模板則通過判斷選單中是否包含 subs 從而顯示二級選單和三級選單。
<el-menu :default-active="onRoutes" :collapse="collapse" router>
<template v-for="item in items">
<template v-if="item.subs">
<el-submenu :index="item.index" :key="item.index">
<template slot="title">
<i :class="item.icon"></i><span slot="title">{{ item.title }}</span>
</template>
<template v-for="subItem in item.subs">
<el-submenu v-if="subItem.subs" :index="subItem.index" :key="subItem.index">
<template slot="title">{{ subItem.title }}</template>
<!-- 三級選單 -->
<el-menu-item v-for="(threeItem,i) in subItem.subs" :key="i" :index="threeItem.index">
{{ threeItem.title }}
</el-menu-item>
</el-submenu>
<el-menu-item v-else :index="subItem.index" :key="subItem.index">
{{ subItem.title }}
</el-menu-item>
</template>
</el-submenu>
</template>
<!-- 沒有二級選單 -->
<template v-else>
<el-menu-item :index="item.index" :key="item.index">
<i :class="item.icon"></i><span slot="title">{{ item.title }}</span>
</el-menu-item>
</template>
</template>
</el-menu>
複製程式碼
這樣就完成了一個動態的導航選單。
通過 Header 元件中的一個按鈕來觸發 Sidebar 元件展開或收起,涉及到了元件之間傳遞資料,這裡通過 Vue.js 單獨的事件中心(Event Bus)管理元件間的通訊。
const bus = new Vue();
複製程式碼
在 Header 元件中點選按鈕時觸發 collapse 事件:
bus.$emit('collapse', true);
複製程式碼
在 Sidebar 元件中監聽 collapse 事件:
bus.$on('collapse', msg => {
this.collapse = msg;
})
複製程式碼
圖表自適應
vue-manage-system 中用到的圖表外掛是 vue-schart,是把一個基於 canvas 的圖表外掛 schart.js 進行了封裝。要做到圖表能夠自適應寬度,隨著 window 或者父元素的大小改變而重新渲染,如果圖表外掛裡沒實現該功能,就需要自己手動實現。
vue-schart 中提供了 renderChart() 的方法可以重新渲染圖表,Vue.js 中父元件呼叫子元件的方法,可以通過 $refs 進行呼叫。
<schart ref="bar" canvasId="bar" :data="data" type="bar" :options="options"></schart>
複製程式碼
然後監聽 window 的 resize 事件,呼叫 renderChart() 方法重新渲染圖表。
import Schart from 'vue-schart';
export default {
components: {
Schart
},
mounted(){
window.addEventListener('resize', ()=>{
this.$refs.bar.renderChart();
})
}
}
複製程式碼
不過也要記得元件銷燬時移除監聽哦!監聽視窗大小改變完成了,那父元素大小改變呢?因為父元素寬度設為百分比,當側邊欄摺疊的時候,父元素的寬度發生了變化。但是 div 並沒有 resize 事件,無法監聽到它的寬度改變,但是觸發摺疊的時候,我們是知道的。那麼是否可以通過監聽到摺疊變化的時候,再呼叫渲染函式重新渲染圖表呢?那麼還是通過 Event Bus 監聽側邊欄的改變,並在 300ms 後重新渲染,因為摺疊時候有 300ms 的動畫過程
bus.$on('collapse', msg => {
setTimeout(() => {
this.$refs.bar.renderChart();
}, 300);
});
複製程式碼
多標籤頁
多標籤頁,也是提 issue 最多的一個功能。
當在 A 標籤頁輸入一些內容之後,開啟 B 標籤再返回到 A,要保留離開前的狀態,因此需要使用 keep-alive 進行快取,而且關閉之後的標籤頁就不再快取,避免關閉後再開啟還是之前的狀態。keep-alive 的屬性 include 的作用就是隻有匹配的元件會被快取。include 匹配的不是路由名,而是元件名,那麼每個元件都需要新增 name 屬性。
在 Tags 元件中,監聽路由變化,將開啟的路由新增到標籤頁中:
export default {
data() {
return {
tagsList: []
}
},
methods: {
setTags(route){
const isExist = this.tagsList.some(item => {
return item.path === route.fullPath;
})
if(!isExist){
this.tagsList.push({
title: route.meta.title,
path: route.fullPath,
name: route.matched[1].components.default.name
})
}
}
},
watch:{
$route(newValue, oldValue){
this.setTags(newValue);
}
}
}
複製程式碼
在 setTags 方法中,將一個標籤物件存到標籤陣列中,包括title(標籤顯示的title),path(標籤的路由地址),name(元件名,用於include匹配的)。路由地址需要用 fullPath 欄位,如果使用 path 欄位,那如果地址後面帶有引數,就都沒儲存起來了。
在 Home 元件中,監聽到標籤的變化,快取需要的元件。
<keep-alive :include="tagsList">
<router-view></router-view>
</keep-alive>
複製程式碼
export default {
data(){
return {
tagsList: []
}
},
created(){
// 只有在標籤頁列表裡的頁面才使用keep-alive,即關閉標籤之後就不儲存到記憶體中了。
bus.$on('tags', msg => {
let arr = [];
for(let i = 0, len = msg.length; i < len; i ++){
// 提取元件名存到tagsList中,通過include匹配
msg[i].name && arr.push(msg[i].name);
}
this.tagsList = arr;
})
}
}
複製程式碼
總結
由於該專案中不包含任何業務程式碼,所以還是相對比較簡單的,不過從開發中還是積累了一些經驗,在其它專案中可以更加熟練地開發。功能雖然不算多,但是也勉強夠用,如果有什麼好的建議,可以開 issue 一起討論。