2025-09-26 17:26:06 +08:00
|
|
|
|
<!--
|
|
|
|
|
|
* SelectTable 下拉表格组件
|
2025-09-29 17:30:02 +08:00
|
|
|
|
* @author: w
|
|
|
|
|
|
* @since: 2025-9-28
|
2025-09-26 17:26:06 +08:00
|
|
|
|
* SelectTable.vue
|
|
|
|
|
|
*
|
|
|
|
|
|
* 属性(带有[Required]为必填属性)
|
|
|
|
|
|
* data[Required]: 双向绑定数据
|
|
|
|
|
|
* fields[Required]: 表格列对象
|
|
|
|
|
|
* tableData[Required]: 表格数据对象
|
|
|
|
|
|
* value: 双向绑定数据的值(为空时data属性为表格当前行数据对象)
|
|
|
|
|
|
* label: 选择框显示的文本内容(为空时分别查看value和objKey是否为空,若value和objKey都有值,则value优先级大于objKey)
|
2025-10-13 17:28:33 +08:00
|
|
|
|
* objKey: 对象key(绑定值的唯一标识,绑定值为对象时必填) 数据回显对应值
|
2025-09-26 17:26:06 +08:00
|
|
|
|
* placeholder: 选择框占位文本
|
|
|
|
|
|
* size: 组件大小
|
|
|
|
|
|
* border: 表格是否带有边框
|
|
|
|
|
|
-->
|
|
|
|
|
|
<template>
|
2025-10-13 17:28:33 +08:00
|
|
|
|
<div class="select-table-container">
|
2025-09-29 17:30:02 +08:00
|
|
|
|
<el-select ref="selectTable" v-model="selectShowValue" :placeholder="props.placeholder" :size="props.size"
|
|
|
|
|
|
:style="{ width: props.width }" @visible-change="visibleChange" @clear="clearHandle" clearable>
|
2025-09-26 17:26:06 +08:00
|
|
|
|
<template #empty>
|
2025-09-29 17:30:02 +08:00
|
|
|
|
<div class="select-table-dropdown">
|
|
|
|
|
|
<div style="text-align: left;">
|
|
|
|
|
|
<el-input v-model="searchKey" ref="searchInput" size="small" placeholder="快速搜索(支持简拼)" class="mb10" clearable
|
2025-10-13 17:28:33 +08:00
|
|
|
|
@clear="filterDataHandle" @input="filterDataHandle" @keydown="handleInputKeydown" />
|
2025-09-29 17:30:02 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<!-- 数据为空时显示空状态提示 -->
|
|
|
|
|
|
<el-empty v-if="filterData.length === 0" description="没有匹配的数据" :image-size="100" />
|
2025-10-13 17:28:33 +08:00
|
|
|
|
<div @keydown="handleKeydown" v-else tabindex="0" style="outline: none;">
|
|
|
|
|
|
<!-- 数据存在时显示表格 -->
|
|
|
|
|
|
<el-table ref="tableRef" :data="filterData" :highlight-current-row="props.isHighlight" style="width: 100%"
|
|
|
|
|
|
:border="props.border" @row-click="handleRowChange" max-height="350px" :row-style="{ height: '25px' }">
|
|
|
|
|
|
<el-table-column v-for="field in props.fields" :prop="field.prop" :label="field.label"
|
|
|
|
|
|
:width="field.width" show-overflow-tooltip align="center" />
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</div>
|
2025-09-29 17:30:02 +08:00
|
|
|
|
</div>
|
2025-09-26 17:26:06 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2025-09-29 17:30:02 +08:00
|
|
|
|
import { reactive, ref, onMounted, watch, nextTick } from "vue";
|
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
|
import { getFirstLetter } from '@/api/pinyin.js';
|
|
|
|
|
|
import { ElEmpty } from 'element-plus';
|
2025-09-26 17:26:06 +08:00
|
|
|
|
|
|
|
|
|
|
interface Field {
|
|
|
|
|
|
prop: string;
|
|
|
|
|
|
label: string;
|
2025-10-13 17:28:33 +08:00
|
|
|
|
width?: number;
|
2025-09-29 17:30:02 +08:00
|
|
|
|
enablePinyinSearch?: boolean; //是否参与拼音搜索
|
2025-09-26 17:26:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
|
data: any;
|
|
|
|
|
|
fields: Field[];
|
|
|
|
|
|
tableData: object[];
|
|
|
|
|
|
label?: string;
|
|
|
|
|
|
value?: string;
|
|
|
|
|
|
objKey?: string;
|
|
|
|
|
|
isHighlight?: boolean;
|
|
|
|
|
|
size?: string;
|
|
|
|
|
|
placeholder?: string;
|
|
|
|
|
|
border?: boolean;
|
2025-09-29 17:30:02 +08:00
|
|
|
|
width?: string | number;
|
2025-09-26 17:26:06 +08:00
|
|
|
|
}
|
2025-09-29 17:30:02 +08:00
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
2025-09-26 17:26:06 +08:00
|
|
|
|
placeholder: "请选择",
|
|
|
|
|
|
size: "default",
|
|
|
|
|
|
isHighlight: true,
|
|
|
|
|
|
value: undefined,
|
|
|
|
|
|
label: undefined,
|
|
|
|
|
|
border: false,
|
2025-09-29 17:30:02 +08:00
|
|
|
|
width: "100%",
|
2025-09-26 17:26:06 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
interface Emits {
|
|
|
|
|
|
(e: "update:data", val: any): void;
|
2025-09-29 17:30:02 +08:00
|
|
|
|
(e: "getDataValue", val: any): void;
|
2025-09-26 17:26:06 +08:00
|
|
|
|
}
|
2025-09-29 17:30:02 +08:00
|
|
|
|
const emits = defineEmits<Emits>();
|
|
|
|
|
|
const searchKey = ref('');
|
|
|
|
|
|
const tableRef = ref();
|
|
|
|
|
|
const selectTable = ref();
|
|
|
|
|
|
const selectShowValue = ref('');
|
|
|
|
|
|
const searchInput = ref();
|
|
|
|
|
|
const filterData = ref<{ [key: string]: any; pinyinMap: Record<string, string> }[]>([]);
|
|
|
|
|
|
// 存储预处理后的带拼音数据
|
|
|
|
|
|
const processedTableData = ref<{ [key: string]: any; pinyinMap: Record<string, string> }[]>([]);
|
2025-10-13 17:28:33 +08:00
|
|
|
|
const currentIndex = ref(-1);
|
2025-09-29 17:30:02 +08:00
|
|
|
|
// 预处理数据:生成拼音信息
|
|
|
|
|
|
const processTableData = (data: object[]) => {
|
|
|
|
|
|
return data.map((item: any) => {
|
|
|
|
|
|
const pinyinMap: Record<string, string> = {};
|
|
|
|
|
|
// 处理label字段拼音
|
|
|
|
|
|
if (item[props.label!]) {
|
|
|
|
|
|
pinyinMap.label = getFirstLetter(item[props.label!]).toLowerCase();
|
|
|
|
|
|
}
|
|
|
|
|
|
// 处理value字段拼音
|
|
|
|
|
|
if (props.value && item[props.value]) {
|
|
|
|
|
|
pinyinMap.value = getFirstLetter(String(item[props.value])).toLowerCase();
|
2025-09-26 17:26:06 +08:00
|
|
|
|
}
|
2025-09-29 17:30:02 +08:00
|
|
|
|
// 处理其他指定字段拼音
|
|
|
|
|
|
props.fields.forEach(field => {
|
|
|
|
|
|
if (field.enablePinyinSearch && item[field.prop]) {
|
|
|
|
|
|
pinyinMap[field.prop] = getFirstLetter(item[field.prop]).toLowerCase();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
return { ...item, pinyinMap };
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 隐藏显示处理
|
|
|
|
|
|
const visibleChange = (val: any) => {
|
|
|
|
|
|
searchKey.value = '';
|
|
|
|
|
|
if (val) {
|
2025-10-13 17:28:33 +08:00
|
|
|
|
|
2025-09-29 17:30:02 +08:00
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
searchInput.value.focus();
|
2025-10-13 17:28:33 +08:00
|
|
|
|
|
|
|
|
|
|
currentIndex.value = filterData.value.findIndex(item => item[props.value!] === props.data);
|
|
|
|
|
|
if (currentIndex.value >= 0) {
|
|
|
|
|
|
tableRef.value.setCurrentRow(filterData.value[currentIndex.value]);
|
|
|
|
|
|
tableRef.value.setScrollTop(currentIndex.value * 25);
|
|
|
|
|
|
}
|
2025-09-29 17:30:02 +08:00
|
|
|
|
});
|
2025-09-26 17:26:06 +08:00
|
|
|
|
}
|
2025-09-29 17:30:02 +08:00
|
|
|
|
filterDataHandle()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化数据
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
processedTableData.value = processTableData(props.tableData);
|
|
|
|
|
|
filterData.value = [...processedTableData.value];
|
2025-09-26 17:26:06 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-29 17:30:02 +08:00
|
|
|
|
// 监听表格数据变化,重新处理
|
|
|
|
|
|
watch(() => props.tableData, (newVal) => {
|
|
|
|
|
|
processedTableData.value = processTableData(newVal);
|
|
|
|
|
|
// 数据回显
|
|
|
|
|
|
handleDataEcho();
|
|
|
|
|
|
}, { deep: true });
|
2025-09-26 17:26:06 +08:00
|
|
|
|
|
2025-10-13 17:28:33 +08:00
|
|
|
|
|
|
|
|
|
|
// 监听绑定值数据变化,重新处理
|
|
|
|
|
|
watch(() => props.data, (newVal) => {
|
|
|
|
|
|
// 数据回显
|
|
|
|
|
|
handleDataEcho();
|
|
|
|
|
|
}, { deep: true });
|
|
|
|
|
|
|
|
|
|
|
|
// 输入框键盘事件处理(专门处理上下键)
|
|
|
|
|
|
const handleInputKeydown = (e: KeyboardEvent) => {
|
|
|
|
|
|
// 仅处理上下方向键
|
|
|
|
|
|
if (e.key !== "ArrowUp" && e.key !== "ArrowDown") return;
|
|
|
|
|
|
|
|
|
|
|
|
// 阻止事件冒泡到输入框父元素
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
|
|
|
|
|
|
// 如果有筛选数据,转移焦点到表格容器
|
|
|
|
|
|
if (filterData.value.length > 0) {
|
|
|
|
|
|
// 筛选后让表格容器获得焦点
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
// 通过组件内部的 DOM 结构关系查找当前实例的表格容器 避免找不到table焦点
|
|
|
|
|
|
const dropdown = searchInput.value.$el.closest('.select-table-dropdown');
|
|
|
|
|
|
const tableContainer = dropdown?.querySelector('div[tabindex="0"]') as HTMLElement;
|
|
|
|
|
|
if (tableContainer) {
|
|
|
|
|
|
tableContainer.focus();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 手动触发一次表格容器的键盘事件
|
|
|
|
|
|
const event = new KeyboardEvent('keydown', { key: e.key });
|
|
|
|
|
|
tableContainer?.dispatchEvent(event);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 监听table键盘事件(核心)
|
|
|
|
|
|
const handleKeydown = (e: any) => {
|
|
|
|
|
|
// console.log('e==>', e);
|
|
|
|
|
|
// 仅处理方向键和回车键
|
|
|
|
|
|
if (!['ArrowUp', 'ArrowDown', 'Enter'].includes(e.key)) return;
|
|
|
|
|
|
// 阻止默认行为(如页面滚动)
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
|
|
// 根据方向键更新“当前选中行索引”
|
|
|
|
|
|
const maxIndex = filterData.value.length - 1; // 末行索引
|
|
|
|
|
|
if (e.key === "ArrowUp") {
|
|
|
|
|
|
// 按↑:索引减1,最小为0(首行)
|
|
|
|
|
|
currentIndex.value = Math.max(0, currentIndex.value - 1);
|
|
|
|
|
|
} else if (e.key === "ArrowDown") {
|
|
|
|
|
|
// 按↓:索引加1,最大为 maxIndex(末行)
|
|
|
|
|
|
currentIndex.value = Math.min(maxIndex, currentIndex.value + 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tableRef.value.setCurrentRow(filterData.value[currentIndex.value]);
|
|
|
|
|
|
|
|
|
|
|
|
// 滚动到当前行
|
|
|
|
|
|
tableRef.value.setScrollTop(currentIndex.value * 25);
|
|
|
|
|
|
|
|
|
|
|
|
// 回车确认
|
|
|
|
|
|
if (e.key === "Enter") {
|
|
|
|
|
|
const currentRow = filterData.value[currentIndex.value];
|
|
|
|
|
|
handleRowChange(currentRow);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-29 17:30:02 +08:00
|
|
|
|
// 数据回显处理
|
|
|
|
|
|
const handleDataEcho = () => {
|
2025-10-13 17:28:33 +08:00
|
|
|
|
if (props.data === null || props.data === undefined) {
|
|
|
|
|
|
selectShowValue.value = '';
|
|
|
|
|
|
emits('update:data', null);
|
|
|
|
|
|
// emits('getDataValue', null);
|
2025-09-26 17:26:06 +08:00
|
|
|
|
|
2025-10-13 17:28:33 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
// 查找匹配的行数据
|
|
|
|
|
|
const row: any = props.tableData.find((item: any) => props.objKey && item[props.objKey] === props.data)
|
|
|
|
|
|
selectShowValue.value = row ? (props.label ? row[props.label] : (props.value ? row[props.value] : '')) : '';
|
|
|
|
|
|
}
|
2025-09-29 17:30:02 +08:00
|
|
|
|
|
2025-10-13 17:28:33 +08:00
|
|
|
|
};
|
2025-09-29 17:30:02 +08:00
|
|
|
|
|
|
|
|
|
|
// 搜索过滤逻辑
|
|
|
|
|
|
const filterDataHandle = () => {
|
|
|
|
|
|
const key = searchKey.value.trim().toLowerCase();
|
|
|
|
|
|
|
|
|
|
|
|
if (!key) {
|
|
|
|
|
|
// 空搜索时显示全部预处理数据
|
|
|
|
|
|
filterData.value = [...processedTableData.value];
|
|
|
|
|
|
return;
|
2025-10-13 17:28:33 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
currentIndex.value = 0
|
2025-09-26 17:26:06 +08:00
|
|
|
|
}
|
2025-09-29 17:30:02 +08:00
|
|
|
|
|
|
|
|
|
|
filterData.value = processedTableData.value.filter((item: any) => {
|
|
|
|
|
|
// 1. 原文本匹配
|
|
|
|
|
|
const matchLabel = props.label && item[props.label]?.toString().toLowerCase().includes(key);
|
|
|
|
|
|
const matchValue = props.value && item[props.value]?.toString().toLowerCase().includes(key);
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 拼音匹配
|
|
|
|
|
|
const matchLabelPinyin = props.label && item.pinyinMap.label?.includes(key);
|
|
|
|
|
|
const matchValuePinyin = props.value && item.pinyinMap.value?.includes(key);
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 其他字段匹配
|
|
|
|
|
|
let matchOtherField = false;
|
|
|
|
|
|
props.fields.forEach(field => {
|
|
|
|
|
|
if (field.enablePinyinSearch) {
|
|
|
|
|
|
const textMatch = item[field.prop]?.toString().toLowerCase().includes(key);
|
|
|
|
|
|
const pinyinMatch = item.pinyinMap[field.prop]?.includes(key);
|
|
|
|
|
|
if (textMatch || pinyinMatch) {
|
|
|
|
|
|
matchOtherField = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return matchLabel || matchValue || matchLabelPinyin || matchValuePinyin || matchOtherField;
|
|
|
|
|
|
});
|
2025-10-13 17:28:33 +08:00
|
|
|
|
|
|
|
|
|
|
if (filterData.value.length > 0) {
|
|
|
|
|
|
tableRef.value.setScrollTop(0)
|
|
|
|
|
|
tableRef.value.setCurrentRow(filterData.value[0]);
|
|
|
|
|
|
}
|
2025-09-29 17:30:02 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleRowChange = (val: any) => {
|
|
|
|
|
|
emits("update:data", props.value ? val[props.value] : val);
|
2025-10-13 17:28:33 +08:00
|
|
|
|
setLabel(val);
|
2025-09-26 17:26:06 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const setLabel = (val: any) => {
|
2025-09-29 17:30:02 +08:00
|
|
|
|
if (props.label) {
|
|
|
|
|
|
selectShowValue.value = val[props.label];
|
|
|
|
|
|
} else if (props.value) {
|
|
|
|
|
|
selectShowValue.value = val[props.value];
|
|
|
|
|
|
}
|
|
|
|
|
|
if (props.objKey) {
|
2025-10-13 17:28:33 +08:00
|
|
|
|
// 返回值objKey对应的对象数据
|
|
|
|
|
|
emits('getDataValue', val);
|
2025-09-26 17:26:06 +08:00
|
|
|
|
}
|
2025-09-29 17:30:02 +08:00
|
|
|
|
selectTable.value.blur();
|
|
|
|
|
|
};
|
|
|
|
|
|
// 清空值
|
|
|
|
|
|
const clearHandle = () => {
|
|
|
|
|
|
selectShowValue.value = '';
|
|
|
|
|
|
emits("update:data", null);
|
|
|
|
|
|
emits('getDataValue', null);
|
2025-09-26 17:26:06 +08:00
|
|
|
|
};
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
2025-09-29 17:30:02 +08:00
|
|
|
|
<style scoped>
|
2025-10-13 17:28:33 +08:00
|
|
|
|
.select-table-container {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-29 17:30:02 +08:00
|
|
|
|
.select-table-dropdown {
|
|
|
|
|
|
padding: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-table .el-table__cell) {
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|