Files
school-developer/src/components/SearchForm/index2.vue
guochunsi 45d2613b3a a
2026-02-03 11:55:41 +08:00

322 lines
7.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="search-form__wrap" :class="{ 'search-form__wrap--with-title': filterTitle }">
<div class="search-form__bar">
<div class="search-form-container">
<el-form :model="formModel" ref="formRef" :inline="true" @keyup.enter="handleKeyupEnter" :label-width="labelWidth">
<!-- 筛选 + 展开更多 放在同一表单项内保证垂直对齐 -->
<el-form-item v-if="filterTitle || hasCollapsibleItems" class="search-form__left-group">
<div class="search-form__left-inner">
<span v-if="filterTitle" class="search-form__title">
<el-icon class="search-form__title-icon"><Search /></el-icon>
{{ filterTitle }}
</span>
<el-button
v-if="hasCollapsibleItems"
link
type="primary"
class="toggle-btn"
@click="toggleExpand"
>
<el-icon style="margin-right: 4px">
<ArrowUp v-if="isExpanded" />
<ArrowDown v-else />
</el-icon>
{{ isExpanded ? '收起' : '展开更多' }}
</el-button>
</div>
</el-form-item>
<slot :visible="true" :expanded="isExpanded"></slot>
<!-- 收起时查询/重置在首行 -->
<div v-show="!isExpanded" class="actions-outside">
<slot name="actions"></slot>
</div>
<!-- 折叠区展开时内部再放一份查询/重置位置在更多筛选项之后 -->
<div v-if="hasCollapsibleItems" class="collapse-grid" :class="{ 'is-expanded': isExpanded }">
<div class="collapse-grid-inner">
<div class="collapse-grid-cell">
<slot :visible="false" :expanded="isExpanded" @has-content="hasCollapsibleContent = true"></slot>
<div v-show="isExpanded" class="actions-inside">
<slot name="actions"></slot>
</div>
</div>
</div>
</div>
<div v-show="false" ref="detectionWrapperRef" class="detection-wrapper">
<slot :visible="false" :expanded="true"></slot>
</div>
</el-form>
</div>
</div>
</div>
</template>
<script setup lang="ts" name="search-form">
import { ref, watch, computed, onMounted, nextTick } from 'vue';
import { ArrowUp, ArrowDown, Search } from '@element-plus/icons-vue';
const props = defineProps({
/**
* 筛选标题(如「筛选」),传入时在表单左侧显示标题+竖线,与表单项同一行
*/
filterTitle: {
type: String,
default: '',
},
/**
* 表单数据模型
*/
model: {
type: Object,
required: true,
},
/**
* 是否默认展开折叠区域
*/
defaultExpanded: {
type: Boolean,
default: false,
},
/**
* 是否显示展开/折叠按钮
* 如果不设置将自动检测是否有可折叠内容visible=false 的插槽)
*/
showCollapse: {
type: Boolean,
default: undefined,
},
/**
* 表单项标签宽度
*/
labelWidth: {
type: String,
default: '',
},
});
const emit = defineEmits(['expand-change', 'keyup-enter']);
const formModel = props.model;
const { filterTitle } = props;
const formRef = ref();
const isExpanded = ref(props.defaultExpanded);
const hasCollapsibleContent = ref(false);
const detectionWrapperRef = ref<HTMLElement | null>(null);
// 检测是否有可折叠内容
const checkCollapsibleContent = () => {
// 如果 showCollapse 明确指定,则使用指定值
if (props.showCollapse !== undefined) {
hasCollapsibleContent.value = props.showCollapse
return
}
// 否则,通过检查隐藏的检测元素是否有内容来判断
// 需要等待 DOM 渲染完成,可能需要多次尝试以确保数据加载完成
let retryCount = 0
const maxRetries = 5
const check = () => {
nextTick(() => {
setTimeout(() => {
if (detectionWrapperRef.value) {
// 检查检测元素是否有子元素(排除文本节点)
// 检查是否有 el-form-item 元素(因为表单项会被渲染为 el-form-item
const hasContent = detectionWrapperRef.value.children.length > 0 ||
detectionWrapperRef.value.querySelector('.el-form-item') !== null ||
(!!detectionWrapperRef.value.textContent && detectionWrapperRef.value.textContent.trim() !== '')
if (hasContent || retryCount >= maxRetries) {
hasCollapsibleContent.value = hasContent
} else {
// 如果还没检测到内容且未达到最大重试次数,继续重试
retryCount++
setTimeout(check, 100)
}
} else {
if (retryCount < maxRetries) {
retryCount++
setTimeout(check, 100)
} else {
hasCollapsibleContent.value = false
}
}
}, 50)
})
}
check()
}
// 是否有需要折叠的项
const hasCollapsibleItems = computed(() => {
if (props.showCollapse !== undefined) {
return props.showCollapse
}
return hasCollapsibleContent.value
});
// 处理回车键
const handleKeyupEnter = () => {
emit('keyup-enter');
};
// 切换展开/收起状态
const toggleExpand = () => {
isExpanded.value = !isExpanded.value;
emit('expand-change', isExpanded.value);
};
// 监听 defaultExpanded 变化
watch(
() => props.defaultExpanded,
(newVal) => {
isExpanded.value = newVal;
}
);
// 监听 showCollapse 变化
watch(
() => props.showCollapse,
() => {
checkCollapsibleContent()
}
);
// 初始化时检测
onMounted(() => {
checkCollapsibleContent()
});
// 暴露方法供外部调用
defineExpose({
formRef,
toggleExpand,
expand: () => {
isExpanded.value = true;
emit('expand-change', true);
},
collapse: () => {
isExpanded.value = false;
emit('expand-change', false);
},
});
</script>
<style lang="scss" scoped>
.search-form__bar {
display: flex;
align-items: flex-start;
flex-wrap: wrap;
gap: 4px 0;
min-height: 32px;
}
.search-form__left-group {
:deep(.el-form-item__label) {
display: none;
}
:deep(.el-form-item__content) {
margin-left: 0 !important;
}
margin-bottom: 18px;
}
.search-form__left-inner {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0;
}
.search-form__title {
position: relative;
display: inline-flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
padding-right: 12px;
margin-right: 12px;
height: 32px;
line-height: 32px;
font-size: 14px;
font-weight: 500;
color: var(--el-text-color-secondary);
cursor: default;
user-select: none;
&::after {
content: '';
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
width: 1px;
height: 14px;
background: var(--el-border-color);
}
}
.search-form__title-icon {
font-size: 15px;
color: var(--el-text-color-secondary);
}
.search-form-container {
flex: 1;
min-width: 0;
:deep(.el-form--inline) {
align-items: center;
}
.search-form__left-inner :deep(.toggle-btn) {
font-size: 14px;
font-weight: 500;
height: 32px;
display: inline-flex;
align-items: center;
border-radius: 6px;
}
:deep(.el-form-item:not(:has(.el-button))) {
margin-right: 15px;
}
/* grid 0fr→1fr 高度过渡ease-out 收尾不弹跳 */
.collapse-grid {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.26s cubic-bezier(0.33, 1, 0.68, 1);
contain: layout;
&.is-expanded {
grid-template-rows: 1fr;
}
}
.collapse-grid-inner {
overflow: hidden;
min-height: 0;
backface-visibility: hidden;
}
.collapse-grid-cell {
overflow: hidden;
min-height: 0;
opacity: 0;
/* 收起时立即隐藏,不出现输入框慢慢消失 */
transition: opacity 0s;
}
.collapse-grid.is-expanded .collapse-grid-cell {
opacity: 1;
/* 仅展开时淡入 */
transition: opacity 0.12s ease-out;
}
.actions-outside,
.actions-inside {
display: contents;
:deep(.el-form-item) {
margin-right: 0!important;
}
}
}
</style>