基于若依框架编写的选人组件(vue3 + ts 版本)

该文章已生成可运行项目,

先上效果图

多选未选中状态:

多选已选中状态:

单选未选中状态:

单选已选中状态:

废话不多说直接贴代码!

组件代码(template部分):

<template>
  <div class="select-people-container">
    <!-- 单选模式的输入框显示 -->
    <div v-if="!multiple" class="people-input" @click="handleOpen">
      <el-input :model-value="singleSelectedName" readonly :placeholder="placeholder" :disabled="disabled">
        <template #suffix>
          <el-icon class="cursor-pointer text-primary">
            <User />
          </el-icon>
        </template>
      </el-input>
    </div>

    <!-- 多选模式的标签显示 -->
    <div v-else class="people-tags" @click="handleOpen">
      <div class="tags-container" :class="{ 'disabled': disabled, 'has-tags': selectedPeople.length > 0 }">
        <el-tag v-for="person in selectedPeople" :key="person.userId" size="default" closable type="primary" effect="light"
          @close="handleRemovePerson(person)" @click.stop>
          <el-icon class="mr-1"><User /></el-icon>
          {{ person.nickName }}
        </el-tag>
        <span v-if="selectedPeople.length === 0" class="placeholder">
          <el-icon class="mr-1"><Plus /></el-icon>
          {{ placeholder }}
        </span>
        <el-icon class="suffix-icon">
          <ArrowDown />
        </el-icon>
      </div>
    </div>

    <!-- 选人对话框 -->
    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="80%" :before-close="handleCancel" append-to-body
      destroy-on-close class="select-people-dialog-wrapper">
      <div class="select-people-dialog">
        <el-row :gutter="24">
          <!-- 左侧部门树 -->
          <el-col :span="6" class="dept-tree-col">
            <div class="section-card">
              <div class="section-header">
                <el-icon class="header-icon"><OfficeBuilding /></el-icon>
                <span class="header-title">组织架构</span>
              </div>
              <div class="search-wrapper">
                <el-input v-model="deptSearchText" placeholder="搜索部门" clearable size="default"
                  @input="handleDeptFilter">
                  <template #prefix>
                    <el-icon class="search-icon">
                      <Search />
                    </el-icon>
                  </template>
                </el-input>
              </div>
              <div class="tree-wrapper">
                <el-tree ref="deptTreeRef" v-loading="deptLoading" :data="deptOptions"
                  :props="{ label: 'label', children: 'children' }" node-key="id" :filter-node-method="filterDeptNode"
                  :expand-on-click-node="false" highlight-current default-expand-all @node-click="handleDeptClick">
                  <template #default="{ node, data }">
                    <span class="tree-node">
                      <el-icon class="node-icon">
                        <OfficeBuilding />
                      </el-icon>
                      <span class="node-label">{{ node.label }}</span>
                    </span>
                  </template>
                </el-tree>
              </div>
            </div>
          </el-col>

          <!-- 中间人员列表 -->
          <el-col :span="12" class="people-list-col">
            <div class="section-card">
              <div class="section-header">
                <div class="header-left">
                  <el-icon class="header-icon"><User /></el-icon>
                  <span class="header-title">人员列表</span>
                  <el-badge :value="peopleTotal" class="ml-2" type="info" />
                </div>
                <div class="header-right">
                  <el-input v-model="peopleSearchText" placeholder="搜索姓名或手机号" clearable size="default"
                    class="search-input" @input="handlePeopleSearch">
                    <template #prefix>
                      <el-icon class="search-icon">
                        <Search />
                      </el-icon>
                    </template>
                  </el-input>
                </div>
              </div>

              <div class="table-wrapper">
                <el-table ref="peopleTableRef" v-loading="peopleLoading" :data="peopleList"
                  size="default" :highlight-current-row="!multiple" row-key="userId"
                  @current-change="handleSingleSelect" @selection-change="handleMultipleSelect"
                  class="people-table" :height="null">
                  <el-table-column v-if="multiple" type="selection" width="55" :reserve-selection="true" />
                  <el-table-column type="index" width="60" label="序号" />
                  <el-table-column prop="nickName" label="姓名" min-width="80">
                    <template #default="{ row }">
                      <div class="user-info">
                        <el-avatar :size="32" class="mr-2">
                          <el-icon><User /></el-icon>
                        </el-avatar>
                        <span class="user-name">{{ row.nickName }}</span>
                      </div>
                    </template>
                  </el-table-column>
                  <el-table-column prop="phonenumber" label="手机号" min-width="110">
                    <template #default="{ row }">
                      <div class="phone-info">
                        <el-icon class="mr-1"><Phone /></el-icon>
                        {{ row.phonenumber || '-' }}
                      </div>
                    </template>
                  </el-table-column>
                  <el-table-column prop="deptName" label="部门" min-width="100" show-overflow-tooltip>
                    <template #default="{ row }">
                      <el-tag size="small" type="info" effect="plain">{{ row.deptName || '-' }}</el-tag>
                    </template>
                  </el-table-column>
                </el-table>
              </div>

              <!-- 分页 -->
              <div class="pagination-wrapper">
                <el-pagination v-show="peopleTotal > 0" v-model:current-page="peopleQuery.pageNum"
                  v-model:page-size="peopleQuery.pageSize" :total="peopleTotal"
                  layout="total, sizes, prev, pager, next, jumper" :page-sizes="[10, 20, 50, 100]"
                  @size-change="handlePeopleSizeChange" @current-change="handlePeopleCurrentChange"
                  background size="small" />
              </div>
            </div>
          </el-col>

          <!-- 右侧已选人员 -->
          <el-col :span="6" class="selected-people-col">
            <div class="section-card">
              <div class="section-header">
                <div class="header-left">
                  <el-icon class="header-icon"><Check /></el-icon>
                  <span class="header-title">已选人员</span>
                  <el-badge :value="tempSelectedPeople.length" class="ml-2" type="primary" />
                </div>
                <el-button link size="small" @click="handleClearAll" class="clear-btn">
                  <el-icon class="mr-1"><Delete /></el-icon>
                  清空
                </el-button>
              </div>

              <div class="selected-list">
                <el-scrollbar>
                  <div v-for="person in tempSelectedPeople" :key="person.userId" class="selected-item">
                    <div class="person-info">
                      <el-avatar :size="28" class="person-avatar">
                        <el-icon><User /></el-icon>
                      </el-avatar>
                      <div class="person-details">
                        <div class="person-name">{{ person.nickName }}</div>
                        <div class="person-dept">{{ person.deptName }}</div>
                      </div>
                    </div>
                    <el-button link size="small" @click="handleRemoveFromSelected(person)" class="remove-btn">
                      <el-icon>
                        <Close />
                      </el-icon>
                    </el-button>
                  </div>
                  <div v-if="tempSelectedPeople.length === 0" class="empty-selected">
                    <el-icon class="empty-icon"><User /></el-icon>
                    <div class="empty-text">暂无选中人员</div>
                    <div class="empty-desc">请从左侧选择人员</div>
                  </div>
                </el-scrollbar>
              </div>
            </div>
          </el-col>
        </el-row>
      </div>

      <template #footer>
        <div class="dialog-footer">
          <div class="footer-info">
            <el-icon class="info-icon"><InfoFilled /></el-icon>
            <span>已选择 {{ tempSelectedPeople.length }} 人</span>
          </div>
          <div class="footer-actions">
            <el-button @click="handleCancel" size="default">
              <el-icon class="mr-1"><Close /></el-icon>
              取消
            </el-button>
            <el-button type="primary" @click="handleConfirm" size="default">
              <el-icon class="mr-1"><Check /></el-icon>
              确定
            </el-button>
          </div>
        </div>
      </template>
    </el-dialog>
  </div>
