自定义下拉框组件

javascript 复制代码
<template>
  <div class="floor-management">
    <!-- 使用 el-select 组件 -->
    <el-select v-model="selectedFloor" placeholder="测试内容" class="floor-select" :popper-class="'floor-select-popper'"
      @visible-change="handleVisibleChange">
      <!-- 自定义下拉框内容 -->
      <el-option v-for="floor in floorList" :key="floor.id" :value="floor.id" :label="floor.displayText"
        class="custom-option">
        <!-- 自定义每个选项的内容 -->
        <div class="floor-option-content" @click="handleOptionClick(floor)">
          <!-- 左侧:测试内容信息 -->
          <div class="floor-info-left">
            <div class="floor-name"> {{ floor.name }} </div>
            <div class="floor-thickness">厚度: <span class="thickness-value">{{ floor.thickness }}</span></div>
          </div>
          <!-- 右侧:操作按钮 -->
          <div class="floor-actions-right" @click.stop>
            <!-- 编辑按钮 -->
            <el-icon class="action-icon edit-icon" @click="handleEdit(floor)">
              <Edit />
            </el-icon>
            <!-- 删除按钮 -->
            <el-icon class="action-icon delete-icon" @click="handleDelete(floor)">
              <Delete />
            </el-icon>
          </div>
        </div>
      </el-option>
      <!-- 新建按钮 -->
      <div class="new-floor-item" @click="handleCreate">
        <span>新建</span>
        <el-icon class="plus-icon">
          <Plus />
        </el-icon>
      </div>
    </el-select>

    <!-- 编辑/新增弹窗 -->
    <el-dialog v-model="dialogVisible" :title="isEditing ? '编辑测试内容' : '新增测试内容'" width="400px" append-to-body
      :close-on-click-modal="false">
      <el-form :model="formData" :rules="formRules" ref="formRef" label-width="80px">
        <el-form-item label="名称" prop="name">
          <el-input v-model="formData.name" placeholder="请输入测试内容名称" @input="autoGenerateNumber" />
        </el-form-item>

        <el-form-item label="厚度" prop="thickness">
          <div class="thickness-form-group">
            <el-input v-if="formData.thicknessType === 'number'" v-model.number="formData.thicknessValue" type="number"
              placeholder="输入厚度值" style="width: 120px">
              <template #append>mm</template>
            </el-input>
            <el-input v-else v-model="formData.customThickness" placeholder="如:按设计" style="width: 200px" />

            <el-radio-group v-model="formData.thicknessType" class="thickness-type-group"
              @change="handleThicknessTypeChange">
              <el-radio label="number">数值</el-radio>
              <el-radio label="custom">自定义</el-radio>
            </el-radio-group>
          </div>
        </el-form-item>
      </el-form>

      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="handleSave">确定</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Edit, Delete, Plus } from '@element-plus/icons-vue'

// 当前选中的测试内容
const selectedFloor = ref('1')

// 弹窗控制
const dialogVisible = ref(false)
const isEditing = ref(false)
const formRef = ref(null)

// 表单数据
const formData = reactive({
  id: '',
  name: '',
  number: '',
  thicknessType: 'number',
  thicknessValue: 100,
  customThickness: '',
  showNumber: true
})

// 表单验证规则
const formRules = {
  name: [
    { required: true, message: '请输入名称', trigger: 'blur' },
    { min: 2, max: 20, message: '名称长度在2-20个字符', trigger: 'blur' }
  ],
  thickness: [
    {
      validator: (rule, value, callback) => {
        if (formData.thicknessType === 'number') {
          if (formData.thicknessValue === null || formData.thicknessValue === undefined || formData.thicknessValue === '') {
            callback(new Error('请输入厚度值'))
          } else if (formData.thicknessValue <= 0) {
            callback(new Error('厚度必须大于0'))
          } else if (formData.thicknessValue > 1000) {
            callback(new Error('厚度不能超过1000mm'))
          } else {
            callback()
          }
        } else {
          if (!formData.customThickness?.trim()) {
            callback(new Error('请输入厚度描述'))
          } else if (formData.customThickness.length > 20) {
            callback(new Error('厚度描述不能超过20个字符'))
          } else {
            callback()
          }
        }
      },
      trigger: 'blur'
    }
  ]
}

