当前位置: 首页 > article >正文

日常学习开发记录-select组件(1)

日常学习开发记录-select组件

  • 简介
  • 实现步骤
    • 第一阶段:基础Select组件
  • 组件API设计
    • 属性 (Props)
    • 事件 (Events)
    • 插槽 (Slots)

简介

本文档将详细说明如何实现一个类似于Element UI的Select组件,从简单的基础功能逐步实现到复杂的高级特性。

实现步骤

第一阶段:基础Select组件

  1. 创建基本结构

    • 实现一个基本的下拉框结构
    • 包含触发器(显示当前选中值)和下拉菜单(选项列表)
    • 使用v-show或v-if控制下拉菜单的显示与隐藏
  2. 基础交互功能

    • 点击触发器显示/隐藏下拉菜单
    • 点击选项后更新选中值
    • 点击外部区域关闭下拉菜单
    • 实现基本样式
  3. 数据绑定

    • 实现v-model双向绑定
    • 支持传入选项数据(options)
    • 默认插槽自定义选项内容

已实现代码

<template>
  <div
    :class="['my-select', { 'is-disabled': disabled }]"
    @click.stop="toggleDropdown"
    @mouseenter="handleMouseEnter"
    @mouseleave="handleMouseLeave"
    v-click-outside="closeDropdown"
  >
    <!-- 选择器触发器 -->
    <div class="my-select__trigger">
      <span v-if="!currentValue && !multiple" class="my-select__placeholder">
        {{ placeholder }}
      </span>
      <span v-else-if="!multiple" class="my-select__label">
        {{ getSelectedLabel() }}
      </span>
      <div v-else class="my-select__tags">
        <span
          v-for="item in selected"
          :key="typeof item === 'object' ? item.value : item"
          class="my-select__tag"
        >
          {{ typeof item === 'object' ? item.label : item }}
          <i class="my-select__tag-close" @click.stop="removeTag(item)">×</i>
        </span>
      </div>
      <i
        v-if="clearable && currentValue && !visible && hover"
        class="my-select__clear"
        @click.stop="clearSelection"
      >
        ×
      </i>
      <i v-else class="my-select__arrow" :class="{ 'is-reverse': visible }"></i>
    </div>

    <!-- 下拉菜单 -->
    <div v-show="visible" class="my-select__dropdown">
      <div
        v-for="(item, index) in options"
        :key="typeof item === 'object' ? item.value : item"
        class="my-select__option"
        :class="{
          'is-selected': isSelected(item),
          'is-disabled': item.disabled,
        }"
        @click.stop="handleOptionClick(item)"
      >
        <slot name="option" :item="item" :index="index">
          {{ typeof item === 'object' ? item.label : item }}
        </slot>
      </div>
      <div v-if="options.length === 0" class="my-select__empty">
        <slot name="empty">无数据</slot>
      </div>
    </div>
  </div>
</template>

<script>
  export default {
    name: 'MySelect',
    directives: {
      clickOutside: {
        bind(el, binding) {
          el.clickOutsideEvent = event => {
            if (!(el == event.target || el.contains(event.target))) {
              binding.value(event)
            }
          }
          document.addEventListener('click', el.clickOutsideEvent)
        },
        unbind(el) {
          document.removeEventListener('click', el.clickOutsideEvent)
        },
      },
    },
    props: {
      value: {
        type: [String, Array],
        default: '',
      },
      options: {
        type: Array,
        default: () => [],
      },
      multiple: {
        type: Boolean,
        default: false,
      },
      clearable: {
        type: Boolean,
        default: false,
      },
      placeholder: {
        type: String,
        default: '请选择',
      },
      disabled: {
        type: Boolean,
        default: false,
      },
    },
    data() {
      return {
        visible: false,
        currentValue: this.value,
        selected: [],
        hover: false,
      }
    },
    watch: {
      value: {
        handler(newVal) {
          this.currentValue = newVal
          if (this.multiple) {
            this.selected = Array.isArray(newVal) ? [...newVal] : []
          }
        },
        immediate: true,
      },
    },
    methods: {
      handleMouseEnter() {
        this.hover = true
      },
      handleMouseLeave() {
        this.hover = false
      },
      isSelected(item) {
        return this.multiple ? this.selected.includes(item) : this.currentValue === item.value
      },
      toggleDropdown() {
        if (this.disabled) return
        this.visible = !this.visible
      },
      closeDropdown() {
        this.visible = false
      },
      getSelectedLabel() {
        return (
          this.options.find(item => item.value === this.currentValue)?.label || this.placeholder
        )
      },
      handleOptionClick(item) {
        if (item.disabled) return

        if (this.multiple) {
          if (this.selected.includes(item)) {
            this.selected = this.selected.filter(i => i !== item)
          } else {
            this.selected.push(item)
          }
          this.$emit('input', this.selected)
          this.$emit('change', this.selected)
        } else {
          this.currentValue = item.value
          this.$emit('input', item.value)
          this.$emit('change', item.value)
          this.closeDropdown()
        }
      },
      clearSelection(event) {
        event.stopPropagation()
        this.currentValue = ''
        this.selected = []
        this.$emit('input', this.multiple ? [] : '')
        this.$emit('clear')
      },
      removeTag(item) {
        this.selected = this.selected.filter(i => i !== item)
        this.$emit('input', this.selected)
        this.$emit('remove-tag', item)
      },
    },
  }