</template>

组件代码(script部分):

<script setup lang="ts" name="SelectPeople">
import { ref, reactive, computed, watch, nextTick, getCurrentInstance } from 'vue'
import { ElMessage } from 'element-plus'
import {
  User, Search, OfficeBuilding, Delete, Plus, ArrowDown,
  Phone, Check, Close, InfoFilled
} from '@element-plus/icons-vue'
import { listUser, deptTreeSelect } from '@/api/system/user'
import { UserVO, UserQuery } from '@/api/system/user/types'
import { DeptVO } from '@/api/system/dept/types'

// Props定义
interface Props {
  modelValue?: UserVO | UserVO[] | string | string[]  // 支持多种数据格式
  multiple?: boolean                    // 是否多选
  placeholder?: string                  // 占位符
  disabled?: boolean                   // 是否禁用
  title?: string                       // 对话框标题
  limit?: number                       // 多选时的最大选择数量
  deptId?: string | number            // 指定部门ID,只显示该部门下的人员
  excludeUserIds?: (string | number)[] // 排除的用户ID列表
}

const props = withDefaults(defineProps<Props>(), {
  multiple: false,
  placeholder: '请选择人员',
  disabled: false,
  title: '',
  limit: 0,
  deptId: '',
  excludeUserIds: () => []
})