// 测试内容数据列表
const floorList = ref([
  {
    id: '1',
    name: '测试内容1',
    number: '1',
    thickness: '100',
    thicknessType: 'number',
    thicknessValue: 100,
    showNumber: true,
    displayText: '测试内容1 厚度: 100'
  },
  {
    id: '2',
    name: '测试内容2',
    number: '2',
    thickness: '30',
    thicknessType: 'number',
    thicknessValue: 30,
    showNumber: true,
    displayText: '测试内容2 厚度: 30'
  },
  {
    id: '3',
    name: '测试内容3',
    number: '3',
    thickness: '按设计',
    thicknessType: 'custom',
    customThickness: '按设计',
    showNumber: true,
    displayText: '测试内容3 厚度: 按设计'
  }
])

// 计算选中的测试内容对象
const currentFloor = computed(() => {
  return floorList.value.find(item => item.id === selectedFloor.value)
})

// 新增方法:处理选项点击
const handleOptionClick = (floor) => {
  selectedFloor.value = floor.id
  // 关闭下拉框
  const select = document.querySelector('.floor-select .el-select')
  if (select) {
    select.blur()
  }
}

// 处理下拉框显示/隐藏
const handleVisibleChange = (visible) => {
  if (visible) {
    nextTick(() => {
      // 调整下拉框宽度
      const dropdown = document.querySelector('.floor-select-popper')
      if (dropdown) {
        dropdown.style.minWidth = '280px'
      }
    })
  }
}

// 编辑测试内容
const handleEdit = (floor) => {
  isEditing.value = true
  Object.assign(formData, {
    id: floor.id,
    name: floor.name,
    number: floor.number,
    thicknessType: floor.thicknessType,
    thicknessValue: floor.thicknessValue || 100,
    customThickness: floor.customThickness || '',
    showNumber: floor.showNumber
  })
  dialogVisible.value = true
}

// 删除测试内容
const handleDelete = (floor) => {
  if (floorList.value.length <= 1) {
    ElMessage.warning('至少需要保留一个测试内容')
    return
  }

  ElMessageBox.confirm(
    `确定要删除"${floor.name}"吗?`,
    '删除确认',
    {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning',
      customClass: 'delete-confirm-dialog'
    }
  ).then(() => {
    const index = floorList.value.findIndex(item => item.id === floor.id)
    if (index !== -1) {
      const deletedFloor = floorList.value[index]
      floorList.value.splice(index, 1)

      // 如果删除的是当前选中的,自动选中上一个
      if (selectedFloor.value === floor.id) {
        if (index > 0) {
          selectedFloor.value = floorList.value[index - 1].id
        } else if (floorList.value.length > 0) {
          selectedFloor.value = floorList.value[0].id
        } else {
          selectedFloor.value = ''
        }
      }

      // 重新编号
      renumberFloors()

      ElMessage.success('删除成功')
      emit('delete', deletedFloor)
    }
  }).catch(() => { })
}

// 新增测试内容
const handleCreate = () => {
  isEditing.value = false
  resetForm()
  formData.id = 'floor_' + Date.now()
  dialogVisible.value = true
}

// 切换厚度类型
const handleThicknessTypeChange = (type) => {
  if (type === 'number') {
    formData.thicknessValue = 100
  } else {
    formData.customThickness = '按设计'
  }
}

// 自动生成编号
const autoGenerateNumber = () => {
  if (!isEditing.value && formData.name && !formData.number) {
    formData.number = (floorList.value.length + 1).toString()
  }
}

