2025-10-24 11:25:23 +08:00

347 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!--
* SelectTable 下拉表格组件
* @author: w
* @since: 2025-9-28
* SelectTable.vue
*
* 属性(带有[Required]为必填属性)
* data[Required]: 双向绑定数据
* fields[Required]: 表格列对象
* tableData[Required]: 表格数据对象
* value: 双向绑定数据的值(为空时data属性为表格当前行数据对象)
* label: 选择框显示的文本内容(为空时分别查看value和objKey是否为空,若value和objKey都有值,则value优先级大于objKey)
* objKey: 对象key(绑定值的唯一标识,绑定值为对象时必填) 数据回显对应值
* placeholder: 选择框占位文本
* size: 组件大小
* border: 表格是否带有边框
-->
<template>
<div class="select-table-container">
<el-select ref="selectTable" v-model="selectShowValue" :placeholder="props.placeholder" :size="props.size"
:style="{ width: props.width }" @visible-change="visibleChange" @clear="clearHandle" :clearable="props.clearable"
:disabled="disabled">
<template #empty>
<div class="select-table-dropdown">
<div style="text-align: left;">
<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 @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>
</div>
</template>
</el-select>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted, watch, nextTick, toRaw } from "vue";
// @ts-ignore
import { getFirstLetter } from '@/api/pinyin.js';
import { ElEmpty } from 'element-plus';
// @ts-ignore
import { comDict } from '@/utils/dict'
interface Field {
prop: string;
label: string;
width?: number;
enablePinyinSearch?: boolean; //是否参与拼音搜索
}
interface Props {
data: any;
fields: Field[];
tableData?: object[];
label?: string;
value?: string;
objKey?: string;
isHighlight?: boolean;
size?: string;
placeholder?: string;
border?: boolean;
width?: string | number;
disabled?: boolean;
dictType?: string;
clearable?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
placeholder: "请选择",
size: "default",
isHighlight: true,
value: undefined,
label: undefined,
border: false,
width: "100%",
dictType: '',
clearable: true
});
interface Emits {
(e: "update:data", val: any): void;
(e: "getDataValue", val: any): void;
}
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> }[]>([]);
const currentIndex = ref(-1);
// 字典数据存储
const dictData = ref<Record<string, any[]>>({});
// 预处理数据:生成拼音信息
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();
}
// 处理其他指定字段拼音
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) {
filterDataHandle()
setTimeout(() => {
searchInput.value.focus();
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);
}
}, 10);
}
};
// 数据回显处理
const handleDataEcho = () => {
if (props.data === null || props.data === undefined) {
selectShowValue.value = '';
emits('update:data', null);
// emits('getDataValue', null);
} else {
// 查找匹配的行数据
let row: any = []
if (props.dictType) {
row = dictData.value[props.dictType]?.find((item: any) => props.objKey && item[props.objKey] === props.data)
} else if (props.tableData) {
row = 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] : '')) : '';
}
};
// 初始化数据
const initData = async () => {
if (props.tableData) {
// 如果传入tableData,直接使用
processedTableData.value = processTableData(props.tableData);
filterData.value = [...processedTableData.value];
} else if (props.dictType) {
// 如果传入dictType,通过comDict获取数据
const dictRefs = await comDict(props.dictType);
// // 从 ref 中获取实际数据
dictData.value = {
[props.dictType]: toRaw(dictRefs[props.dictType].value) || [],
};
processedTableData.value = processTableData(dictData.value[props.dictType]);
filterData.value = [...processedTableData.value];
}
// 处理数据回显
handleDataEcho();
};
// 初始化数据
initData();
onMounted(() => {
});
// 监听表格数据和字典类型变化
watch(() => props.tableData, (newVal) => {
if (newVal) {
processedTableData.value = processTableData(newVal);
handleDataEcho();
}
}, { deep: true });
// 监听绑定值数据变化,重新处理
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);
}
}
// 搜索过滤逻辑
const filterDataHandle = () => {
const key = searchKey.value.trim().toLowerCase();
if (!key) {
// 空搜索时显示全部预处理数据
filterData.value = [...processedTableData.value];
return;
} else {
currentIndex.value = 0
}
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;
});
if (filterData.value.length > 0) {
tableRef.value.setScrollTop(0)
tableRef.value.setCurrentRow(filterData.value[0]);
}
};
const handleRowChange = (val: any) => {
emits("update:data", props.value ? val[props.value] : val);
setLabel(val);
};
const setLabel = (val: any) => {
if (props.label) {
selectShowValue.value = val[props.label];
} else if (props.value) {
selectShowValue.value = val[props.value];
}
if (props.objKey) {
// 返回值objKey对应的对象数据
emits('getDataValue', val);
}
selectTable.value.blur();
};
// 清空值
const clearHandle = () => {
selectShowValue.value = '';
emits("update:data", null);
emits('getDataValue', null);
};
</script>
<style scoped>
.select-table-container {
width: 100%;
}
.select-table-dropdown {
padding: 8px;
}
:deep(.el-table .el-table__cell) {
padding: 0;
}
</style>