336 lines
11 KiB
Vue
336 lines
11 KiB
Vue
<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';
|
||
/** date 字段的日期粒度 */
|
||
dateType?: 'date' | 'month';
|
||
/** dateRange 字段的日期范围粒度 */
|
||
dateRangeType?: 'daterange' | 'monthrange';
|
||
/** 日期字段提交格式 */
|
||
valueFormat?: string;
|
||
/** 占位列数,默认 1 */
|
||
span?: number;
|
||
/** select 类型的选项 */
|
||
options?: Option[];
|
||
/** dict 类型的字典编码 */
|
||
dictCode?: string;
|
||
/** dict 类型下拉项右侧追加字典 remark 释义(如优先级 "P0 → 紧急") */
|
||
showRemark?: boolean;
|
||
/** 占位提示文本 */
|
||
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="field.dateType || 'date'"
|
||
:placeholder="field.placeholder"
|
||
clearable
|
||
:disabled="props.disabled"
|
||
:value-format="field.valueFormat || '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="field.dateRangeType || 'daterange'"
|
||
:placeholder="field.placeholder"
|
||
clearable
|
||
:disabled="props.disabled"
|
||
:value-format="field.valueFormat || 'YYYY-MM-DD'"
|
||
:start-placeholder="field.dateRangeType === 'monthrange' ? '开始月份' : '开始日期'"
|
||
:end-placeholder="field.dateRangeType === 'monthrange' ? '结束月份' : '结束日期'"
|
||
@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"
|
||
:show-remark="field.showRemark"
|
||
@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="field.dateType || 'date'"
|
||
:placeholder="field.placeholder"
|
||
clearable
|
||
:disabled="props.disabled"
|
||
:value-format="field.valueFormat || '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="field.dateRangeType || 'daterange'"
|
||
:placeholder="field.placeholder"
|
||
clearable
|
||
:disabled="props.disabled"
|
||
:value-format="field.valueFormat || 'YYYY-MM-DD'"
|
||
:start-placeholder="field.dateRangeType === 'monthrange' ? '开始月份' : '开始日期'"
|
||
:end-placeholder="field.dateRangeType === 'monthrange' ? '结束月份' : '结束日期'"
|
||
@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"
|
||
:show-remark="field.showRemark"
|
||
@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>
|