// 保存测试内容
const handleSave = async () => {
  if (!formRef.value) return

  try {
    await formRef.value.validate()

    const thickness = formData.thicknessType === 'number'
      ? formData.thicknessValue.toString()
      : formData.customThickness

    if (isEditing.value) {
      // 编辑现有测试内容
      const index = floorList.value.findIndex(item => item.id === formData.id)
      if (index !== -1) {
        const updatedFloor = {
          ...floorList.value[index],
          name: formData.name,
          thickness: thickness,
          thicknessType: formData.thicknessType,
          thicknessValue: formData.thicknessValue,
          customThickness: formData.customThickness,
          displayText: `${formData.name} 厚度: ${thickness}`
        }

        floorList.value[index] = updatedFloor
        ElMessage.success('更新成功')
        emit('update', updatedFloor)
      }
    } else {
      // 新增测试内容
      const floorName = formData.name || `测试内容${formData.number}`
      const newFloor = {
        id: formData.id,
        name: floorName,
        number: formData.number || (floorList.value.length + 1).toString(),
        thickness: thickness,
        thicknessType: formData.thicknessType,
        thicknessValue: formData.thicknessValue,
        customThickness: formData.customThickness,
        showNumber: true,
        displayText: `${floorName} 厚度: ${thickness}`
      }

      floorList.value.push(newFloor)
      // 自动选中新增的测试内容
      selectedFloor.value = newFloor.id

      ElMessage.success('添加成功')
      emit('create', newFloor)
    }

    dialogVisible.value = false
    resetForm()

  } catch (error) {
    console.error('表单验证失败:', error)
  }
}

// 重新编号
const renumberFloors = () => {
  floorList.value.forEach((item, index) => {
    const newNumber = (index + 1).toString()
    item.number = newNumber
    item.name = `测试内容${newNumber}`
    item.displayText = `${item.name} 厚度: ${item.thickness}`
  })
}

// 重置表单
const resetForm = () => {
  Object.assign(formData, {
    id: '',
    name: '',
    number: '',
    thicknessType: 'number',
    thicknessValue: 100,
    customThickness: '',
    showNumber: true
  })
}

// 初始化
onMounted(() => {
  // 初始化选中第一个
  if (floorList.value.length > 0 && !selectedFloor.value) {
    selectedFloor.value = floorList.value[0].id
  }
})

// 暴露给父组件的方法
defineExpose({
  getSelectedFloor: () => currentFloor.value,
  getFloorList: () => floorList.value,
  addFloor: (floor) => {
    const newFloor = {
      ...floor,
      id: 'floor_' + Date.now(),
      number: (floorList.value.length + 1).toString(),
      displayText: `${floor.name} 厚度: ${floor.thickness}`
    }
    floorList.value.push(newFloor)
  }
})

// 定义事件
const emit = defineEmits(['change', 'create', 'update', 'delete'])
</script>

<style scoped lang="scss">
.floor-management {
  width: 280px;
}

.floor-select {
  width: 80%;
}

.floor-select :deep(.el-input__wrapper) {
  border-radius: 4px;
  border: 1px solid #DCDFE6;
  box-shadow: none;
}

.floor-select :deep(.el-input__wrapper:hover) {
  border-color: #C0C4CC;
}

.floor-select :deep(.el-input__wrapper.is-focus) {
  border-color: #409EFF;
  box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}

.floor-select :deep(.el-select__caret) {
  color: #C0C4CC;
}

.floor-select :deep(.el-select__caret.is-reverse) {
  color: #409EFF;
}

/* 自定义选项样式 - 关键修复 */
.floor-option-content {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
  cursor: pointer;
  transition: background-color 0.2s;
  border-bottom: 1px solid #ECF5FF;
  padding: 5px 0px;
}

.custom-option {
  height: auto;
}

:deep(.el-select-dropdown__item) {
  padding: 0 8px !important;
  height: auto !important;
  display: flex;
  align-items: center;
  border-bottom: 1px solid #f0f0f0;
}

:deep(.el-select-dropdown__item):last-child {
  border-bottom: none;
}

:deep(.el-select-dropdown__item.selected) {
  background-color: #ECF5FF !important;
  position: relative;
}

:deep(.el-select-dropdown__item.selected)::after {
  content: "✓";
  position: absolute;
  right: 10px;
  color: #409EFF;
  font-weight: bold;
  font-size: 16px;
}

:deep(.el-select-dropdown__item.selected) .floor-actions-right {
  margin-right: 20px;
}

:deep(.el-select-dropdown__item):hover {
  background-color: #F5F7FA !important;
}

.floor-info-left {
  flex: 1;
  min-width: 0;
}

.floor-name {
  display: flex;
  align-items: center;
  gap: 5px;
}

.floor-name .floor-copntent {
  color: #333;
  font-weight: 500;
}

.floor-number {
  color: #666;
}

.floor-thickness {
  font-size: 12px;
  color: #999;
  display: flex;
  align-items: center;
  gap: 4px;
}

.thickness-value {
  font-weight: 500;
}


