element-ui 的組件庫中沒有圖片下拉選擇組件,基於 el-select 組件做的改動並不能完全滿足需求,因此決定重寫一個。 從頭到尾做下來收穫很多,我決定把實現過程中遇到的問題記錄一下。 效果圖 線上試用地址 設計要點 接下來將上面代碼中的關鍵部分拆分介紹 1. 回顯選中的圖片和 label ...
element-ui 的組件庫中沒有圖片下拉選擇組件,基於 el-select 組件做的改動並不能完全滿足需求,因此決定重寫一個。
從頭到尾做下來收穫很多,我決定把實現過程中遇到的問題記錄一下。
效果圖
設計要點
接下來將上面代碼中的關鍵部分拆分介紹
1. 回顯選中的圖片和 label
下拉選項組件的本質是一個 input
,畢竟下拉選擇也是為了快速 input 嘛。那我們的設計理念就是 "以 input 為中心",input 左側留出固定的寬度回顯選擇的圖片,input 的右側留出固定寬度顯示 icon,提醒用戶支持下拉/搜索。
為了在輸入框左側顯示圖片,我們設置圖片元素為 position: absolute;
input 元素通過設置 padding-left
和 padding-right
將 image 和 icon 的空間預留出。
右側預設顯示下拉 icon,當顯示下拉選項時切換為搜索 icon,提示用戶輸入框支持搜索功能。
<div class="input-wrapper">
<div class="input-prefix-icon-container">
<img class="input-prefix-icon" :src="selectedOption.icon" />
</div>
<input v-model="inputContent" class="input-text" />
<div class="input-postfix-icon">
<!-- 這裡顯示下拉選擇的 icon -->
<!-- 這裡顯示搜索的 icon -->
</div>
</div>
2. 下拉選項
點擊選擇控制項時顯示下拉選項,選擇某個選項或者點擊頁面空白處時隱藏下拉選項。
下拉選項中依次顯示選項的 image、選項的 label,選項的 category。
已選中的選項要區別於未選中的選項,這裡用到了動態 css 綁定,通過比較 selectedOption.key 和 option.key 是否相等來判斷選中狀態。
<div class="select-option-list" v-show="showSelectOptions">
<div v-for="option in (showAllOptions ? options : filteredOptions)" :key="option.key"
@click="selectOption(option)" class="select-option"
:class="{ 'selected-option': option.key === selectedOption.key }">
<div class="select-option-icon-container">
<img v-if="option.icon" :src="option.icon" :alt="option.label" class="select-option-icon" />
</div>
<div class="flex-between fill-content">
<div class="select-option-name">{{ option.label }}</div>
<div class="select-option-label">{{ option.type }}</div>
</div>
</div>
</div>
.selected-option {
background-color: #f5f7fa;
}
為了保證每個 image 占據相同的寬度,label 有相同的縮進,為 image 設置了 max-width
和 min-width
為相同值:
.select-option-icon-container {
min-width: 60px;
max-width: 60px;
height: 100%;
}
下拉選項 list 要設置 max-height
和 overflow-y:auto
,防止選項較多時占據太多頁面空間。微調滾動條的顯示樣式:
/* 滾動條整體樣式 */
.select-option-list::-webkit-scrollbar {
width: 6px;
}
/* 滾動條軌道樣式 */
.select-option-list::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 6px;
}
/* 滾動條滑塊樣式 */
.select-option-list::-webkit-scrollbar-thumb {
background: #dadcdd;
border-radius: 6px;
}
/* 滑塊 hover 樣式 */
.select-option-list::-webkit-scrollbar-thumb:hover {
background: #999;
}
3. 支持搜索
當用戶輸入了搜索內容時(@input),希望顯示過濾後的選項以快速定位;當我們點擊控制項時,一般是有選項切換的需求,此時需要顯示全部的選項,通過 showAllOptions
來控制是否顯示全部的選項。
<div class="input-wrapper" @click="showSelectOptions=true;">
<div class="input-prefix-icon-container">
<img class="input-prefix-icon" :src="selectedOption.icon" />
</div>
<input v-model="inputContent" @focus="showAllOptions=true" @input="showAllOptions=false" class="input-text" />
<div class="input-postfix-icon" @click="showAllOptions=true">
<!-- 這裡顯示下拉選擇的 icon -->
<!-- 這裡顯示搜索的 icon -->
</div>
</div>
4. 組件數據傳遞
父組件傳遞 options 選項給子組件,子組件將選中的選項通知給父組件:
export default {
props: {
// 父組件傳遞來的所有選項
options: {
type: Array,
required: true
}
},
data () {
return {
// 選擇的選項
selectedOption: this.options[0]
}
},
methods: {
// 點擊某個選項時
selectOption (option) {
this.selectedOption = option
this.showSelectOptions = false
this.inputContent = option.label
// 將選擇的選項通知給父組件
// v-model 預設監聽input事件
this.$emit('input', option)
}
}
}
完整實現
ImgSelect.vue
<template>
<div class="img-select">
<div class="input-wrapper" @click="showSelectOptions = true;">
<div class="input-prefix-icon-container">
<img class="input-prefix-icon" v-if="selectedOption.icon" :src="selectedOption.icon" :alt="selectedOption.label" />
</div>
<input v-model="inputContent" @focus="showAllOptions = true" @input="showAllOptions = false" class="input-text" />
<div class="input-postfix-icon" @click="showAllOptions = true">
<svg v-show="!showSelectOptions" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" class="css-1d3xu67-Icon">
<path d="M17,9.17a1,1,0,0,0-1.41,0L12,12.71,8.46,9.17a1,1,0,0,0-1.41,0,1,1,0,0,0,0,1.42l4.24,4.24a1,1,0,0,0,1.42,0L17,10.59A1,1,0,0,0,17,9.17Z"></path>
</svg>
<svg v-show="showSelectOptions" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" class="css-1d3xu67-Icon">
<path d="M21.71,20.29,18,16.61A9,9,0,1,0,16.61,18l3.68,3.68a1,1,0,0,0,1.42,0A1,1,0,0,0,21.71,20.29ZM11,18a7,7,0,1,1,7-7A7,7,0,0,1,11,18Z"></path>
</svg>
</div>
</div>
<div class="select-option-list" v-show="showSelectOptions">
<div v-for="option in (showAllOptions ? options : filteredOptions)" :key="option.key"
@click="selectOption(option)" class="select-option"
:class="{ 'selected-option': option.key === selectedOption.key }">
<div class="select-option-icon-container">
<img v-if="option.icon" :src="option.icon" :alt="option.label" class="select-option-icon" />
</div>
<div class="flex-between fill-content">
<div class="select-option-name">{{ option.label }}</div>
<div class="select-option-label">{{ option.type }}</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
options: {
type: Array,
required: true
}
},
data () {
return {
// 是否顯示下拉選項
showSelectOptions: false,
// 顯示全部的選項,還是過濾後的選項
showAllOptions: false,
// 選擇的選項
selectedOption: this.options[0],
// 輸入的搜索內容
inputContent: this.options[0].label
}
},
computed: {
filteredOptions () {
return this.options.filter(item => {
return item.label.toLowerCase().includes(this.inputContent.toLowerCase())
})
}
},
methods: {
selectOption (option) {
this.selectedOption = option
this.showSelectOptions = false
this.inputContent = option.label
// 將選擇的選項通知給父組件
this.$emit('input', option)
},
// 點擊空白處選項列表消失
handleClickOutside (event) {
const inputWrapper = this.$el.querySelector('.input-wrapper')
const selectOptionList = this.$el.querySelector('.select-option-list')
if (inputWrapper && !inputWrapper.contains(event.target)) {
if (selectOptionList && !selectOptionList.contains(event.target)) {
this.showSelectOptions = false
}
}
}
},
mounted () {
document.addEventListener('click', this.handleClickOutside)
},
beforeDestroy () {
document.removeEventListener('click', this.handleClickOutside)
}
}
</script>
<style scoped>
.img-select {
position: relative;
width: 400px;
}
.input-wrapper {
display: flex;
align-items: center;
position: relative;
height: 32px;
}
.input-prefix-icon {
width: 100%;
z-index: 1;
}
.input-prefix-icon-container {
position: absolute;
padding: 5px 8px;
height: 100%;
display: flex;
justify-content: flex-start;
box-sizing: border-box;
}
.input-prefix-icon-container:hover {
cursor: pointer;
}
.input-postfix-icon {
height: 100%;
padding-left: 8px;
padding-right: 8px;
position: absolute;
top: 0px;
right: 0px;
z-index: 1;
display: flex;
align-items: center;
}
.input-postfix-icon:hover {
cursor: pointer;
}
.input-text {
padding-left: 65px;
padding-right: 28px;
background: rgb(255, 255, 255);
line-height: 1.57143;
font-size: 14px;
color: rgb(36, 41, 46);
border: 1px solid rgba(36, 41, 46, 0.3);
flex-grow: 1;
border-radius: 4px;
height: 100%;
width: 100%;
z-index: 0;
}
.input-text:focus {
outline: unset;
box-shadow: rgb(244, 245, 245) 0px 0px 0px 2px, rgb(56, 113, 220) 0px 0px 0px 4px;
}
.select-option-list {
display: flex;
flex-direction: column;
box-shadow: rgba(24, 26, 27, 0.18) 0px 13px 20px 1px;
max-height: 200px;
width: 400px;
overflow-y: auto;
border: 1px solid rgba(36, 41, 46, 0.12);
position: absolute;
top: 38px;
}
.select-option {
display: flex;
align-items: center;
height: 24px;
padding: 6px;
cursor: pointer;
background-color: #ffffff;
border-bottom: 1px solid rgba(36, 41, 46, 0.12);
}
.select-option:hover {
background-color: #eceded;
}
.selected-option {
background-color: #f5f7fa;
}
.select-option-icon-container {
min-width: 60px;
max-width: 60px;
height: 100%;
}
.select-option-icon {
height: 100%;
display: flex;
}
.select-option-name {
white-space: nowrap;
font-size: 14px;
}
.select-option-label {
font-size: 12px;
color: rgba(36, 41, 46, 0.75);
white-space: nowrap;
}
.flex-between {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.fill-content {
width: 100%;
height: 100%;
}
/* 滾動條整體樣式 */
.select-option-list::-webkit-scrollbar {
width: 6px;
}
/* 滾動條軌道樣式 */
.select-option-list::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 6px;
}
/* 滾動條滑塊樣式 */
.select-option-list::-webkit-scrollbar-thumb {
background: #dadcdd;
border-radius: 6px;
}
/* 滑塊 hover 樣式 */
.select-option-list::-webkit-scrollbar-thumb:hover {
background: #999;
}
</style>
在父組件中使用:
<template>
<div id="app">
<img-select v-model="selectedDatasource" :options="datasourceOptions"></img-select>
</div>
</template>
<script>
import ImgSelect from './components/ImgSelect'
export default {
name: 'App',
components: {
ImgSelect
},
data () {
return {
datasourceOptions: [
{
key: '1',
label: 'MySQL-1',
type: 'MySQL',
icon: require('@/assets/images/mysql_logo.svg')
},
{
key: '2',
label: 'PostgresSQL-2',
type: 'PostgresSQL',
icon: require('@/assets/images/postgresql_logo.svg')
},
{
key: '3',
label: 'Oracle-3',
type: 'Oracle',
icon: require('@/assets/images/oracle_logo.svg')
},
],
selectedDatasource: {}
}
}
}
</script>