317 lines
9.0 KiB
Vue
Raw Normal View History

2026-06-26 17:55:13 +08:00
<template>
<div class="dropdown-table-select" :style="{ width: inputWidth }">
2026-07-10 17:40:50 +08:00
<el-dropdown ref="dropdownRef" trigger="manual" placement="bottom-start" style="width:100%" :teleported="false"
:popper-style="{ width: realtableWidth }">
2026-06-26 17:55:13 +08:00
<div class="input-wrap" style="width:100%">
<el-input v-model="inputText" :placeholder="placeholder" @keydown.enter.prevent="handleInputEnter" />
<span class="arrow-icon" @click.stop="toggleDropdown">
<el-icon>
<ArrowDown />
</el-icon>
</span>
</div>
<template #dropdown>
2026-07-10 17:40:50 +08:00
<div class="table-panel mytable-style" style="width:100%">
<!-- 搜索框-->
<div style="text-align: left;" v-if="props.showSearch">
<el-input v-model="searchKey" ref="searchInput" size="small" placeholder="快速搜索(支持简拼)" class="mb10" clearable
@clear="filterDataHandle" @input="filterDataHandle" @keydown="handleInputKeydown" />
</div>
<!-- 空数据提示 -->
<el-empty v-if="filterData.length === 0" description="没有匹配的数据" :image-size="100" />
<!-- 表格容器增加键盘监听 -->
<div v-else @keydown="handleTableKeydown" tabindex="0" style="outline: none;">
<vxe-table ref="tableRef" border resizable max-height="350px" v-loading="tableLoading"
:row-config="{ isHover: true, keyField: labelField, height: 30, isCurrent: true }" :data="filterData"
:current-row="currentRow" @cell-click="handleRowClick"
:tooltip-config="{ appendToBody: true, zIndex: 3000 }" :show-overflow="true"
:scroll-y="{ enabled: true, rSize: 30, height: '100%', oSize: 20, gt: 30, }">
<vxe-table-column v-for="field in props.columnList" :key="field.prop" :field="field.prop"
:title="field.label" align="center" :width="field.width" min-width="100" />
</vxe-table>
</div>
2026-06-26 17:55:13 +08:00
</div>
</template>
</el-dropdown>
</div>
</template>
2026-07-10 17:40:50 +08:00
2026-06-26 17:55:13 +08:00
<script setup lang="ts">
2026-07-10 17:40:50 +08:00
import { ArrowDown } from '@element-plus/icons-vue'
// 引入拼音首字母工具
import { getFirstLetter } from '@/utils/pinyin';
2026-06-26 17:55:13 +08:00
export interface TableColumnItem {
label: string
prop: string
width?: string
align?: 'left' | 'center' | 'right'
}
export type TableRowItem = Record<string, any>
2026-07-10 17:40:50 +08:00
2026-06-26 17:55:13 +08:00
const props = defineProps({
// 绑定值
modelValue: {
type: String,
default: ''
},
// 占位符
placeholder: {
type: String,
default: ''
},
2026-07-10 17:40:50 +08:00
// 绑定值字段
2026-06-26 17:55:13 +08:00
labelField: {
type: String,
default: 'billNo'
},
2026-07-10 17:40:50 +08:00
// 输入框宽度
2026-06-26 17:55:13 +08:00
inputWidth: {
type: String,
default: '100%'
},
2026-07-10 17:40:50 +08:00
// 表格宽度
tableWidth: {
2026-06-26 17:55:13 +08:00
type: String,
2026-07-10 17:40:50 +08:00
default: '100%'
2026-06-26 17:55:13 +08:00
},
2026-07-10 17:40:50 +08:00
// 表格高度
2026-06-26 17:55:13 +08:00
tableHeight: {
type: String,
default: '150px'
},
2026-07-10 17:40:50 +08:00
// 是否显示表头
showHeader: {
type: Boolean,
default: true
},
// 表格列配置
2026-06-26 17:55:13 +08:00
columnList: {
type: Array as () => TableColumnItem[],
required: true
},
2026-07-10 17:40:50 +08:00
// 表格原始数据
2026-06-26 17:55:13 +08:00
tableData: {
type: Array as () => TableRowItem[],
required: true
2026-07-10 17:40:50 +08:00
},
//是否展示搜索框
showSearch: {
type: Boolean,
default: true
2026-06-26 17:55:13 +08:00
}
})
2026-07-10 17:40:50 +08:00
2026-06-26 17:55:13 +08:00
const emit = defineEmits<{
'update:modelValue': [val: string]
change: [row: TableRowItem]
open: []
search: [val: string]
}>()
2026-07-10 17:40:50 +08:00
2026-06-26 17:55:13 +08:00
const bindField = props.labelField ?? 'billNo'
const dropdownRef = useTemplateRef('dropdownRef')
const showPanel = ref(false)
const inputText = ref('')
const tableLoading = ref(false)
2026-07-10 17:40:50 +08:00
const currentRow = ref<any>(null)
const tableRef = useTemplateRef('tableRef')
// 搜索相关变量
const searchKey = ref('');
const searchInput = ref();
// 预处理带拼音的完整原始数据
const processedTableData = ref<{ [key: string]: any; pinyinMap: Record<string, string> }[]>([]);
// 过滤后渲染表格的数据
const filterData = ref<{ [key: string]: any; pinyinMap: Record<string, string> }[]>([]);
const currentIndex = ref(-1);
const realtableWidth = computed(() => {
if (props.tableWidth === '100%') {
return props.inputWidth
}
return props.tableWidth
})
// 数据预处理生成拼音简拼
const processTableData = (data: TableRowItem[]) => {
const list = Array.isArray(data) ? data : [];
return list.map((item: any) => {
const pinyinMap: Record<string, string> = {};
// 所有列都生成拼音用于检索
props.columnList.forEach((field) => {
if (item[field.prop]) {
pinyinMap[field.prop] = getFirstLetter(item[field.prop]).toLowerCase();
}
});
return { ...item, pinyinMap };
});
};
//搜索过滤逻辑
const filterDataHandle = () => {
const key = searchKey.value.trim().toLowerCase();
if (!key) {
filterData.value = [...processedTableData.value];
currentIndex.value = -1;
return;
}
currentIndex.value = 0;
filterData.value = processedTableData.value.filter((item) => {
let match = false;
// 遍历所有列:文本匹配 + 拼音简拼匹配
props.columnList.forEach((field) => {
const text = item[field.prop]?.toString().toLowerCase() || '';
const pinyin = item.pinyinMap[field.prop] || '';
if (text.includes(key) || pinyin.includes(key)) {
match = true;
}
});
return match;
});
// 过滤完成自动选中第一行
if (filterData.value.length > 0) {
nextTick(() => {
tableRef.value?.setCurrentRow(filterData.value[0]);
tableRef.value?.scrollToRow(filterData.value[0]);
});
} else {
tableRef.value?.clearCurrentRow();
}
};
//搜索框上下回车键盘事件 ==========
const handleInputKeydown = (e: KeyboardEvent) => {
if (!['ArrowUp', 'ArrowDown', 'Enter'].includes(e.key)) return;
e.stopPropagation();
if (filterData.value.length > 0) {
nextTick(() => {
const tableBox = searchInput.value.$el.closest('.table-panel').querySelector('div[tabindex="0"]') as HTMLElement;
if (tableBox) {
tableBox.focus();
tableBox.dispatchEvent(new KeyboardEvent('keydown', { key: e.key }));
}
});
if (e.key === 'Enter') {
const targetRow = filterData.value[currentIndex.value];
targetRow && handleRowClick({ row: targetRow });
}
}
};
//表格容器上下键导航 ==========
const handleTableKeydown = (e: KeyboardEvent) => {
if (!['ArrowUp', 'ArrowDown', 'Enter'].includes(e.key)) return;
e.preventDefault();
const maxIdx = filterData.value.length - 1;
if (e.key === 'ArrowUp') {
currentIndex.value = Math.max(0, currentIndex.value - 1);
} else if (e.key === 'ArrowDown') {
currentIndex.value = Math.min(maxIdx, currentIndex.value + 1);
}
tableRef.value.setCurrentRow(filterData.value[currentIndex.value]);
tableRef.value.scrollToRow(filterData.value[currentIndex.value]);
if (e.key === 'Enter') {
const targetRow = filterData.value[currentIndex.value];
targetRow && handleRowClick({ row: targetRow });
}
};
// ========== 监听原始表格数据变化,重新生成拼音数据 ==========
watch(
() => props.tableData,
(newVal) => {
if (!Array.isArray(newVal)) return;
processedTableData.value = processTableData(newVal);
filterDataHandle();
},
{ deep: true, immediate: true }
)
// 原有监听逻辑不变
2026-06-26 17:55:13 +08:00
watch(
() => props.modelValue,
(val) => {
inputText.value = val ?? ''
},
{ immediate: true }
)
watch(inputText, (val) => {
emit('update:modelValue', val)
})
2026-07-10 17:40:50 +08:00
// 输入框回车(原有逻辑)
2026-06-26 17:55:13 +08:00
const handleInputEnter = (e: KeyboardEvent) => {
2026-07-10 17:40:50 +08:00
e.stopPropagation()
2026-06-26 17:55:13 +08:00
emit('search', inputText.value)
}
2026-07-10 17:40:50 +08:00
// 切换下拉框(修改:打开时聚焦搜索框、刷新过滤)
2026-06-26 17:55:13 +08:00
const toggleDropdown = () => {
if (!dropdownRef.value) return
showPanel.value = !showPanel.value
if (showPanel.value) {
dropdownRef.value.handleOpen()
tableLoading.value = true
emit('open')
2026-07-10 17:40:50 +08:00
searchKey.value = '';
filterDataHandle();
2026-06-26 17:55:13 +08:00
setTimeout(() => {
tableLoading.value = false
2026-07-10 17:40:50 +08:00
// 只有开启搜索框才聚焦
if (props.showSearch && searchInput.value) {
searchInput.value?.focus();
}
2026-06-26 17:55:13 +08:00
}, 300)
} else {
dropdownRef.value.handleClose()
}
}
2026-07-10 17:40:50 +08:00
// 表格行点击(原有逻辑完全不变)
const handleRowClick = (params: { row: TableRowItem }) => {
const text = params.row[bindField] ?? ''
2026-06-26 17:55:13 +08:00
inputText.value = text
emit('update:modelValue', text)
2026-07-10 17:40:50 +08:00
emit('change', params.row)
currentRow.value = params.row
2026-06-26 17:55:13 +08:00
dropdownRef.value?.handleClose()
showPanel.value = false
}
</script>
2026-07-10 17:40:50 +08:00
2026-06-26 17:55:13 +08:00
<style scoped lang="scss">
.dropdown-table-select {
.input-wrap {
position: relative;
}
.arrow-icon {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
color: #909399;
z-index: 999 !important;
pointer-events: auto !important;
font-size: 16px;
transition: transform 0.2s;
}
.table-panel {
padding: 12px;
2026-07-10 17:40:50 +08:00
width: 100%;
2026-06-26 17:55:13 +08:00
}
}
2026-07-10 17:40:50 +08:00
/* 只清除最小宽度限制,删除强制 width:100% 代码 */
:deep(.el-popper) {
min-width: 0 !important;
max-width: none !important;
}
/* 修复vxe-table滚动空白样式 */
:deep(.vxe-table--body) {
overflow-anchor: none;
}
2026-06-26 17:55:13 +08:00
</style>