305 lines
6.3 KiB
Vue
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>
|