// Emits定义
const emit = defineEmits<{
  'update:modelValue': [value: UserVO | UserVO[] | string | string[] | undefined]
  'change': [value: UserVO | UserVO[], people: UserVO[]]
}>()

const { proxy } = getCurrentInstance()!

// 响应式数据
const dialogVisible = ref(false)
const deptLoading = ref(false)
const peopleLoading = ref(false)
const deptSearchText = ref('')
const peopleSearchText = ref('')

// 部门相关
const deptTreeRef = ref()
const deptOptions = ref<DeptVO[]>([])
const currentDeptId = ref<string | number>('')

// 人员相关
const peopleTableRef = ref()
const peopleList = ref<UserVO[]>([])
const peopleTotal = ref(0)
const peopleQuery = reactive<UserQuery>({
  pageNum: 1,
  pageSize: 20,
  nickName: '',
  phonenumber: '',
  deptId: ''
})

// 选中的人员
const selectedPeople = ref<UserVO[]>([])          // 当前实际选中的人员
const tempSelectedPeople = ref<UserVO[]>([])      // 对话框中临时选中的人员

// 方法定义(在监听器之前定义)
const initSelectedPeople = (value: any) => {
  if (!value) {
    selectedPeople.value = []
    return
  }

  if (props.multiple) {
    if (Array.isArray(value)) {
      // 如果是用户对象数组
      if (value.length > 0 && typeof value[0] === 'object') {
        selectedPeople.value = value as UserVO[]
      } else {
        // 如果是用户ID数组,需要根据ID获取用户信息
        selectedPeople.value = []
        // 这里可以根据需要调用API获取用户详细信息
      }
    }
  } else {
    if (typeof value === 'object') {
      selectedPeople.value = [value as UserVO]
    } else {
      // 如果是用户ID,需要根据ID获取用户信息
      selectedPeople.value = []
    }
  }
}

const emitChange = () => {
  let value: any

  if (props.multiple) {
    value = selectedPeople.value
  } else {
    value = selectedPeople.value.length > 0 ? selectedPeople.value[0] : undefined
  }

  emit('update:modelValue', value)
  emit('change', value, selectedPeople.value)
}

const loadDeptTree = async () => {
  try {
    deptLoading.value = true
    const res = await deptTreeSelect()
    deptOptions.value = res.data

    // 如果指定了部门ID,设置为当前选中
    if (props.deptId) {
      currentDeptId.value = props.deptId
      nextTick(() => {
        deptTreeRef.value?.setCurrentKey(props.deptId)
      })
    }
  } catch (error) {
    console.error('加载部门树失败:', error)
    ElMessage.error('加载部门信息失败')
  } finally {
    deptLoading.value = false
  }
}

