element-ui 因其元件豐富、可擴充性強、文件詳細等優點成為 Vue 最火的第三方 UI 框架。element-ui 其本身就針對後臺系統設計了很多實用的元件,基本上滿足了平時的開發需求。
既然如此,那麼我們為什麼還要進行二次封裝呢?
有以下兩種場景
在日常的開發過程中,部分模組重複性比較強,這個時候就會產生大量重複的程式碼。這些模組的樣式基本上是比較固定的,而且實現的功能也比較相近。如果每個地方都複製一份相似的程式碼,既不遵守程式碼的簡潔之道,也不利於後期的維護修改
此外,在一些業務背景下,產品可能會要求設計新的互動。這個時候也可以基於 element-ui 進行二次開發,將其封裝成一個新的元件方便多個地方使用
因為在日常開發過程中,專案主要以 Vue2 為主,並且現在很多公司仍在使用著 Vue2。故本文主要探討 Vue2 + element-ui 的專案可以怎麼封裝一些比較通用化的元件
核心思想
- 主要以父元件傳遞資料給子元件來實現一些功能,子元件定義固定的展示樣式,將具體要實現的業務邏輯丟擲來給父元件處理
- 儘量保持 element-ui 元件原有的方法(可以使用 v-bind="$attrs" 和 v-on="$listeners"),如果確實要做更改也儘量讓相似的方法方法名不變
元件
InputNumber
el-input-number 是一個很好用的元件,它只允許使用者輸入數字值。但是這個元件會有個預設值,給他賦予一個null 或""的時候會顯示0
這對於有些業務來說並不是很友好,例如新增頁面和編輯頁面
並且它這個元件的值是居中顯示的,和普通的input 框居左顯示不同,這就導致了樣式不太統一
改造:
讓 InputNumber 可以居左顯示且沒有預設值,用法保持和el-input-number元件相似
子元件 InputNumber.vue
<template>
<el-input-number id="InputNumber"
style="width: 100%"
v-model="insideValue"
v-bind="$attrs"
:controls="controls"
v-on="$listeners" />
</template>
<script>
export default {
// 讓父元件 v-model 傳參
model: {
prop: 'numberValue',
event: 'change',
},
props: {
numberValue: {
type: [Number, String],
default: undefined,
},
// 預設不顯示控制按鈕,這個可以根據實際情況做調整
controls: {
type: Boolean,
default: false,
},
},
data () {
return {
insideValue: undefined,
};
},
watch: {
numberValue (newVlalue) {
// 若傳入一個數字就顯示。為空則不顯示
if (typeof newVlalue === 'number') {
this.insideValue = newVlalue;
} else this.insideValue = undefined;
},
},
};
</script>
<style lang="scss" scoped>
#InputNumber {
/deep/ .el-input__inner {
text-align: left;
}
}
</style>
父元件
<template>
<InputNumber v-model="value"
style="width: 200px" />
</template>
<script>
import InputNumber from './InputNumber';
export default {
components: {
InputNumber,
},
data () {
return {
value: null,
};
},
};
</script>
演示:
OptionPlus
select 元件用在有較多選項時,但是有些選項的長度難免比較長,就會把選項框整個給撐大,例如:
這種還是比較短的時候了,有時因為公司名稱較長,或者其他業務要展示的欄位過長時就不太友好。
改造:
固定選項框的大小,讓選項顯示更加合理
子元件 OptionPlus.vue
<template>
<el-option :style="`width: ${width}px`"
v-bind="$attrs"
v-on="$listeners">
<slot />
</el-option>
</template>
<script>
export default {
props: {
width: {
type: Number,
},
},
};
</script>
<style lang="scss" scoped>
.el-select-dropdown__item {
min-height: 35px;
height: auto;
white-space: initial;
overflow: hidden;
text-overflow: initial;
line-height: 25px;
padding: 5px 20px;
}
</style>
父元件
<template>
<el-select v-model="value"
placeholder="請選擇">
<OptionPlus v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
:width="200">
</OptionPlus>
</el-select>
</template>
<script>
import OptionPlus from './OptionPlus';
export default {
components: {
OptionPlus,
},
data () {
return {
value: null,
options: [{
value: '選項1',
label: '黃金糕',
}, {
value: '選項2',
label: '雙皮奶特別好吃,以順德的最出名,推薦嘗試',
}, {
value: '選項3',
label: '蚵仔煎',
}, {
value: '選項4',
label: '龍鬚麵',
}, {
value: '選項5',
label: '北京烤鴨',
}],
};
},
};
效果:
FormPlus
後臺系統肯定會有查詢功能,搜尋條件大部分都是這三種,輸入框、下拉框和日期選擇。所以可以整合這三個常用的元素,將它們封裝成一個易於使用的元件
這三個元件是用來過濾條件的,因此一般與查詢和重置按鈕在一起
子元件FormPlus.vue
<template>
<div id="FormPlus">
<el-form ref="ruleForm"
:rules="rules"
:inline="inline"
:model="ruleForm"
class="ruleForm"
:label-width="labelWidth"
:style="formStyle">
<template v-for="(item, index) in list">
<template v-if="!item.type || item.type === 'input'">
<el-form-item :key="index"
:label="item.label"
:prop="item.model"
:required="item.required">
<el-input v-model.trim="ruleForm[item.model]"
:clearable="item.clearable === undefined || item.clearable"
filterable
:placeholder="item.placeholder" />
</el-form-item>
</template>
<template v-if="item.type === 'select'">
<el-form-item :key="index"
:label="item.label"
:prop="item.model"
:required="item.required">
<el-select :style="`width: ${formItemContentWidth}`"
v-model.trim="ruleForm[item.model]"
:clearable="item.clearable === undefined || item.clearable"
filterable
:placeholder="item.placeholder || ''">
<!-- 使用上文提到的 OptionPlus 元件 -->
<OptionPlus v-for="(i, key) in item.options"
:key="i[item.optionsKey] || key"
:label="i[item.optionsLabel] || i.label"
:value="i[item.optionsValue] || i.value"
:width="formItemContentWidth" />
</el-select>
</el-form-item>
</template>
<template v-if="item.type === 'date-picker'">
<el-form-item :key="index"
:prop="item.model"
:label="item.label"
:required="item.required">
<el-date-picker v-model.trim="ruleForm[item.model]"
:clearable="item.clearable === undefined || item.clearable"
:type="item.pickerType"
:placeholder="item.placeholder"
:format="item.format"
:value-format="item.valueFormat"
:picker-options="item.pickerOptions" />
</el-form-item>
</template>
</template>
<slot />
</el-form>
<el-row>
<el-col class="btn-container">
<el-button class="el-icon-search"
type="primary"
@click="submitForm">查詢</el-button>
<el-button class="el-icon-refresh"
@click="resetForm">重置</el-button>
</el-col>
</el-row>
</div>
</template>
<script>
import OptionPlus from './OptionPlus';
export default {
components: { OptionPlus },
props: {
list: {
type: Array,
default: () => [],
},
inline: {
type: Boolean,
default: true,
},
labelWidth: {
type: String,
default: '100px',
},
formItemWidth: {
type: String,
default: '400px',
},
formItemContentWidth: {
type: String,
default: '250px',
},
rules: {
type: Object,
default: () => { },
},
},
data () {
return {
ruleForm: {},
};
},
computed: {
formStyle () {
return {
'--formItemWidth': this.formItemWidth,
'--formItemContentWidth': this.formItemContentWidth,
};
},
},
watch: {
list: {
handler (list) {
this.handleList(list);
},
immediate: true,
deep: true,
},
},
methods: {
// 所填寫資料
submitForm () {
this.$refs['ruleForm'].validate((valid) => {
if (valid) {
const exportData = { ...this.ruleForm };
this.$emit('submitForm', exportData);
} else {
return false;
}
});
},
// 預設清空所填寫資料
resetForm () {
this.$refs.ruleForm.resetFields();
this.handleList(this.list);
this.$emit('resetForm');
},
handleList (list) {
for (let i = 0; i < list.length; i++) {
const formitem = list[i];
const { model } = formitem;
this.$set(this.ruleForm, model, '');
}
},
},
};
</script>
<style lang="scss" scoped>
#FormPlus {
.ruleForm {
width: 100%;
::v-deep.el-form-item {
width: var(--formItemWidth);
}
::v-deep.el-form-item__content {
width: var(--formItemContentWidth);
}
::v-deep.el-form-item__content .el-date-editor,
.el-input {
width: var(--formItemContentWidth);
}
}
.btn-container {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
}
</style>
父元件
<template>
<FormPlus :list="formList"
@submitForm="searchPage"
@resetForm="resetForm" />
</template>
<script>
import FormPlus from './FormPlus';
export default {
components: {
FormPlus,
},
data () {
return {
formList: [
{ label: '編號', model: 'applyNumber', placeholder: '請輸入編號' },
{ label: '名稱', model: 'name', placeholder: '請輸入名稱' },
{ type: 'date-picker', label: '開始時間', model: 'startTime', valueFormat: 'yyyy-MM-dd HH:mm:ss', placeholder: '請選擇開始時間' },
{ type: 'select', label: '狀態', model: 'status', placeholder: '請選擇狀態', options: [] },
],
};
},
methods: {
// 可以取到子元件傳遞過來的資料
searchPage (ruleForm) {
console.log(ruleForm, 'ruleForm');
},
resetForm () {
},
},
};
</script>
演示:
介面獲取到的資料可以用this.formList[index] = res.data;
來將資料塞進 el-select 的選項陣列中
這個元件其實是有一定侷限性的,如果確實有特別的需求還是要用 el-form 表單來寫
DrawerPlus
抽屜元件可以提供更深一級的操作,往往內容會比較多比較長。因此可以封裝一個元件,讓操作按鈕固定在 drawer 底部,以實現較好的互動
子元件 DrawerPlus.vue
<template>
<div id="drawerPlus">
<el-drawer v-bind="$attrs"
v-on="$listeners">
<el-scrollbar class="scrollbar">
<slot />
<div class="seat"></div>
<div class="footer">
<slot name="footer" />
</div>
</el-scrollbar>
</el-drawer>
</div>
</template>
<style lang="scss" scoped>
$height: 100px;
#drawerPlus {
.scrollbar {
height: 100%;
position: relative;
.seat {
height: $height;
}
.footer {
z-index: 9;
box-shadow: 0 -4px 6px rgba(0, 0, 0, 0.08);
width: 100%;
position: absolute;
bottom: 0px;
height: $height;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
}
}
}
</style>
父元件
<template>
<DrawerPlus title="編輯"
:visible.sync="drawerVisible"
direction="rtl"
size="45%">
<template slot="footer">
<el-button @click="drawerVisible = false">取消</el-button>
<el-button type="primary"
@click="drawerVisible = false">確定</el-button>
</template>
</DrawerPlus>
</template>
<script>
import DrawerPlus from './DrawerPlus';
export default {
components: {
DrawerPlus,
},
data () {
return {
drawerVisible: false,
};
},
};
</script>
效果:
使用 el-scrollbar 元件來實現更優雅的滾動效果,底部固定並增加一些陰影增加美觀
CopyIcon
在日常開發中,有時可能想實現一鍵複製,我們可以選擇手寫複製方法,也可以選擇引入 clipboard.js 庫幫助快速實現功能
在筆者寫過的一篇文章《在網站copy時自帶的版權小尾巴以及“複製程式碼“,可以怎麼實現 》,這篇文章中有提到怎麼手寫複製功能
當然,嚴格意義上來說,這個元件主要實現不是依賴 element-ui 的,但也有用到其中的一些元件,所以也寫在這裡
子元件 CopyIcon.vue
<template>
<i :class="`${icon} icon-cursor`"
title="點選複製"
@click="handleCopy($event, text)" />
</template>
<script>
// 引入 clipboard.js
import Clipboard from 'clipboard';
export default {
props: {
// 接收復制的內容
text: {
type: [String, Number],
default: null,
},
// 預設是複製 icon,可自定義 icon
icon: {
type: [String],
default: 'el-icon-copy-document',
},
// 自定義成功提示
message: {
type: [String, Number],
default: null,
},
},
methods: {
handleCopy (e, _text, message) {
const clipboard = new Clipboard(e.target, { text: () => _text });
const messageText = message || `複製成功:${_text}`;
clipboard.on('success', () => {
this.$message({ type: 'success', message: messageText });
clipboard.off('error');
clipboard.off('success');
clipboard.destroy();
});
clipboard.on('error', () => {
this.$message({ type: 'warning', message: '複製失敗,請手動複製' });
clipboard.off('error');
clipboard.off('success');
clipboard.destroy();
});
clipboard.onClick(e);
},
},
};
</script>
<style lang="scss" scoped>
.icon-cursor {
cursor: pointer;
}
</style>
父元件
<template>
<div>
<span>{{ value }}</span>
<CopyIcon :text="value" />
</div>
</template>
<script>
import CopyIcon from './CopyIcon';
export default {
components: {
CopyIcon,
},
data () {
return {
value: '這裡來測試一下-初見雨夜',
};
},
};
</script>
演示:
二次封裝雖說方便了後續的開發,但是當封裝的元件不能滿足需求時,可以考慮迭代或者用回 element-ui 原生的元件
因為筆者水平有限,對元件都是進行比較簡單的封裝,並且有些地方設計可能不是很合理,還請多多指教~