Vue 实现图片下拉选择控件

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>
Vue 实现图片下拉选择控件的更多相关文章
- thinter中combobox下拉选择控件(九)
combobox控件,下拉菜单控件 combobox控件在tkinter中的ttk下 简单的实现下: import tkinter from tkinter import ttk # 导入ttk模块, ...
- tkinter中combobox下拉选择控件(九)
combobox控件,下拉菜单控件 combobox控件在tkinter中的ttk下 简单的实现下: import tkinter from tkinter import ttk # 导入ttk模块, ...
- ASP.NET MVC页面UI之联动下拉选择控件(省、市、县联动选择)
地区选择操作在WEB应用中比较常见的操作,本文在.net mvc3下实现了省市县三级联动选择功能. 本文博客出处:http://www.kwstu.com/ArticleView/admin_2013 ...
- Android下拉刷新控件--PullToRefresh的简单使用
Android中很多时候都会用到上下拉刷新,这是一个很常用的功能,Android的v4包中也为我们提供了一种原生的下拉刷新控件--SwipeRefreshLayout,可以用它实现一个简洁的刷新效果, ...
- [Android]下拉刷新控件RefreshableView的实现
以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/4172483.html 需求:自定义一个ViewGroup,实现 ...
- 上拉加载下拉刷新控件WaterRefreshLoadMoreView
上拉加载下拉刷新控件WaterRefreshLoadMoreView 效果: 源码: // // SRSlimeView // @author SR // Modified by JunHan on ...
- Android仿苹果版QQ下拉刷新实现(一) ——打造简单平滑的通用下拉刷新控件
前言: 忙完了结婚乐APP的开发,终于可以花一定的时间放在博客上了.好了,废话不多说,今天我们要带来的效果是苹果版本的QQ下拉刷新.首先看一下目标效果以及demo效果: 因为此效果实现的步骤 ...
- Android 解决下拉刷新控件和ScrollVIew的滑动冲突问题。
最近项目要实现ScrollView中嵌套广告轮播图+RecyleView卡片布局,并且RecyleView按照header和内容的排列样式,因为RecyleView的可扩展性很强,所以我毫无疑问的选择 ...
- android官方下拉刷新控件SwipeRefreshLayout的使用
可能开发安卓的人大多数都用过很多下拉刷新的开源组件,但是今天用了官方v4支持包的SwipeRefreshLayout觉得效果也蛮不错的,特拿出来分享. 简介:SwipeRefreshLayout组件只 ...
- android SwipeRefreshLayout google官方下拉刷新控件
下拉刷新功能之前一直使用的是XlistView很方便我前面的博客有介绍 SwipeRefreshLayout是google官方推出的下拉刷新控件使用方法也比较简单 今天就来使用下SwipeRefres ...
随机推荐
- lamada 表达式
语法篇 -- \(lamada\) 表达式 函数内定义的函数,看起来能使代码更加美观. 具体定义方法: 前面挂个 auto ,不管他返不返回值 后面是函数名(表达式名) 例: Cekas 先是中括号表 ...
- 详谈怎样配置微信小程序的分包以解决体积过大问题(转载)
一.文件结构和工具功能 1.小程序编译的文件结构 非常必要推荐了解小程序文件结构,对于稍大的项目,对于包的精简会起到柳暗花明又一村的效果 .众所周知,微信小程序分为"主包"和&qu ...
- error: rpmdb: BDB0113... rpm安装或尝查询时报错
等保要求安装杀毒软件,我跑脚本的时候发现异常退出了,一查芜湖,rpm管理包出问题了 root@VM_0_12_centos equal-protection]# rpm -g clamav error ...
- LaTex “too many unprocessed floats”
latex编辑时出现LaTex "too many unprocessed floats" 如何解决? 有人说是用/usepackage[section] {placeins} 我 ...
- JS处理html的编码(encode)与解码(decode)
一.用浏览器内部转换器实现转换 代码: var HtmlUtil = { // 1.用浏览器内部转换器实现html编码 htmlEncode: function(html) { // 创建一个元素容器 ...
- CSS – Selector
前言 这篇记入一些我常用到. 以前写的笔记 css 选择器 (学习笔记) Whatever (*) * {} By Id (#) #id {} By Class (.) .class-name {} ...
- C# 开源浏览器性能提升,体验Chrome级速度
前言 使用 C# 和 CefSharp 开发的全功能网页浏览器. 项目介绍 SharpBrowser 是目前最快的开源 C# 网页浏览器! 采用了轻量级的 CEF 渲染器,在呈现网页时甚至比 Goog ...
- 如何判断一个网站是用的Nginx,还是Apache
事件起因: 接手了同事移交过来的一个网站,但是不知道这个网站是用什么做代理的,于是就去网上查资料 解决办法: 打开cmd窗口,输入以下命令即可 curl --head 域名/IP 注意,--hea ...
- 解决 Vue 项目打包上线后客户端缓存的问题
由于重新打包后会导致对应的 js 和 css 文件 hash 值发生变化,客户端不刷新的话就会存在之前的文件找不到,导致报错的问题. 通过 build.sh 定义打包命令 #!/usr/bin/env ...
- Java日期时间API系列36-----Jdk8中java.time包中的新的日期时间API类应用,使用LocalTime计算十二时辰。
十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰.二十四小时和十二时辰对照表: 时辰 时间 24时制 子时 深夜 11:00 - 凌晨 01:00 23:00 - 01 : ...