const setTableSelection = () => {
  if (props.multiple && peopleTableRef.value) {
    // 多选模式:设置复选框选中状态
    peopleList.value.forEach(person => {
      const isSelected = tempSelectedPeople.value.some(p => p.userId === person.userId)
      peopleTableRef.value.toggleRowSelection(person, isSelected)
    })
  } else {
    // 单选模式:设置当前行
    if (tempSelectedPeople.value.length > 0) {
      const selected = tempSelectedPeople.value[0]
      const person = peopleList.value.find(p => p.userId === selected.userId)
      if (person) {
        peopleTableRef.value?.setCurrentRow(person)
      }
    }
  }
}

const loadPeopleList = async () => {
  try {
    peopleLoading.value = true

    // 设置查询参数
    const query = { ...peopleQuery }
    if (currentDeptId.value) {
      query.deptId = currentDeptId.value
    }
    if (props.deptId) {
      query.deptId = props.deptId
    }

    const res = await listUser(query)
    let list = res.rows || []

    // 排除指定的用户
    if (props.excludeUserIds.length > 0) {
      list = list.filter(user => !props.excludeUserIds.includes(user.userId))
    }

    peopleList.value = list
    peopleTotal.value = res.total || 0

    // 设置表格选中状态
    nextTick(() => {
      setTableSelection()
    })

  } catch (error) {
    console.error('加载人员列表失败:', error)
    ElMessage.error('加载人员信息失败')
  } finally {
    peopleLoading.value = false
  }
}

// 计算属性
const dialogTitle = computed(() => {
  return props.title || `人员选择${props.multiple ? '(多选)' : '(单选)'}`
})

const singleSelectedName = computed(() => {
  if (!props.multiple && selectedPeople.value.length > 0) {
    const person = selectedPeople.value[0]
    return person.nickName
  }
  return ''
})

// 监听器
watch(() => props.modelValue, (newVal) => {
  initSelectedPeople(newVal)
}, { immediate: true, deep: true })

watch(() => props.deptId, (newVal) => {
  if (newVal) {
    currentDeptId.value = newVal
    peopleQuery.deptId = newVal
  }
})

// 事件处理方法
const handleOpen = () => {
  if (props.disabled) return

  // 复制当前选中的人员到临时变量
  tempSelectedPeople.value = [...selectedPeople.value]
  dialogVisible.value = true

  // 加载数据
  loadDeptTree()
  loadPeopleList()
}

const handleDeptClick = (data: DeptVO) => {
  currentDeptId.value = data.id
  peopleQuery.deptId = data.id
  peopleQuery.pageNum = 1
  loadPeopleList()
}

const handleDeptFilter = (value: string) => {
  deptTreeRef.value?.filter(value)
}

const filterDeptNode = (value: string, data: any) => {
  if (!value) return true
  // 支持label或deptName属性
  const name = data.label || data.deptName || ''
  return name.includes(value)
}

const handlePeopleSearch = () => {
  peopleQuery.pageNum = 1
  if (peopleSearchText.value) {
    // 简单判断是否为手机号
    if (/^\d+$/.test(peopleSearchText.value)) {
      peopleQuery.phonenumber = peopleSearchText.value
      peopleQuery.nickName = ''
    } else {
      peopleQuery.nickName = peopleSearchText.value
      peopleQuery.phonenumber = ''
    }
  } else {
    peopleQuery.nickName = ''
    peopleQuery.phonenumber = ''
  }
  loadPeopleList()
}

const handleSingleSelect = (currentRow: UserVO) => {
  if (!props.multiple && currentRow) {
    tempSelectedPeople.value = [currentRow]
  }
}

