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>
|