/* 右侧操作按钮 */
.floor-actions-right {
  display: flex;
  gap: 12px;
  padding: 0 4px;
  opacity: 0.7;
  transition: opacity 0.2s;
}

:deep(.el-select-dropdown__item):hover .floor-actions-right {
  opacity: 1;
}

/* 操作图标样式 */
.action-icon {
  width: 20px;
  height: 20px;
  cursor: pointer;
  color: #8B4513;
  transition: all 0.2s;
  padding: 2px;
  border-radius: 3px;
  // border: 1px solid transparent;
  display: flex;
  align-items: center;
  justify-content: center;
}

.action-icon:hover {
  background-color: #F5F5F5;
  border-color: #E8E8E8;
  transform: scale(1.1);
}

.delete-icon {
  color: #F56C6C;
}

/* 分隔线 */
.custom-divider {
  height: 1px;
  background: linear-gradient(to right,
      transparent 0%,
      #f0f0f0 20%,
      #f0f0f0 80%,
      transparent 100%);
  margin: 8px 12px;
}

/* 新建选项 */
.new-floor-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 15px 25px;
  cursor: pointer;
  color: #409EFF;
  transition: all 0.2s;
  user-select: none;
  font-size: 16px;
}

.new-floor-item:hover {
  background-color: #F5F7FA;
  color: #66B1FF;
}

.plus-icon {
  color: #F56C6C;
  font-size: 18px;
  font-weight: bold;
  width: 20px;
  height: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #FEF0F0;
  // border: 1px solid #FDE2E2;
  border-radius: 50%;
  transition: all 0.2s;
}

.new-floor-item:hover .plus-icon {
  background: #FFEBEB;
  border-color: #F56C6C;
  transform: rotate(90deg);
}

/* 对话框表单样式 */
.thickness-form-group {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}

.thickness-type-group {
  margin-left: auto;
}
</style>

<style>
/* 全局下拉框样式 - 修复显示不完整问题 */
.floor-select-popper {
  /* min-width: 200px !important; */
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1) !important;
  /* border: 1px solid #E4E7ED !important; */
  border-radius: 4px !important;
  overflow: hidden !important;
}

.floor-select-popper .el-select-dropdown__wrap {
  max-height: 400px !important;
  overflow-y: auto !important;
}

.floor-select-popper .el-select-dropdown__list {
  padding: 0 !important;
}

/* 修复选中项背景色 */
.floor-select-popper .el-select-dropdown__item.selected {
  background-color: #ECF5FF !important;
}

.floor-select-popper .el-select-dropdown__item:hover:not(.selected) {
  background-color: #F5F7FA !important;
}

/* 滚动条样式 */
.floor-select-popper .el-select-dropdown__wrap::-webkit-scrollbar {
  width: 6px;
}

.floor-select-popper .el-select-dropdown__wrap::-webkit-scrollbar-track {
  background: #f5f5f5;
  border-radius: 3px;
}

.floor-select-popper .el-select-dropdown__wrap::-webkit-scrollbar-thumb {
  background: #c1c1c1;
  border-radius: 3px;
}

.floor-select-popper .el-select-dropdown__wrap::-webkit-scrollbar-thumb:hover {
  background: #a8a8a8;
}
</style>
相关推荐
阿珊和她的猫几秒前
IIFE:JavaScript 中的立即调用函数表达式
开发语言·javascript·状态模式
+VX:Fegn089513 分钟前
计算机毕业设计|基于springboot + vue在线音乐播放系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
智商偏低25 分钟前
JSEncrypt
javascript
+VX:Fegn089544 分钟前
计算机毕业设计|基于springboot + vue律师咨询系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·课程设计
2501_944711432 小时前
构建 React Todo 应用:组件通信与状态管理的最佳实践
前端·javascript·react.js
困惑阿三2 小时前
2025 前端技术全景图:从“夯”到“拉”排行榜
前端·javascript·程序人生·react.js·vue·学习方法
苏瞳儿2 小时前
vue2与vue3的区别
前端·javascript·vue.js
weibkreuz3 小时前
收集表单数据@10
开发语言·前端·javascript
在西安放羊的牛油果4 小时前
浅谈 import.meta.env 和 process.env 的区别
前端·vue.js·node.js
王林不想说话4 小时前
提升工作效率的Utils
前端·javascript·typescript