const handleMultipleSelect = (selection: UserVO[]) => {
  if (props.multiple) {
    // 检查数量限制
    if (props.limit > 0 && selection.length > props.limit) {
      ElMessage.warning(`最多只能选择 ${props.limit} 个人员`)
      // 移除超出的选择
      nextTick(() => {
        const excess = selection.slice(props.limit)
        excess.forEach(person => {
          peopleTableRef.value.toggleRowSelection(person, false)
        })
      })
      return
    }

    tempSelectedPeople.value = selection
  }
}

const handlePeopleSizeChange = (size: number) => {
  peopleQuery.pageSize = size
  peopleQuery.pageNum = 1
  loadPeopleList()
}

const handlePeopleCurrentChange = (page: number) => {
  peopleQuery.pageNum = page
  loadPeopleList()
}

const handleRemoveFromSelected = (person: UserVO) => {
  const index = tempSelectedPeople.value.findIndex(p => p.userId === person.userId)
  if (index > -1) {
    tempSelectedPeople.value.splice(index, 1)

    // 同时更新表格选中状态
    nextTick(() => {
      peopleTableRef.value?.toggleRowSelection(person, false)
    })
  }
}

const handleRemovePerson = (person: UserVO) => {
  const index = selectedPeople.value.findIndex(p => p.userId === person.userId)
  if (index > -1) {
    selectedPeople.value.splice(index, 1)
    emitChange()
  }
}

const handleClearAll = () => {
  tempSelectedPeople.value = []

  // 清空表格选中状态
  nextTick(() => {
    if (props.multiple) {
      peopleTableRef.value?.clearSelection()
    } else {
      peopleTableRef.value?.setCurrentRow()
    }
  })
}

const handleConfirm = () => {
  if (!props.multiple && tempSelectedPeople.value.length === 0) {
    ElMessage.warning('请选择一个人员')
    return
  }

  selectedPeople.value = [...tempSelectedPeople.value]
  emitChange()
  dialogVisible.value = false
}

const handleCancel = () => {
  // 恢复原来的选择
  tempSelectedPeople.value = [...selectedPeople.value]
  dialogVisible.value = false
}

// 对外暴露的方法
defineExpose({
  open: handleOpen,
  clear: () => {
    selectedPeople.value = []
    emitChange()
  }
})
</script>

组件代码(style部分)

<style lang="scss" scoped>
.select-people-container {
  width: 100%;
}

.people-input {
  :deep(.el-input) {
    cursor: pointer;
    transition: all 0.3s ease;

    &:hover {
      border-color: var(--el-color-primary);
      box-shadow: 0 0 0 1px var(--el-color-primary-light-7);
    }

    input {
      cursor: pointer;
    }
  }

  .text-primary {
    color: var(--el-color-primary);
  }
}

.people-tags {
  .tags-container {
    min-height: 40px;
    border: 2px solid var(--el-border-color-light);
    border-radius: 8px;
    padding: 8px 40px 8px 12px;
    position: relative;
    cursor: pointer;
    transition: all 0.3s ease;
    background: var(--el-fill-color-blank);

    &:hover {
      border-color: var(--el-color-primary);
      box-shadow: 0 0 0 1px var(--el-color-primary-light-7);
    }

    &.disabled {
      background-color: var(--el-disabled-bg-color);
      cursor: not-allowed;
      opacity: 0.6;

      &:hover {
        border-color: var(--el-border-color-light);
        box-shadow: none;
      }
    }

    &.has-tags {
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
      align-items: center;
    }

    .placeholder {
      color: var(--el-text-color-placeholder);
      font-size: 14px;
      display: flex;
      align-items: center;
    }

    .suffix-icon {
      position: absolute;
      right: 12px;
      top: 50%;
      transform: translateY(-50%);
      color: var(--el-text-color-regular);
      transition: transform 0.3s ease;
    }

    &:hover .suffix-icon {
      transform: translateY(-50%) rotate(180deg);
    }
  }
}

