【 antd + vue 】Select二次封装:多选+二级级联+可自定义添加item组件

<template>
  <div
    ref="select"
    @mousedown="
      (e) => {
        e.preventDefault();
        handleClickLink();
        selectData.selectOpen = true;
      }
    "
  >
    <a-select
      id="selectCheck"
      :allowClear="allowClear"
      :disabled="disabled"
      dropdownClassName="select"
      :maxTagCount="maxTagCount"
      :mode="mode"
      :placeholder="placeholder"
      v-model:value="selectData.selectValue"
      :options="options"
      :getPopupContainer="() => $refs.select"
      @change="changeSelect"
      @focus="focusSelect"
      @blur="blurSelect"
      :open="selectData.selectOpen"
    >
      <template #dropdownRender>
        <a-tabs
          v-model:activeKey="selectData.activeKey"
          tab-position="left"
          @change="handleClickLink"
        >
          <a-tab-pane v-for="item in selectData.options" :key="item.value">
            <template #tab>
              <span class="left-title"> {{ item.label }}</span>
            </template>
            <a-checkbox-group
              v-model:value="selectData.checkbox"
              :options="item.children"
              @change="checkboxChange"
            >
            </a-checkbox-group>
            <div>
              <a-input
                v-model:value="selectData.addValue"
                placeholder="请输入自定义项"
                :maxlength="selectData.inputMaxlength"
                v-if="selectData.add"
                @click.stop="
                  (e) => {
                    e.target.focus();
                  }
                "
                @focus="focusInput"
                @blur="blurInput"
              />
              <a-button
                type="dashed"
                @click.stop="selectData.add = true"
                v-if="!selectData.add"
                >+ Add</a-button
              >
              <a-button type="primary" @click.stop="addItem" v-if="selectData.add"
                >确定</a-button
              >
            </div>
          </a-tab-pane>
        </a-tabs>
      </template>
    </a-select>
  </div>
</template>

