Files
cn-rdms-web/src/components/custom/table-search-fields.vue

326 lines
10 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import { computed, ref } from 'vue';
import type { VNode } from 'vue';
import { ElButton, ElCol, ElDatePicker, ElFormItem, ElInput, ElOption, ElRow, ElSelect } from 'element-plus';
import DictSelect from './dict-select.vue';
defineOptions({ name: 'TableSearchFields' });
interface Option {
label: string;
value: string | number;
}
export interface SearchField {
/** 字段键名 */
key: string;
/** 字段标签 */
label: string;
/** 字段类型 */
type: 'input' | 'select' | 'date' | 'dateRange' | 'dict';
/** 占位列数,默认 1 */
span?: number;
/** select 类型的选项 */
options?: Option[];
/** dict 类型的字典编码 */
dictCode?: string;
/** 占位提示文本 */
placeholder?: string;
/** select 类型的自定义选项渲染函数 */
renderOption?: (option: Option) => VNode | VNode[] | string;
}
interface Props {
/** 绑定表单数据对象 */
modelValue: Record<string, any>;
/** 查询字段定义数组 */
fields: SearchField[];
/** 每行格子数(按钮占 1 格) */
columns: number;
/** 表单标签宽度 */
labelWidth?: string | number;
/** 格子间距 */
gutter?: number;
/** 是否禁用 */
disabled?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
labelWidth: 80,
gutter: 16,
disabled: false
});
interface Emits {
(e: 'search'): void;
(e: 'reset'): void;
}
const emit = defineEmits<Emits>();
// 折叠/展开状态
const expanded = ref(false);
// 是否需要折叠(字段数 > columns - 1
const needsCollapse = computed(() => props.fields.length > props.columns - 1);
// 第一行字段数(留一个位置给按钮)
const firstRowFieldCount = computed(() => props.columns - 1);
// 计算第一行字段
const firstRowFields = computed(() => {
if (expanded.value || !needsCollapse.value) {
return props.fields.slice(0, firstRowFieldCount.value);
}
return props.fields.slice(0, firstRowFieldCount.value);
});
// 计算后续行字段(用于展开后显示)
const remainingFields = computed(() => {
if (expanded.value || !needsCollapse.value) {
return props.fields.slice(firstRowFieldCount.value);
}
return [];
});
const firstRowButtonSpan = computed(() => {
return Math.floor(24 / props.columns);
});
// 计算第一行字段的 span字段和按钮区保持同一列宽
const firstRowFieldSpan = computed(() => {
return firstRowButtonSpan.value;
});
// 计算每个字段的 span用于后续行
const fieldSpan = computed(() => {
return Math.floor(24 / props.columns);
});
// 字段不足时补足首行空列,确保按钮区始终落在 columns 定义的最后一格。
const firstRowPlaceholderSpan = computed(() => {
const emptySlotCount = Math.max(props.columns - 1 - firstRowFields.value.length, 0);
return emptySlotCount * fieldSpan.value;
});
function handleToggle() {
expanded.value = !expanded.value;
}
function handleReset() {
emit('reset');
}
function handleSearch() {
emit('search');
}
</script>
<!-- eslint-disable vue/no-mutating-props -->
<template>
<ElCard class="card-wrapper">
<ElForm :model="props.modelValue" :label-width="props.labelWidth" @submit.prevent @keyup.enter="handleSearch">
<!-- 第一行fields + 按钮 -->
<ElRow :gutter="props.gutter">
<ElCol
v-for="field in firstRowFields"
:key="field.key"
class="table-search-fields__col"
:span="firstRowFieldSpan"
>
<ElFormItem :label="field.label">
<ElInput
v-if="field.type === 'input'"
:model-value="props.modelValue[field.key]"
:placeholder="field.placeholder"
clearable
:disabled="props.disabled"
@update:model-value="val => (props.modelValue[field.key] = val)"
/>
<ElSelect
v-else-if="field.type === 'select'"
:model-value="props.modelValue[field.key]"
:placeholder="field.placeholder"
clearable
:disabled="props.disabled"
@update:model-value="val => (props.modelValue[field.key] = val)"
>
<ElOption v-for="opt in field.options" :key="opt.value" :label="opt.label" :value="opt.value">
<template v-if="field.renderOption" #default>
<component :is="field.renderOption(opt)" />
</template>
</ElOption>
</ElSelect>
<ElDatePicker
v-else-if="field.type === 'date'"
:model-value="props.modelValue[field.key]"
type="date"
:placeholder="field.placeholder"
clearable
:disabled="props.disabled"
value-format="YYYY-MM-DD"
@update:model-value="val => (props.modelValue[field.key] = val)"
/>
<ElDatePicker
v-else-if="field.type === 'dateRange'"
:model-value="props.modelValue[field.key]"
type="daterange"
:placeholder="field.placeholder"
clearable
:disabled="props.disabled"
value-format="YYYY-MM-DD"
start-placeholder="开始日期"
end-placeholder="结束日期"
@update:model-value="val => (props.modelValue[field.key] = val)"
/>
<DictSelect
v-else-if="field.type === 'dict'"
:model-value="props.modelValue[field.key]"
:dict-code="field.dictCode!"
:placeholder="field.placeholder"
:disabled="props.disabled"
@update:model-value="val => (props.modelValue[field.key] = val)"
/>
</ElFormItem>
</ElCol>
<ElCol
v-if="firstRowPlaceholderSpan > 0"
class="table-search-fields__col table-search-fields__placeholder-col"
:span="firstRowPlaceholderSpan"
aria-hidden="true"
/>
<!-- 按钮区域 -->
<ElCol class="table-search-fields__col table-search-fields__action-col" :span="firstRowButtonSpan">
<ElFormItem class="table-search-fields__actions" label-width="0">
<ElButton
v-if="needsCollapse"
circle
:title="expanded ? '收起' : '展开'"
:aria-label="expanded ? '收起查询条件' : '展开查询条件'"
:disabled="props.disabled"
@click="handleToggle"
>
<icon-mdi-chevron-double-up v-if="expanded" />
<icon-mdi-chevron-double-down v-else />
</ElButton>
<ElButton :disabled="props.disabled" @click="handleReset">
<template #icon>
<icon-ic-round-refresh class="text-icon" />
</template>
重置
</ElButton>
<ElButton type="primary" :disabled="props.disabled" @click="handleSearch">
<template #icon>
<icon-ic-round-search class="text-icon" />
</template>
查询
</ElButton>
</ElFormItem>
</ElCol>
</ElRow>
<!-- 展开后的后续行 -->
<ElRow v-if="expanded && remainingFields.length > 0" :gutter="props.gutter">
<ElCol v-for="field in remainingFields" :key="field.key" class="table-search-fields__col" :span="fieldSpan">
<ElFormItem :label="field.label">
<ElInput
v-if="field.type === 'input'"
:model-value="props.modelValue[field.key]"
:placeholder="field.placeholder"
clearable
:disabled="props.disabled"
@update:model-value="val => (props.modelValue[field.key] = val)"
/>
<ElSelect
v-else-if="field.type === 'select'"
:model-value="props.modelValue[field.key]"
:placeholder="field.placeholder"
clearable
:disabled="props.disabled"
@update:model-value="val => (props.modelValue[field.key] = val)"
>
<ElOption v-for="opt in field.options" :key="opt.value" :label="opt.label" :value="opt.value">
<template v-if="field.renderOption" #default>
<component :is="field.renderOption(opt)" />
</template>
</ElOption>
</ElSelect>
<ElDatePicker
v-else-if="field.type === 'date'"
:model-value="props.modelValue[field.key]"
type="date"
:placeholder="field.placeholder"
clearable
:disabled="props.disabled"
value-format="YYYY-MM-DD"
@update:model-value="val => (props.modelValue[field.key] = val)"
/>
<ElDatePicker
v-else-if="field.type === 'dateRange'"
:model-value="props.modelValue[field.key]"
type="daterange"
:placeholder="field.placeholder"
clearable
:disabled="props.disabled"
value-format="YYYY-MM-DD"
start-placeholder="开始日期"
end-placeholder="结束日期"
@update:model-value="val => (props.modelValue[field.key] = val)"
/>
<DictSelect
v-else-if="field.type === 'dict'"
:model-value="props.modelValue[field.key]"
:dict-code="field.dictCode!"
:placeholder="field.placeholder"
:disabled="props.disabled"
@update:model-value="val => (props.modelValue[field.key] = val)"
/>
</ElFormItem>
</ElCol>
</ElRow>
</ElForm>
</ElCard>
</template>
<style scoped lang="scss">
:deep(.el-form-item) {
display: flex;
align-items: center;
}
.table-search-fields__col {
min-width: 0;
}
.table-search-fields__placeholder-col {
pointer-events: none;
}
.table-search-fields__actions {
:deep(.el-form-item__content) {
display: flex;
flex-wrap: nowrap;
justify-content: flex-end;
gap: 8px;
min-width: 0;
}
:deep(.el-button + .el-button) {
margin-left: 0;
}
}
:deep(.el-form-item__content) {
min-width: 0;
}
:deep(.el-input),
:deep(.el-select),
:deep(.el-date-editor) {
width: 100%;
min-width: 0;
}
</style>