:deep(.select-people-dialog-wrapper) {
  .el-dialog {
    border-radius: 16px;
    overflow: hidden;
  }

  .el-dialog__header {
    background: linear-gradient(135deg, var(--el-color-primary) 0%, var(--el-color-primary-light-3) 100%);
    color: white;
    padding: 20px 24px;
    margin: 0;

    .el-dialog__title {
      font-size: 18px;
      font-weight: 600;
    }

    .el-dialog__headerbtn {
      .el-dialog__close {
        color: white;
        font-size: 18px;

        &:hover {
          color: var(--el-color-primary-light-7);
        }
      }
    }
  }

  .el-dialog__body {
    padding: 24px;
    background: var(--el-bg-color-page);
  }

  .el-dialog__footer {
    padding: 20px 24px;
    background: var(--el-fill-color-blank);
    border-top: 1px solid var(--el-border-color-light);
  }
}

.select-people-dialog {
  .section-card {
    background: white;
    border-radius: 12px;
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
    overflow: hidden;
    transition: all 0.3s ease;
    height: 520px;
    display: flex;
    flex-direction: column;

    &:hover {
      box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
    }
  }

  .section-header {
    background: linear-gradient(135deg, var(--el-fill-color-light) 0%, var(--el-fill-color-blank) 100%);
    padding: 16px 20px;
    border-bottom: 1px solid var(--el-border-color-lighter);
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-shrink: 0;

    .header-left {
      display: flex;
      align-items: center;
    }

    .header-icon {
      font-size: 16px;
      color: var(--el-color-primary);
      margin-right: 8px;
    }

    .header-title {
      font-weight: 600;
      font-size: 15px;
      color: var(--el-text-color-primary);
    }

    .search-input {
      width: 220px;
    }

    .clear-btn {
      color: var(--el-color-danger);
      font-weight: 500;

      &:hover {
        background: var(--el-color-danger-light-9);
      }
    }
  }

  .search-wrapper {
    padding: 16px 20px;
    background: var(--el-fill-color-blank);
    flex-shrink: 0;

    .search-icon {
      color: var(--el-text-color-regular);
    }
  }

  .tree-wrapper {
    padding: 0 20px 20px;
    flex: 1;
    overflow: hidden;
    display: flex;
    flex-direction: column;

    :deep(.el-tree) {
      background: transparent;
      flex: 1;
      overflow: auto;

      .el-tree-node {
        margin-bottom: 2px;

        .el-tree-node__content {
          height: 36px;
          border-radius: 6px;
          transition: all 0.3s ease;

          &:hover {
            background: var(--el-color-primary-light-9);
          }

          &.is-current {
            background: var(--el-color-primary-light-8);
            color: var(--el-color-primary);
            font-weight: 500;
          }
        }
      }
    }

    .tree-node {
      display: flex;
      align-items: center;
      width: 100%;

      .node-icon {
        font-size: 14px;
        color: var(--el-color-primary);
        margin-right: 6px;
      }

      .node-label {
        font-size: 14px;
        color: var(--el-text-color-primary);
      }
    }
  }

  .table-wrapper {
    padding: 0 20px;
    flex: 1;
    overflow: hidden;
    display: flex;
    flex-direction: column;

    .people-table {
      border-radius: 8px;
      overflow: hidden;
      flex: 1;
      min-height: 0;

      :deep(.el-table) {
        height: 100% !important;
      }

      :deep(.el-table__body-wrapper) {
        overflow: auto;
      }

      :deep(.el-table__header) {
        background: var(--el-fill-color-light);

        th {
          background: var(--el-fill-color-light);
          color: var(--el-text-color-primary);
          font-weight: 600;
        }
      }

      :deep(.el-table__row) {
        transition: all 0.3s ease;

        &:hover {
          background: var(--el-color-primary-light-9);
        }
      }

      .user-info {
        display: flex;
        align-items: center;

        .user-name {
          font-weight: 500;
          color: var(--el-text-color-primary);
        }
      }

      .phone-info {
        display: flex;
        align-items: center;
        color: var(--el-text-color-regular);
        font-size: 13px;
      }
    }
  }

  .pagination-wrapper {
    padding: 16px 20px;
    display: flex;
    justify-content: center;
    background: var(--el-fill-color-blank);
    border-top: 1px solid var(--el-border-color-lighter);
    flex-shrink: 0;
  }

  .selected-list {
    padding: 20px;
    flex: 1;
    overflow: hidden;
    display: flex;
    flex-direction: column;

    :deep(.el-scrollbar) {
      flex: 1;
    }

    .selected-item {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 12px;
      background: var(--el-fill-color-blank);
      border: 1px solid var(--el-border-color-lighter);
      border-radius: 8px;
      margin-bottom: 8px;
      transition: all 0.3s ease;

      &:hover {
        border-color: var(--el-color-primary-light-5);
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
        transform: translateY(-1px);
      }

      .person-info {
        display: flex;
        align-items: center;
        flex: 1;

        .person-avatar {
          margin-right: 10px;
          background: var(--el-color-primary-light-8);
          color: var(--el-color-primary);
        }

        .person-details {
          .person-name {
            font-weight: 500;
            color: var(--el-text-color-primary);
            margin-bottom: 2px;
            font-size: 14px;
          }

          .person-dept {
            font-size: 12px;
            color: var(--el-text-color-secondary);
          }
        }
      }

      .remove-btn {
        color: var(--el-color-danger);
        opacity: 0.7;
        transition: all 0.3s ease;

        &:hover {
          opacity: 1;
          background: var(--el-color-danger-light-9);
        }
      }
    }

    .empty-selected {
      text-align: center;
      padding: 60px 20px;
      color: var(--el-text-color-placeholder);
      flex: 1;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;

      .empty-icon {
        font-size: 48px;
        color: var(--el-color-info-light-5);
        margin-bottom: 16px;
      }

      .empty-text {
        font-size: 16px;
        margin-bottom: 8px;
        color: var(--el-text-color-regular);
      }

      .empty-desc {
        font-size: 12px;
        color: var(--el-text-color-placeholder);
      }
    }
  }
}