</script>

<style lang="scss" scoped>
  .my-select {
    position: relative;
    display: inline-block;
    width: 240px;
    font-size: 14px;
    cursor: pointer;

    &.is-disabled {
      .my-select__trigger {
        background-color: rgb(245, 247, 250);
        color: rgb(192, 196, 204);
        cursor: not-allowed;
        border-color: rgb(228, 231, 237);
      }
    }

    &__trigger {
      display: flex;
      align-items: center;
      background-color: #fff;
      border: 1px solid #dcdfe6;
      border-radius: 4px;
      padding: 0 35px 0 15px;
      min-height: 40px;
      line-height: 40px;
      position: relative;
      transition: border-color 0.2s;
      box-sizing: border-box;

      &:hover {
        border-color: #c0c4cc;
      }
    }

    &__placeholder {
      color: #909399;
    }

    &__arrow {
      position: absolute;
      right: 15px;
      top: 50%;
      transform: translateY(-50%);
      transition: transform 0.3s;
      width: 0;
      height: 0;
      border-style: solid;
      border-width: 5px 5px 0 5px;
      border-color: #c0c4cc transparent transparent transparent;

      &.is-reverse {
        transform: translateY(-50%) rotate(180deg);
      }
    }

    &__clear {
      position: absolute;
      right: 15px;
      top: 50%;
      transform: translateY(-50%);
      color: #c0c4cc;
      font-size: 14px;

      &:hover {
        color: #909399;
      }
    }

    &__dropdown {
      position: absolute;
      top: 100%;
      left: 0;
      margin-top: 5px;
      background-color: #fff;
      border: 1px solid #e4e7ed;
      border-radius: 4px;
      box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
      box-sizing: border-box;
      z-index: 1000;
      width: 100%;
      max-height: 274px;
      overflow-y: auto;
    }

    &__option {
      padding: 0 20px;
      position: relative;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      height: 34px;
      line-height: 34px;
      box-sizing: border-box;

      &:hover {
        background-color: #f5f7fa;
      }

      &.is-selected {
        color: #409eff;
        font-weight: 700;
      }

      &.is-disabled {
        color: #c0c4cc;
        cursor: not-allowed;
      }
    }

    &__empty {
      padding: 10px 0;
      text-align: center;
      color: #909399;
    }

    &__tags {
      display: flex;
      flex-wrap: wrap;
      line-height: normal;
      max-width: 100%;
      overflow: hidden;
    }

    &__tag {
      display: inline-flex;
      align-items: center;
      max-width: 100%;
      margin: 2px 0 2px 6px;
      padding: 0 5px 0 10px;
      background-color: #f0f2f5;
      border-radius: 4px;
      height: 24px;
      line-height: 24px;
      white-space: nowrap;
      overflow: hidden;
      box-sizing: border-box;
    }

    &__tag-close {
      margin-left: 5px;
      color: #909399;
      font-size: 12px;
      cursor: pointer;

      &:hover {
        color: #606266;
      }
    }
  }
</style>

主要还是v-click-out的优化和 触发器,下拉菜单的逻辑。目前简单的功能是可以的了。效果:
在这里插入图片描述

组件API设计

属性 (Props)

  • value/v-model: 绑定值
  • options: 选项数据
  • placeholder: 占位文本
  • disabled: 是否禁用
  • clearable: 是否可清空
  • multiple: 是否多选

事件 (Events)

插槽 (Slots)

  • option: 自定义选项模板

http://www.kler.cn/a/599243.html

相关文章:

  • 【Linux】同步原理剖析及模拟BlockQueue生产消费模型
  • 数据结构--红黑树
  • SpringBoot星之语明星周边产品销售网站设计与实现
  • 23种设计模式-组合(Composite)设计模式
  • 第十六届蓝桥杯康复训练--6
  • 【C++】类和对象(匿名对象)
  • 【Unity】批处理和实例化的底层优化原理(未完)
  • 图论 | 98. 所有可达路径
  • C++效率掌握之STL库:stack queue函数全解
  • vue java 实现大地图切片上传
  • 分页查询互动问题(用户端)
  • getClass()和instanceof()有啥不同,如何记忆
  • 计算机视觉算法实战——相机标定技术
  • 【后端】【Django】【ORM】SearchFilter 详解
  • 基于javaweb的SpringBoot实习管理系统设计与实现(源码+文档+部署讲解)
  • Linux应用:异步IO、存储映射IO、显存的内存映射
  • 搜索引擎工作原理图解:抓取→索引→排名全链路拆解
  • clamav服务器杀毒(Linux服务器断网状态下如何进行clamav安装、查杀)
  • 04_Linux驱动_06_GPIO子系统总结
  • jangow-01-1.0.1靶机攻略