Files
EasyFlow/easyflow-ui-admin/app/src/components/headerSearch/HeaderSearch.vue
陈子默 c2ed5a24a3 fix: 保留工作流列表返回状态
- 记录页码、分类和搜索条件并同步到路由

- 从工作流设计页返回时恢复原列表位置
2026-07-31 09:51:21 +08:00

305 lines
6.3 KiB
Vue

<script setup>
import { computed, ref, watch } from 'vue';
import { ArrowDown, Search } from '@element-plus/icons-vue';
import {
ElButton,
ElDropdown,
ElDropdownItem,
ElDropdownMenu,
ElIcon,
ElInput,
} from 'element-plus';
import { hasPermission } from '#/api/common/hasPermission.ts';
import { $t } from '#/locales';
const props = defineProps({
buttons: {
type: Array,
default: () => [],
validator: (value) => {
return value.every((button) => {
return (
typeof button.text === 'string' &&
(button.key || typeof button.key === 'string')
);
});
},
},
maxVisibleButtons: {
type: Number,
default: 3,
},
initialValue: {
type: String,
default: '',
},
searchPlaceholder: {
type: String,
default: $t('common.searchPlaceholder'),
},
});
const emit = defineEmits(['search', 'buttonClick', 'reset']);
const searchValue = ref(props.initialValue);
watch(
() => props.initialValue,
(value) => {
searchValue.value = value;
},
);
const filterButtonsByPermission = (buttons) => {
return buttons.filter((button) => {
return !button.permission || hasPermission([button.permission]);
});
};
const visibleButtons = computed(() => {
return filterButtonsByPermission(props.buttons).slice(
0,
props.maxVisibleButtons,
);
});
const dropdownButtons = computed(() => {
return filterButtonsByPermission(props.buttons).slice(
props.maxVisibleButtons,
);
});
const emitButtonEvent = (payload) => {
emit('buttonClick', payload);
};
const handleSearch = () => {
emit('search', searchValue.value);
};
const handleReset = () => {
searchValue.value = '';
emit('reset');
emit('search', '');
};
defineExpose({
reset: handleReset,
});
const handleButtonClick = (button) => {
emitButtonEvent({
type: 'button',
key: button.key,
button,
data: button.data,
});
};
const handleDropdownClick = (button) => {
emitButtonEvent({
type: 'dropdown',
key: button.key,
button,
data: button.data,
});
};
</script>
<template>
<div class="custom-header">
<div class="header-left">
<div class="search-group">
<ElInput
v-model="searchValue"
:placeholder="searchPlaceholder"
class="search-input"
clearable
@keyup.enter="handleSearch"
>
<template #prefix>
<ElIcon class="search-prefix">
<Search />
</ElIcon>
</template>
</ElInput>
<ElButton type="primary" auto-insert-space @click="handleSearch">
{{ $t('button.query') }}
</ElButton>
<ElButton auto-insert-space @click="handleReset">
{{ $t('button.reset') }}
</ElButton>
<div v-if="$slots.middle" class="search-middle">
<slot name="middle"></slot>
</div>
</div>
</div>
<div
v-if="visibleButtons.length > 0 || dropdownButtons.length > 0"
class="header-right"
>
<template
v-for="(button, index) in visibleButtons"
:key="button.key || index"
>
<ElButton
:type="button.type || 'default'"
:icon="button.icon"
:disabled="button.disabled"
:loading="button.loading"
v-access:code="button.permission"
@click="handleButtonClick(button)"
>
{{ button.text }}
</ElButton>
</template>
<ElDropdown
v-if="dropdownButtons.length > 0"
@command="handleDropdownClick"
>
<ElButton>
{{ $t('button.more') }}
<ElIcon class="el-icon--right"><ArrowDown /></ElIcon>
</ElButton>
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem
v-for="button in dropdownButtons"
:key="button.key"
:command="button"
:disabled="button.disabled"
>
<ElIcon v-if="button.icon">
<component :is="button.icon" />
</ElIcon>
<span class="dropdown-label">{{ button.text }}</span>
</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
</div>
</div>
</template>
<style scoped>
.custom-header {
display: flex;
gap: 16px;
align-items: center;
justify-content: space-between;
width: 100%;
}
.header-left,
.header-right {
display: flex;
align-items: center;
min-width: 0;
}
.header-left {
flex: 1;
}
.search-group {
display: flex;
flex: 1;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
.search-middle {
display: flex;
align-items: center;
min-width: 0;
}
.search-input {
width: min(360px, 100%);
}
.search-input :deep(.el-input__wrapper) {
min-height: 40px;
background: hsl(var(--surface-contrast-soft) / 92%);
border-radius: 14px;
box-shadow:
inset 0 1px 0 hsl(var(--glass-border) / 44%),
0 10px 24px -24px hsl(var(--foreground) / 24%);
}
.search-input :deep(.el-input__wrapper:hover),
.search-input :deep(.el-input__wrapper.is-focus) {
background: hsl(var(--surface-subtle) / 98%);
box-shadow:
inset 0 1px 0 hsl(var(--glass-border) / 56%),
0 0 0 3px hsl(var(--primary) / 8%),
0 12px 24px -24px hsl(var(--foreground) / 24%);
}
.search-prefix {
color: hsl(var(--text-muted));
}
.header-right {
flex-shrink: 0;
flex-wrap: wrap;
gap: 12px;
justify-content: flex-end;
}
.header-right :deep(.el-button),
.search-group :deep(.el-button) {
min-width: 64px;
height: 32px;
min-height: 32px;
padding-inline: 18px;
border-color: transparent;
border-radius: 10px;
box-shadow: none;
}
.header-right :deep(.el-button:not(.el-button--primary)),
.search-group :deep(.el-button:not(.el-button--primary)) {
color: hsl(var(--text-strong));
background: hsl(var(--surface-contrast-soft) / 86%);
border-color: transparent;
box-shadow:
inset 0 1px 0 hsl(var(--glass-border) / 34%),
0 10px 24px -24px hsl(var(--foreground) / 20%);
}
.header-right :deep(.el-button--primary),
.search-group :deep(.el-button--primary) {
box-shadow: 0 16px 28px -22px hsl(var(--primary) / 48%);
}
.dropdown-label {
margin-left: 8px;
}
@media (max-width: 768px) {
.custom-header {
flex-direction: column;
gap: 12px;
align-items: stretch;
}
.header-right {
justify-content: flex-start;
}
.search-middle {
width: 100%;
}
.search-input {
width: 100%;
}
}
</style>