<template>
<div style="position: relative; padding: 20px;">
<!-- 表格组件,绑定过滤后的数据 filteredData -->
<el-table
:data="filteredData"
row-key="date"
style="width: 100%"
height="320"
>
<!-- 普通列,显示日期,支持排序 -->
<el-table-column prop="date" label="Date" sortable width="180" />
<!-- 自定义表头列,筛选“Name” -->
<el-table-column prop="name" label="Name" width="220">
<!-- 自定义表头插槽 -->
<template #header>
<span>Name</span>
<!-- 点击触发下拉筛选菜单 -->
<span
ref="dropdownTrigger"
@click="toggleDropdown"
tabindex="0"
@keydown.enter.prevent="toggleDropdown"
aria-haspopup="listbox"
:aria-expanded="dropdownVisible.toString()"
class="custom-dropdown-trigger"
>
<el-icon><arrow-down /></el-icon>
</span>
<!-- 显示已选项数量 -->
<small v-if="selectedNames.length" style="margin-left: 6px; color: #409eff;">
{{ selectedNames.length }} 项已选
</small>
</template>
<!-- 默认单元格内容插槽,显示行的 name -->
<template #default="{ row }">
{{ row.name }}
</template>
</el-table-column>
<!-- 普通列,地址,使用 formatter 格式化显示 -->
<el-table-column prop="address" label="Address" :formatter="formatter" />
<!-- 使用 Element Plus 自带过滤功能的列 -->
<el-table-column
prop="tag"
label="Tag"
width="100"
:filters="[
{ text: 'Home', value: 'Home' },
{ text: 'Office', value: 'Office' }
]"
:filter-method="filterTag"
filter-placement="bottom-end"
>
<!-- 自定义单元格内容,标签显示不同颜色 -->
<template #default="scope">
<el-tag
:type="scope.row.tag === 'Home' ? 'primary' : 'success'"
disable-transitions
>{{ scope.row.tag }}</el-tag>
</template>
</el-table-column>
</el-table>
<!-- 下拉筛选菜单使用 teleport 渲染到 body 避免遮挡 -->
<teleport to="body">
<transition name="fade">
<div
v-if="dropdownVisible"
class="custom-dropdown-menu"
:style="dropdownStyle"
role="listbox"
@click.stop
>
<!-- 搜索输入框,双向绑定 search -->
<el-input
v-model="search"
placeholder="输入搜索关键词"
size="small"
clearable
@clear="search = ''"
style="margin: 6px 5px; width: 200px;"
/>
<!-- 下拉列表 -->
<ul class="dropdown-list">
<!-- 无匹配提示 -->
<li v-if="filteredNames.length === 0" class="no-data">无匹配项</li>
<!-- 选项列表 -->
<li
v-for="name in filteredNames"
:key="name"
class="dropdown-item"
>
<!-- 多选复选框,控制临时选中状态 -->
<el-checkbox
:model-value="tempSelectedNames.includes(name)"
@change="(checked, ev) => {
ev.stopPropagation()
toggleSelect(name)
}"
>
{{ name }}
</el-checkbox>
</li>
</ul>
<!-- 底部操作按钮 -->
<div class="dropdown-footer">
<!-- 清除选择 -->
<el-button type="text" size="small" @click="clearSelection">清除筛选</el-button>
<!-- 应用选择 -->
<el-button type="primary" size="small" @click="applySelection">确定</el-button>
</div>
</div>
</transition>
</teleport>
</div>
</template>
<script setup>
import { ref, computed, nextTick, onMounted, onBeforeUnmount } from 'vue'
import { ArrowDown } from '@element-plus/icons-vue'
// 表格数据源
const tableData = ref([
{ date: '2016-05-03', name: 'Tom', address: 'No. 189, Grove St, Los Angeles', tag: 'Home' },
{ date: '2016-05-02', name: 'Jerry', address: 'No. 189, Grove St, Los Angeles', tag: 'Office' },
{ date: '2016-05-04', name: 'Spike', address: 'No. 189, Grove St, Los Angeles', tag: 'Home' },
{ date: '2016-05-01', name: 'Tom', address: 'No. 189, Grove St, Los Angeles', tag: 'Office' }
])
// 下拉触发元素引用
const dropdownTrigger = ref(null)
// 下拉菜单显示状态
const dropdownVisible = ref(false)
// 下拉菜单样式(位置固定)
const dropdownStyle = ref({
position: 'fixed',
left: '0px',
top: '0px',
minWidth: '220px',
zIndex: 3000
})
// 搜索关键词
const search = ref('')
// 已确定的筛选名字列表
const selectedNames = ref([])
// 临时选择的名字列表(用于下拉中选择,未确认前不影响表格筛选)
const tempSelectedNames = ref([])
// 从表格数据中提取所有唯一的名字,排序后返回,用于展示下拉选项
const nameOptions = computed(() => {
const set = new Set()
tableData.value.forEach(item => set.add(item.name))
return Array.from(set).sort()
})
// 根据搜索关键词过滤名字选项
const filteredNames = computed(() => {
if (!search.value.trim()) return nameOptions.value
const kw = search.value.trim().toLowerCase()
return nameOptions.value.filter(name => name.toLowerCase().includes(kw))
})
// 根据已选的名字筛选表格数据
const filteredData = computed(() => {
let data = tableData.value
if (selectedNames.value.length > 0) {
data = data.filter(item => selectedNames.value.includes(item.name))
}
return data
})
// 切换下拉菜单显示隐藏
function toggleDropdown() {
dropdownVisible.value = !dropdownVisible.value
if (dropdownVisible.value) {
// 打开时同步临时选中数据,重置搜索框
tempSelectedNames.value = [...selectedNames.value]
search.value = ''
nextTick(() => updateDropdownPosition()) // 更新下拉位置
}
}
// 根据触发元素位置更新下拉菜单的定位
function updateDropdownPosition() {
if (!dropdownTrigger.value) return
const rect = dropdownTrigger.value.getBoundingClientRect()
dropdownStyle.value = {
position: 'fixed',
left: `${rect.left}px`,
top: `${rect.bottom + 6}px`, // 触发元素底部向下偏移6px
minWidth: '220px',
maxHeight: '300px',
overflowY: 'auto',
zIndex: 3000
}
}
// 切换某个名字的选中状态(临时选中)
function toggleSelect(name) {
const index = tempSelectedNames.value.indexOf(name)
if (index > -1) {
tempSelectedNames.value.splice(index, 1)
} else {
tempSelectedNames.value.push(name)
}
}
// 清除临时选择和搜索关键词
function clearSelection() {
tempSelectedNames.value = []
search.value = ''
}
// 确认筛选,应用临时选择到正式选择,并关闭下拉
function applySelection() {
const kw = search.value.trim().toLowerCase()
// 如果搜索框中有关键词,自动将匹配项加入临时选择
if (kw) {
const matched = nameOptions.value.filter(name => name.toLowerCase().includes(kw))
const merged = new Set([...tempSelectedNames.value, ...matched])
tempSelectedNames.value = Array.from(merged)
}
selectedNames.value = [...tempSelectedNames.value]
dropdownVisible.value = false
}
// 地址列格式化函数,直接返回地址文本
function formatter(row) {
return row.address
}
// tag 列过滤方法,只显示选中标签的数据
function filterTag(value, row) {
return row.tag === value
}
// 点击页面其他位置时关闭下拉菜单
function onClickOutside(event) {
if (
dropdownTrigger.value &&
!dropdownTrigger.value.contains(event.target) &&
!event.target.closest('.custom-dropdown-menu')
) {
dropdownVisible.value = false
}
}
// 组件挂载时注册事件监听
onMounted(() => {
document.addEventListener('click', onClickOutside)
// 页面滚动时更新下拉位置
window.addEventListener('scroll', () => {
if (dropdownVisible.value) updateDropdownPosition()
}, true)
})
// 组件卸载时移除事件监听
onBeforeUnmount(() => {
document.removeEventListener('click', onClickOutside)
window.removeEventListener('scroll', () => {
if (dropdownVisible.value) updateDropdownPosition()
}, true)
})
</script>
<style scoped>
/* 下拉触发图标样式 */
.custom-dropdown-trigger {
font-size: 14px;
color: #909399;
vertical-align: middle;
transition: color 0.3s;
display: inline-flex;
align-items: center;
margin-left: 6px;
cursor: pointer;
user-select: none;
}
.custom-dropdown-trigger:hover {
color: #409eff;
}
/* 下拉菜单整体样式 */
.custom-dropdown-menu {
padding: 8px 0 4px 0;
background: #fff;
border: 1px solid #ebeef5;
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 0.1);
border-radius: 4px;
max-height: 300px;
overflow-y: auto;
min-width: 220px;
font-size: 14px;
}
/* 下拉列表 */
.dropdown-list {
list-style: none;
margin: 0;
padding: 0 10px;
max-height: 220px;
overflow-y: auto;
}
/* 下拉项 */
.dropdown-item {
padding: 4px 0;
cursor: pointer;
display: flex;
align-items: center;
}
.dropdown-item:hover {
background-color: #f5f7fa;
}
/* 无匹配项提示 */
.no-data {
padding: 8px 10px;
color: #999;
text-align: center;
}
/* 下拉底部操作栏 */
.dropdown-footer {
padding: 8px 10px 4px;
border-top: 1px solid #ebeef5;
display: flex;
justify-content: space-between;
}
/* 淡入淡出动画 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>