Vue 實現圖片下拉選擇控制項

来源:https://www.cnblogs.com/myownswordsman/p/-/vue-img-select
-Advertisement-
Play Games

element-ui 的組件庫中沒有圖片下拉選擇組件,基於 el-select 組件做的改動並不能完全滿足需求,因此決定重寫一個。 從頭到尾做下來收穫很多,我決定把實現過程中遇到的問題記錄一下。 效果圖 線上試用地址 設計要點 接下來將上面代碼中的關鍵部分拆分介紹 1. 回顯選中的圖片和 label ...


element-ui 的組件庫中沒有圖片下拉選擇組件,基於 el-select 組件做的改動並不能完全滿足需求,因此決定重寫一個。

從頭到尾做下來收穫很多,我決定把實現過程中遇到的問題記錄一下。

效果圖

線上試用地址

設計要點

接下來將上面代碼中的關鍵部分拆分介紹

1. 回顯選中的圖片和 label

下拉選項組件的本質是一個 input,畢竟下拉選擇也是為了快速 input 嘛。那我們的設計理念就是 "以 input 為中心",input 左側留出固定的寬度回顯選擇的圖片,input 的右側留出固定寬度顯示 icon,提醒用戶支持下拉/搜索。

為了在輸入框左側顯示圖片,我們設置圖片元素為 position: absolute; input 元素通過設置 padding-leftpadding-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-widthmin-width 為相同值:

.select-option-icon-container {
    min-width: 60px;
    max-width: 60px;
    height: 100%;
}

下拉選項 list 要設置 max-heightoverflow-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>

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ​GSYVideoPlayer是一個國產的移動端視頻播放器,它採用了IJKPlayer、Media3(EXOPlayer)、MediaPlayer、AliPlayer等四種播放器內核,支持彈幕、濾鏡、廣告等多項功能。 GSYVideoPlayer的Github主頁為https://github.co ...
  • 函數柯里化 含義: 可以大概理解為: 將fn(a,b,c)轉換為fn(a)(b)(c) 原函數: function sum(a,b){ return a+b } console.log(sum(1,2)) 柯里化後: function sum(a) { return function (b) { r ...
  • title: 如何在 Nuxt 3 中有效使用 TypeScript date: 2024/9/9 updated: 2024/9/9 author: cmdragon excerpt: 摘要:本文詳細介紹瞭如何在Nuxt 3項目中有效使用TypeScript,包括創建新項目、安裝TypeScrip ...
  • JavaScript速查表 本手冊絕大部分內容是從Airbnb JavaScript Style Guide精簡整理,將開發者們都明確的操作去掉,目的為了就是更快的速查。 此處為源地址。 譯制:HaleNing 目錄 基礎知識 類型 引用 對象 數組 解構 字元串 變數 屬性 測試 公共約束 註釋 ...
  • title: 使用 nuxi preview 命令預覽 Nuxt 應用 date: 2024/9/8 updated: 2024/9/8 author: cmdragon excerpt: 摘要:本文介紹瞭如何使用nuxi preview命令預覽Nuxt.js應用,包括安裝和準備環境、啟動預覽伺服器 ...
  • title: 使用 nuxi prepare 命令準備 Nuxt 項目 date: 2024/9/7 updated: 2024/9/7 author: cmdragon excerpt: 摘要:本文介紹nuxi prepare命令在Nuxt.js項目中的使用,該命令用於創建.nuxt目錄並生成類型 ...
  • 受 LabelImg 啟發的基於 web 的圖像標註工具,基於 Vue 框架 喲,網友們好,年更鴿子終於想起了他的博客園密碼。如標題所述,今天給大家帶來的是一個基於 vue2 的圖像標註工具。至於它誕生的契機呢,應該是我導 pass 掉了我的提議(讓甲方使用 LabelImg 進行數據標註),說是要 ...
  • title: 使用 nuxi init 創建全新 Nuxt 項目 date: 2024/9/6 updated: 2024/9/6 author: cmdragon excerpt: 摘要:本文介紹瞭如何使用nuxi init命令創建全新的Nuxt.js項目,包括安裝所需環境、命令使用方法、指定模板 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...