315 lines
9.7 KiB
Vue
315 lines
9.7 KiB
Vue
|
|
<script setup lang="ts">
|
|||
|
|
import { computed, ref } 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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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" />
|
|||
|
|
</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" />
|
|||
|
|
</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>
|