Files
school-developer/src/components/SearchForm/index.vue
guochunsi 94b5b8b7ab a
2026-02-05 15:35:46 +08:00

318 lines
7.6 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 && props.showFilterTitle }">
<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 && props.showFilterTitle) || hasCollapsibleItems" class="search-form__left-group">
<div class="search-form__left-inner">
<span v-if="filterTitle && props.showFilterTitle" class="search-form__title" :style="{ marginRight: hasCollapsibleItems ? '12px' : '0' }">
<el-icon class="search-form__title-icon"><Search /></el-icon>
{{ filterTitle }}
</span>
<el-button
v-if="hasCollapsibleItems"
type="primary"
class="toggle-btn"
@click="toggleExpand"
round
>
<el-icon
class="toggle-btn__icon"
:class="{ 'toggle-btn__icon--expanded': isExpanded }"
>
<CaretBottom />
</el-icon>
<span class="toggle-btn__text">
{{ isExpanded ? '收起筛选' : '更多筛选' }}
</span>
</el-button>
</div>
</el-form-item>
<slot :visible="true" :expanded="isExpanded"></slot>
<!-- 折叠区收起时隐藏display:contents 不破坏表单行内流 -->
<div v-if="hasCollapsibleItems" v-show="isExpanded" class="collapse-visibility">
<slot :visible="false" :expanded="isExpanded" @has-content="hasCollapsibleContent = true"></slot>
</div>
<div class="search-form__actions">
<slot name="actions" :expanded="isExpanded"></slot>
</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 { CaretBottom, Search } from '@element-plus/icons-vue';
const props = defineProps({
/**
* 筛选标题(如「筛选」),传入时在表单左侧显示标题+竖线,与表单项同一行
*/
filterTitle: {
type: String,
default: '筛选',
},
/**
* 是否显示筛选标题文案(仅控制左侧「筛选」文字和图标)
*/
showFilterTitle: {
type: Boolean,
default: false,
},
/**
* 表单数据模型
*/
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;
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__icon) {
transition: transform 0.25s ease;
}
.search-form__left-inner :deep(.toggle-btn__icon--expanded) {
transform: rotate(180deg);
}
.search-form__left-inner :deep(.toggle-btn) {
padding: 0 12px;
display: inline-flex;
align-items: center;
font-weight: 500;
// 默认态直接使用 hover 的视觉样式
color: var(--el-color-primary);
background-color: var(--el-fill-color-lighter);
border: 1px solid var(--el-color-primary-light-7);
transition: color 0.2s ease, background-color 0.2s ease, border-color 0.2s ease;
&:hover {
background-color: var(--el-fill-color-light);
border-color: var(--el-color-primary);
color: var(--el-color-primary);
}
}
:deep(.el-form-item:not(:has(.el-button))) {
margin-right: 15px;
}
/* 折叠区收起时隐藏,展开时 display:contents 不破坏表单行内流 */
.collapse-visibility {
display: contents;
}
/* 查询/重置按钮区:表单项 margin-right 为 0 */
.search-form__actions {
display: contents;
:deep(.el-form-item) {
margin-right: 0 !important;
}
}
}
</style>