<script>
import { reactive, onMounted, inject,watch } from "vue";
export default {
  name: "",
  emits: ["selectValue"],
  props: {
    // 是否支持清除
    allowClear: {
      type: Boolean,
      default: false,
    },
    // 是否禁用
    disabled: {
      type: Boolean,
      default: false,
    },
    // 最多显示多少个 tag
    maxTagCount: {
      type: Number,
      // default: 10,
    },
    // 设置 Select 的模式为多选或标签
    mode: {
      type: String,
      default: "multiple", // 'multiple' | 'tags' | 'combobox'
    },
    // 选择框默认文字
    placeholder: {
      type: String,
      default: "请选择",
    },
    // 下拉选项
    options: {
      type: Array,
      default: () => {
        return [];
      },
    },
    // 已选择的选项
    selectValue: {
      type: Array,
      default: () => {
        return [];
      },
    },
    // 输入框限制位数
    inputMaxlength: {
      type: Number,
      default: 6,
    },
  },
  components: {},
  setup(props, { emit }) {
    const message = inject("$message");
    //数据
    const selectData = reactive({
      selectValue: [],
      activeKey: "",
      activeKeyVal: "", // 一级菜单初始化值
      options: [],
      checkbox: [],
      add: false,
      addValue: "",
      selectClass: "",
      selectOpen: false,
      inputMaxlength: 0,
      isSelectFocus:false,
      isInputFocus:false,
    });

    // #region 监听区间 生命周期
    onMounted(async () => {
      selectData.options = props.options;
      selectData.activeKeyVal = selectData.options[0].value;
      selectData.activeKey = selectData.activeKeyVal;
      selectData.selectValue = props.selectValue;
      selectData.inputMaxlength = props.inputMaxlength;
    });

    watch(
      () => selectData.selectOpen,
      (newVal, oldVal) => {
        if(newVal){
          selectData.add = false;
          selectData.addValue = "";
          selectData.activeKey = selectData.activeKeyVal;// 默认一级选中第一个
          return;
        }
      }
    );

    watch(
      () => selectData.activeKey,
      (newVal, oldVal) => {
        if(newVal !== oldVal){
          selectData.add = false;
          selectData.addValue = "";
          return;
        }
      }
    );
    // #endregion

    /**
     * @description: Add
     * @return {*}
     */
    const addItem = () => {
      let arr = [];
      if (selectData.addValue) {
        selectData.options.forEach((item) => {
          arr.push(item.value);
          item.children.forEach((it) => {
            arr.push(it.value);
          });
        });

        if (arr.indexOf(selectData.addValue) !== -1) {
          message.warning("输入项重复,请重新输入");
        } else {
          selectData.options.forEach((item) => {
            if (selectData.activeKey === item.value) {
              item.children.push({
                value: `${selectData.addValue}`,
                label: `${selectData.addValue}`,
              });
            }
          });
          selectData.add = false;
          selectData.addValue = "";
          document.getElementById("selectCheck").focus();
        }
      } else {
        message.warning("请输入自定义项");
      }
    };

    /**
     * @description: 点击一级选项
     * @return {*}
     */
    const handleClickLink = () => {
      document.getElementById("selectCheck").focus();
      selectData.checkbox = [];
      selectData.options.forEach((item) => {
        if (selectData.activeKey === item.value) {
          item.children.forEach((it) => {
            if (selectData.selectValue.indexOf(it.value) !== -1) {
              selectData.checkbox.push(it.value);
            }
          });
        }
      });
    };

    /**
     * @description: 点击二级选项
     * @param {*} val 当前选中的值
     * @return {*}
     */
    const checkboxChange = (val) => {
      selectData.add = false;
      selectData.addValue = "";
      document.getElementById("selectCheck").focus();
      if (val.length > 0) {
        selectData.options.forEach((item) => {
          if (selectData.activeKey === item.value) {
            item.children.forEach((it) => {
              if (val.indexOf(it.value) === -1) {
                if (selectData.selectValue.indexOf(it.value) !== -1) {
                  selectData.selectValue.splice(
                    selectData.selectValue.indexOf(it.value),
                    1
                  );
                }
                return;
              }
            });
            return;
          }
        });
        val.forEach((item) => {
          if (selectData.selectValue.indexOf(item) === -1) {
            selectData.selectValue.push(item);
            return;
          }
        });
      } else {
        selectData.options.forEach((item) => {
          if (item.value === selectData.activeKey) {
            item.children.forEach((it) => {
              if (selectData.selectValue.indexOf(it.value) !== -1) {
                selectData.selectValue.splice(
                  selectData.selectValue.indexOf(it.value),
                  1
                );
              }
            });
            return;
          }
        });
      }
    };

    /**
     * @description: 选中 option 时
     * @return {*}
     */
    const changeSelect = () => {
      handleClickLink();
      emit("selectValue", selectData.selectValue);
    };

    /**
     * @description: 下拉框获得焦点时回调
     * @return {*}
     */
     const focusSelect = () => {
      selectData.isSelectFocus = true;
      handleClickLink();
    };

    /**
     * @description: 下拉框失去焦点的时回调
     * @return {*}
     */
    const blurSelect = () => {
      emit("selectValue", selectData.selectValue);

      selectData.isSelectFocus = false;
      if(!selectData.isSelectFocus && !selectData.isInputFocus){
        selectData.selectOpen = false;
      }else{
        selectData.selectOpen = true;
      }
    };

    /**
     * @description: 输入框获得焦点时回调
     * @return {*}
     */
     const focusInput = () => {
      selectData.isInputFocus = true;
    };

    /**
     * @description: 输入框失去焦点时回调
     * @return {*}
     */
     const blurInput = () => {
      selectData.isInputFocus = false;
      if(!selectData.isSelectFocus && !selectData.isInputFocus){
        selectData.selectOpen = false;
      }else{
        selectData.selectOpen = true;
      }
    };

    return {
      //变量
      selectData,
      //事件
      addItem,
      checkboxChange,
      handleClickLink,
      changeSelect,
      blurSelect,
      focusSelect,
      focusInput,
      blurInput,
    };
  },
};
</script>

