Merge pull request '🌶 feat(Web): 新增下拉表格选择器组件, 支持远程搜索、分页、自定义查询条件表单等功能' (#402) from jasondom/Admin.NET.Pro:v2-1 into v2
Reviewed-on: https://code.adminnet.top/Admin.NET/Admin.NET.Pro/pulls/402
This commit is contained in:
commit
3dbb59b4db
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