.dialog-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;

  .footer-info {
    display: flex;
    align-items: center;
    color: var(--el-text-color-regular);
    font-size: 14px;

    .info-icon {
      margin-right: 6px;
      color: var(--el-color-primary);
    }
  }

  .footer-actions {
    display: flex;
    gap: 12px;

    .el-button {
      min-width: 80px;
      border-radius: 6px;
      font-weight: 500;
      transition: all 0.3s ease;

      &:not(.el-button--primary):hover {
        transform: translateY(-1px);
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
      }

      &.el-button--primary {
        background: linear-gradient(135deg, var(--el-color-primary) 0%, var(--el-color-primary-light-3) 100%);
        border: none;

        &:hover {
          transform: translateY(-1px);
          box-shadow: 0 4px 12px rgba(var(--el-color-primary), 0.4);
        }
      }
    }
  }
}

// 响应式设计
@media (max-width: 1200px) {
  :deep(.select-people-dialog-wrapper) {
    .el-dialog {
      width: 90% !important;
    }
  }
}

// 动画效果
.selected-item {
  animation: slideInRight 0.3s ease;
}

@keyframes slideInRight {
  from {
    opacity: 0;
    transform: translateX(20px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

// 滚动条美化
:deep(.el-scrollbar__bar) {
  &.is-vertical {
    .el-scrollbar__thumb {
      background: var(--el-color-primary-light-5);
      border-radius: 4px;
    }
  }
}
</style>

父组件调用方法(直接贴图吧):

本文章已经生成可运行项目
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Rookie WLK

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值