<style lang="less" scoped>
.ant-select {
  width: 300px !important;
}

/deep/.select {
  // display: block !important;

  .ant-tabs {
    .ant-tabs-tab {
      margin: 0;
      padding-right: 16px;
      &::after {
        display: inline-block;
        content: ">";
        margin-left: 30px;
      }
    }

    .ant-tabs-ink-bar {
      display: none !important;
    }
    .ant-tabs-left-content {
      padding-left: 10px;

      .ant-input {
        width: 122px;
      }
    }
  }

  .ant-checkbox-group {
    display: flex;
    flex-direction: column;

    .ant-checkbox-group-item {
      margin: 8px;
    }
  }

  .ant-btn {
    margin-left: 4px;
    font-size: 12px;
  }
}
</style>

<think>我们使用前端框架(这里Vue和React)来优化二级级联下拉框的实现。主要优化点包括: 1. 响应式数据绑定:自动更新视图,无需手动操作DOM 2. 组件化:将级联下拉框封装为可复用组件 3. 状态管理:更清晰的数据流和状态管理 由于用户要求使用前端框架优化,我们将分别给出Vue和React的实现示例。 注意:以下示例将使用模拟数据,实际项目中可替换为API调用。 一、Vue实现方案(使用Vue 3 Composition API) 二、React实现方案(使用函数组件和Hooks) 三、对比分析 四、相关问题 下面分别实现: 一、Vue实现方案 我们创建一个Vue组件,包含两个select,第二个select项依赖于第一个select择。 代码示例:</think>### 使用前端框架优化二级级联下拉框实现 下面分别使用 React 和 Vue 框架实现优化的二级级联下拉框,解决原生实现中的常见问题(如漂浮定位、响应式更新等),并提供更优雅的代码结构: --- #### 一、React 实现方案(使用 Ant Design 组件库) ```jsx import React, { useState, useEffect } from 'react'; import { Select, Spin } from 'antd'; const { Option } = Select; const CascadingSelect = () => { const [provinces, setProvinces] = useState([]); const [cities, setCities] = useState([]); const [selectedProvince, setSelectedProvince] = useState(null); const [loading, setLoading] = useState(false); // 模拟API获取省份数据 useEffect(() => { const fetchProvinces = async () => { setLoading(true); // 实际项目中替换为API调用 setTimeout(() => { setProvinces([ { id: 1, name: '广东省' }, { id: 2, name: '江苏省' }, { id: 3, name: '浙江省' } ]); setLoading(false); }, 500); }; fetchProvinces(); }, []); // 根据省份获取城市 useEffect(() => { const fetchCities = async () => { if (!selectedProvince) return; setLoading(true); setCities([]); // 模拟API根据省份获取城市 setTimeout(() => { const cityData = { 1: ['广州市', '深圳市', '东莞市'], 2: ['南京市', '苏州市', '无锡市'], 3: ['杭州市', '宁波市', '温州市'] }; setCities(cityData[selectedProvince] || []); setLoading(false); }, 800); }; fetchCities(); }, [selectedProvince]); return ( <div style={{ maxWidth: 500, margin: '40px auto' }}> <Select placeholder="择省份" onChange={setSelectedProvince} style={{ width: '100%', marginBottom: 16 }} getPopupContainer={trigger => trigger.parentElement} // 解决漂浮问题[^2] showSearch optionFilterProp="children" filterOption={(input, option) => option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0 } > {provinces.map(province => ( <Option key={province.id} value={province.id}> {province.name} </Option> ))} </Select> <Select placeholder={selectedProvince ? "择城市" : "请先择省份"} disabled={!selectedProvince || loading} style={{ width: '100%' }} getPopupContainer={trigger => trigger.parentElement} showSearch optionFilterProp="children" > {loading ? ( <Option key="loading"> <Spin size="small" /> 加载中... </Option> ) : ( cities.map((city, index) => ( <Option key={index} value={city}> {city} </Option> )) )} </Select> </div> ); }; export default CascadingSelect; ``` **React 实现优化点:** 1. 使用 `useEffect` 分离数据获取逻辑,符合 React 的声明式特性 2. 通过 `getPopupContainer` 解决下拉框漂浮问题[^2] 3. 内置搜索过滤功能(`showSearch` 属性) 4. 组件化状态管理,避免直接 DOM 操作 5. 添加加载状态指示器提升用户体验 --- #### 二、Vue 实现方案(使用 Element Plus 组件库) ```vue <template> <div class="cascade-container"> <el-select v-model="selectedProvince" placeholder="择省份" filterable clearable @change="handleProvinceChange" style="margin-bottom: 15px; width: 100%" > <el-option v-for="province in provinces" :key="province.id" :label="province.name" :value="province.id" /> </el-select> <el-select v-model="selectedCity" placeholder="请先择省份" :disabled="!selectedProvince" filterable clearable style="width: 100%" > <el-option v-if="loading" disabled label="加载中..." > <el-icon class="is-loading"><Loading /></el-icon> </el-option> <el-option v-for="city in cities" :key="city" :label="city" :value="city" /> </el-select> </div> </template> <script setup> import { ref, watch, onMounted } from 'vue'; import { Loading } from '@element-plus/icons-vue'; const provinces = ref([]); const cities = ref([]); const selectedProvince = ref(null); const selectedCity = ref(null); const loading = ref(false); // 获取省份数据 onMounted(async () => { loading.value = true; // 实际项目中替换为API调用 setTimeout(() => { provinces.value = [ { id: 1, name: '广东省' }, { id: 2, name: '江苏省' }, { id: 3, name: '浙江省' } ]; loading.value = false; }, 500); }); // 监听省份变化获取城市 watch(selectedProvince, async (newVal) => { if (!newVal) { cities.value = []; return; } loading.value = true; selectedCity.value = null; // 模拟API调用 setTimeout(() => { const cityData = { 1: ['广州市', '深圳市', '东莞市'], 2: ['南京市', '苏州市', '无锡市'], 3: ['杭州市', '宁波市', '温州市'] }; cities.value = cityData[newVal] || []; loading.value = false; }, 800); }); </script> <style scoped> .cascade-container { max-width: 500px; margin: 40px auto; padding: 20px; border-radius: 8px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); } </style> ``` **Vue 实现优化点:** 1. 使用 Composition API 组织逻辑,相比 Options API 更灵活[^1] 2. 响应式系统自动处理视图更新,无需手动操作 DOM 3. 内置过滤功能(`filterable` 属性) 4. 通过 `watch` 监听器实现级联逻辑 5. 组件化设计便于复用和状态管理 --- ### 框架对比与择建议 | 特性 | React | Vue | |--------------------|--------------------------------|--------------------------------| | **响应式原理** | 需手动管理状态更新 | 自动依赖追踪[^1] | | **模板语法** | JSX | 类HTML模板 | | **组件化** | 函数组件 + Hooks | 单文件组件(SFC) | | **学习曲线** | 较陡峭 | 较平缓 | | **性能** | 虚拟DOM优化 | 更细粒度的响应式更新 | | **适用场景** | 大型复杂应用 | 中小型快速开发 | **择建议:** - 择 React 如果: - 项目需要高度灵活性 - 团队熟悉函数式编程 - 需要丰富的第三方库支持 - Vue 如果: - 追求开发速度和简洁性 - 需要渐进式集成到现有项目 - 团队更熟悉HTML/CSS传统开发 > 两种方案都解决了原生实现中的关键问题:通过组件库的 `getPopupContainer` 或内置定位机制解决下拉框漂浮问题[^2],利用响应式系统简化状态管理,并提供搜索过滤等增强功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值