🌶 feat(Web): 新增下拉表格选择器组件, 支持远程搜索、分页、自定义查询条件表单等功能
This commit is contained in:
parent
e9b051848d
commit
4f9b3610a3
402
Web/src/components/selector/pulldownSelecter.vue
Normal file
402
Web/src/components/selector/pulldownSelecter.vue
Normal file
@ -0,0 +1,402 @@
|
|||||||
|
<!-- 下拉选择组件,支持远程搜索、分页、自定义查询表单等功能 -->
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, onUnmounted, reactive, ref, watch } from 'vue';
|
||||||
|
import { debounce } from 'xe-utils';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
/**
|
||||||
|
* @example
|
||||||
|
* <pulldown-selecter
|
||||||
|
* v-model="state.ruleForm.userId"
|
||||||
|
* :defaultOptions="[ id: state.ruleForm.userId, realName: state.ruleForm.realName ]"
|
||||||
|
* :fetch-options="handleSysUserTable"
|
||||||
|
* :queryParams="state.queryParams"
|
||||||
|
* :dropdown-width="dropdownWidth"
|
||||||
|
* :placeholder="placeholder"
|
||||||
|
* :label-prop="realName"
|
||||||
|
* :value-prop="id"
|
||||||
|
* @@change="handelChange"
|
||||||
|
* allow-create
|
||||||
|
* filterable
|
||||||
|
* clearable
|
||||||
|
* class="w100"
|
||||||
|
* >
|
||||||
|
* <-- 查询条件表单插槽 -->
|
||||||
|
* <template #queryForm="{ query, handleQuery }">
|
||||||
|
* <el-col class="mb5" :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
|
||||||
|
* <el-form-item label="姓名" prop="realName">
|
||||||
|
* <el-input v-model="query.realName" placeholder="请输入姓名" class="w100" clearable @keydown.enter.native="handleQuery()" />
|
||||||
|
* </el-form-item>
|
||||||
|
* </el-col>
|
||||||
|
* <el-col class="mb5" :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
|
||||||
|
* <el-form-item label="电话" prop="phone">
|
||||||
|
* <el-input v-model="query.phone" placeholder="请输入电话" class="w100" clearable @keydown.enter.native="handleQuery()" />
|
||||||
|
* </el-form-item>
|
||||||
|
* </el-col>
|
||||||
|
* </template>
|
||||||
|
* <-- 表格列插槽 -->
|
||||||
|
* <template #columns>
|
||||||
|
* <el-table-column prop="realName" label="姓名" />
|
||||||
|
* <el-table-column prop="account" label="账号" width="160"/ >
|
||||||
|
* <el-table-column prop="idCardNo" label="身份证号" width="140" />
|
||||||
|
* <el-table-column prop="cardNo" label="卡号" width="140" />
|
||||||
|
* <el-table-column prop="gender" label="性别" width="85">
|
||||||
|
* <template #default="{ row }">
|
||||||
|
* <g-sys-dict v-model="row.gender" code="GenderEnum" />
|
||||||
|
* </template>
|
||||||
|
* </el-table-column>
|
||||||
|
* <el-table-column prop="birthday" label="生日">
|
||||||
|
* <template #default="{ row, $index }">
|
||||||
|
* {{ commonFun.dateFormatYMD(row, $index, row.birthday) }}
|
||||||
|
* </template>
|
||||||
|
* </el-table-column>
|
||||||
|
* <el-table-column prop="phone" label="联系电话" width="100" />
|
||||||
|
* </template>
|
||||||
|
* </pulldown-selecter>
|
||||||
|
*/
|
||||||
|
modelValue: [String, Number, null],
|
||||||
|
/**
|
||||||
|
* 获取表数据的异步方法
|
||||||
|
* @example
|
||||||
|
* // 异步方式示例
|
||||||
|
* const handleSysUserTable = (params: any) => {
|
||||||
|
* return getAPI(SysUserApi).apiSysUserPagePost(params);
|
||||||
|
* };
|
||||||
|
*/
|
||||||
|
fetchOptions: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 选中记录后绑定值属性名
|
||||||
|
*/
|
||||||
|
valueProp: {
|
||||||
|
type: String,
|
||||||
|
default: 'id',
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 选中记录后绑定值回显属性名
|
||||||
|
*/
|
||||||
|
labelProp: {
|
||||||
|
type: String,
|
||||||
|
default: 'name',
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 显示值格式化方法
|
||||||
|
* @example
|
||||||
|
* <pulldown-selecter
|
||||||
|
* v-model="state.ruleForm.userId"
|
||||||
|
* :labelFormat="(item: any) => `${item.realName}(${item.account})`"
|
||||||
|
* ....
|
||||||
|
*/
|
||||||
|
labelFormat: {
|
||||||
|
type: Function,
|
||||||
|
default: (item: any) => {},
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 默认查询条件值绑定的属性名
|
||||||
|
*/
|
||||||
|
keywordProp: {
|
||||||
|
type: String,
|
||||||
|
default: 'keyword',
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 下拉框宽度
|
||||||
|
*/
|
||||||
|
dropdownWidth: {
|
||||||
|
type: String,
|
||||||
|
default: '100%',
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 下拉框高度
|
||||||
|
*/
|
||||||
|
dropdownHeight: {
|
||||||
|
type: String,
|
||||||
|
default: '400px',
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 占位符
|
||||||
|
*/
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: '请输入关键词',
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 默认选项,用于回显
|
||||||
|
*/
|
||||||
|
defaultOptions: {
|
||||||
|
type: Array<any>,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 查询参数
|
||||||
|
*/
|
||||||
|
queryParams: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 是否显示分页
|
||||||
|
*/
|
||||||
|
pagination: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 查询字符输入字符醒目展示
|
||||||
|
*/
|
||||||
|
allowCreate: Boolean,
|
||||||
|
/**
|
||||||
|
* 是否禁用
|
||||||
|
*/
|
||||||
|
disabled: Boolean,
|
||||||
|
/**
|
||||||
|
* 是否多选
|
||||||
|
*/
|
||||||
|
multiple: Boolean,
|
||||||
|
/**
|
||||||
|
* 是否可清空
|
||||||
|
*/
|
||||||
|
clearable: Boolean,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableRef = ref();
|
||||||
|
const selectRef = ref();
|
||||||
|
const emit = defineEmits(['update:modelValue', 'change']);
|
||||||
|
const state = reactive({
|
||||||
|
selectedValues: '' as string | string[],
|
||||||
|
tableQuery: {
|
||||||
|
[props.keywordProp]: '',
|
||||||
|
page: 1,
|
||||||
|
pageSize: props.queryParams?.pageSize ?? 10,
|
||||||
|
},
|
||||||
|
tableData: {
|
||||||
|
items: [] as any[],
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
defaultOptions: props.defaultOptions,
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 当前选中行索引
|
||||||
|
const currentRowIndex = ref(-1);
|
||||||
|
const handleKeydown = (e: KeyboardEvent) => {
|
||||||
|
const tableData = state.tableData?.items || [];
|
||||||
|
if (!tableData.length) return;
|
||||||
|
|
||||||
|
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
const direction = e.key === 'ArrowDown' ? 1 : -1;
|
||||||
|
const newIndex = Math.max(0, Math.min(currentRowIndex.value + direction, tableData.length - 1));
|
||||||
|
|
||||||
|
if (newIndex !== currentRowIndex.value) {
|
||||||
|
currentRowIndex.value = newIndex;
|
||||||
|
const row = tableData[newIndex];
|
||||||
|
tableRef.value?.setCurrentRow(row);
|
||||||
|
}
|
||||||
|
} else if (e.key === 'Enter') {
|
||||||
|
const row = tableData[currentRowIndex.value];
|
||||||
|
handleChange(row); // 触发选中逻辑
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSizeChange = (pageSize: number) => {
|
||||||
|
state.tableQuery.pageSize = pageSize;
|
||||||
|
handleQuery();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCurrentChange = (page: number) => {
|
||||||
|
state.tableQuery.page = page;
|
||||||
|
handleQuery();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
selectRef.value?.inputRef?.addEventListener('keydown', handleKeydown);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
selectRef.value?.inputRef?.removeEventListener('keydown', handleKeydown);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 重置查询
|
||||||
|
const resetQuery = () => {
|
||||||
|
state.tableQuery = Object.assign({}, props.queryParams) as any;
|
||||||
|
state.tableQuery.page ??= 1;
|
||||||
|
handleQuery();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 远程查询方法
|
||||||
|
const remoteMethod = debounce((query) => {
|
||||||
|
if (query) {
|
||||||
|
state.loading = true;
|
||||||
|
if (typeof query === 'string') {
|
||||||
|
state.tableQuery[props.keywordProp] = query.trim();
|
||||||
|
} else {
|
||||||
|
state.tableQuery = query;
|
||||||
|
}
|
||||||
|
props.fetchOptions(Object.assign({}, props.queryParams, state.tableQuery)).then((res: any) => {
|
||||||
|
const result = res.data?.result;
|
||||||
|
state.tableData.items = result?.items ?? [];
|
||||||
|
state.tableData.total = result.total;
|
||||||
|
state.loading = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
state.tableData.items = [];
|
||||||
|
state.tableData.total = 0;
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
const handleQuery = () => {
|
||||||
|
remoteMethod(state.tableQuery);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 选择值改变事件
|
||||||
|
const handleChange = (row: any) => {
|
||||||
|
state.tableQuery = Object.assign(state.tableQuery, props.queryParams);
|
||||||
|
state.defaultOptions = props.defaultOptions ?? [];
|
||||||
|
state.defaultOptions.push(row);
|
||||||
|
|
||||||
|
if (props.multiple && !state.selectedValues) state.selectedValues = [];
|
||||||
|
if (typeof row[props.valueProp] === 'string') row[props.valueProp] = row[props.valueProp]?.trim();
|
||||||
|
state.selectedValues = props.multiple ? Array.from(new Set([...state.selectedValues, row[props.valueProp]])) : row[props.valueProp];
|
||||||
|
emit('update:modelValue', state.selectedValues);
|
||||||
|
emit('change', state.selectedValues, row);
|
||||||
|
if (!props.multiple || state.selectedValues) selectRef.value?.blur();
|
||||||
|
tableRef.value?.setCurrentRow(row);
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(val: any) => {
|
||||||
|
state.selectedValues = val;
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.defaultOptions,
|
||||||
|
(val: any) => {
|
||||||
|
state.defaultOptions = val;
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 设置默认值
|
||||||
|
const setDefaultOptions = (options: any[]) => {
|
||||||
|
state.defaultOptions = options;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 设置查询参数
|
||||||
|
const setQueryParams = (query: any, append: boolean = true) => {
|
||||||
|
if (!pagination) { // 不启用分页,则全量显示
|
||||||
|
query = Object.assign(query, {
|
||||||
|
page: 1,
|
||||||
|
pageSize: 99999,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
query = Object.assign({}, props.queryParams, query ?? {});
|
||||||
|
state.tableQuery = append ? Object.assign(state.tableQuery, query) : query;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 设置值
|
||||||
|
const setValue = (option: any | any[], row: any) => {
|
||||||
|
option = Array.isArray(option) ? option : [option];
|
||||||
|
state.tableData.total = option.length;
|
||||||
|
state.tableData.items = option;
|
||||||
|
state.selectedValues = props.multiple ? option.map((item: any) => item[props.valueProp]) : option[0][props.valueProp];
|
||||||
|
emit('update:modelValue', state.selectedValues);
|
||||||
|
emit('change', state.selectedValues, row);
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
setValue,
|
||||||
|
handleQuery,
|
||||||
|
setQueryParams,
|
||||||
|
setDefaultOptions,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<el-select
|
||||||
|
v-model="state.selectedValues"
|
||||||
|
:clearable="clearable"
|
||||||
|
:multiple="multiple"
|
||||||
|
:disabled="disabled"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:allow-create="allowCreate"
|
||||||
|
:remote-method="remoteMethod"
|
||||||
|
:default-first-option="allowCreate"
|
||||||
|
@visible-change="(val: boolean) => (val ? handleQuery() : null)"
|
||||||
|
popper-class="popper-class"
|
||||||
|
ref="selectRef"
|
||||||
|
remote-show-suffix
|
||||||
|
filterable
|
||||||
|
remote
|
||||||
|
>
|
||||||
|
<!-- 隐藏的选项,用于占位 -->
|
||||||
|
<el-option style="width: 0; height: 0" />
|
||||||
|
|
||||||
|
<!-- 默认选项,用于回显数据 -->
|
||||||
|
<el-option v-for="item in (state.defaultOptions ?? []).filter((e) => !state.tableData?.items?.find((e2) => e2[valueProp] === e[valueProp]))" :key="item[valueProp]" :label="labelFormat(item) || item[labelProp]" :value="item[valueProp]" style="width: 0; height: 0" />
|
||||||
|
|
||||||
|
<!-- 下拉框内容区域 -->
|
||||||
|
<div class="w100" v-loading="state.loading">
|
||||||
|
<el-form :model="state.tableQuery" v-if="$slots.queryForm" class="mg5 query-form" @click.stop>
|
||||||
|
<el-row :gutter="10">
|
||||||
|
<!-- 查询表单插槽内容 -->
|
||||||
|
<slot name="queryForm" :query="state.tableQuery" :handleQuery="handleQuery"></slot>
|
||||||
|
<!-- 查询和重置按钮 -->
|
||||||
|
<el-button-group style="position: absolute; right: 10px;">
|
||||||
|
<el-button type="primary" icon="ele-Search" @click="remoteMethod(state.tableQuery)"> 查询 </el-button>
|
||||||
|
<el-button icon="ele-Refresh" @click="resetQuery"> 重置 </el-button>
|
||||||
|
</el-button-group>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<!-- 数据表格 -->
|
||||||
|
<el-table
|
||||||
|
ref="tableRef"
|
||||||
|
@row-click="handleChange"
|
||||||
|
:data="state.tableData?.items ?? []"
|
||||||
|
:height="`calc(${dropdownHeight} - 175px${$slots.queryForm ? ' - 35px' : ''})`"
|
||||||
|
:style="{ width: dropdownWidth }"
|
||||||
|
highlight-current-row
|
||||||
|
>
|
||||||
|
<template #empty><el-empty :image-size="25" /></template>
|
||||||
|
<slot name="columns"></slot>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页组件 -->
|
||||||
|
<el-pagination
|
||||||
|
v-if="props.showPagination"
|
||||||
|
:disabled="state.loading"
|
||||||
|
:currentPage="state.tableQuery.page"
|
||||||
|
:page-size="state.tableQuery.pageSize"
|
||||||
|
:total="state.tableData.total"
|
||||||
|
:pager-count="4"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
layout="prev, pager, next"
|
||||||
|
size="small"
|
||||||
|
background
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.query-form {
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
:deep(.el-select-dropdown) {
|
||||||
|
.el-scrollbar > .el-scrollbar__bar {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.popper-class {
|
||||||
|
min-width: 400px !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in New Issue
Block a user