332 lines
8.9 KiB
Vue
332 lines
8.9 KiB
Vue
<template>
|
|
<v-col class="d-flex align-center bg-surface pr-2 pl-2 pl-m-3 py-1">
|
|
<v-btn
|
|
v-if="isMobile"
|
|
:icon="mdiMenu"
|
|
size="small"
|
|
variant="text"
|
|
@click="emit('toggle-drawer')"
|
|
></v-btn>
|
|
|
|
<div v-if="features.showSearch" class="search-input-wrapper">
|
|
<span id="admin-layout-search-label" class="sr-only">{{ searchConfig.label }}</span>
|
|
<v-text-field
|
|
v-model="searchValueModel"
|
|
aria-labelledby="admin-layout-search-label"
|
|
:aria-label="searchConfig.label"
|
|
class="search-input"
|
|
density="compact"
|
|
hide-details
|
|
name="layout-search"
|
|
:placeholder="searchConfig.placeholder"
|
|
variant="outlined"
|
|
@keyup.enter="triggerSearch"
|
|
>
|
|
<template v-if="!isMobile" #prepend-inner>
|
|
<v-icon size="small" :icon="mdiMagnify" />
|
|
</template>
|
|
<template #append-inner>
|
|
<v-btn
|
|
:aria-label="searchConfig.label"
|
|
color="primary"
|
|
size="small"
|
|
variant="text"
|
|
@click="triggerSearch"
|
|
>
|
|
開始搜尋
|
|
</v-btn>
|
|
</template>
|
|
</v-text-field>
|
|
</div>
|
|
|
|
<div v-if="features.showToolbarActions" class="top-actions">
|
|
<slot name="actions">
|
|
<!-- 通知 -->
|
|
<v-tooltip
|
|
location="bottom"
|
|
:text="toolbarActions.notificationsLabel"
|
|
:aria-label="toolbarActions.notificationsLabel"
|
|
>
|
|
<template #activator="{ props: activatorProps }">
|
|
<v-btn
|
|
v-bind="activatorProps"
|
|
:aria-label="toolbarActions.notificationsLabel"
|
|
icon
|
|
size="small"
|
|
variant="text"
|
|
@click="emit('action', 'notifications')"
|
|
>
|
|
<v-badge
|
|
v-if="toolbarCounts.notifications"
|
|
color="error"
|
|
:content="toolbarCounts.notifications"
|
|
offset-x="4"
|
|
offset-y="-2"
|
|
>
|
|
<v-icon :icon="mdiBellOutline" />
|
|
</v-badge>
|
|
<v-icon v-else :icon="mdiBellOutline" />
|
|
</v-btn>
|
|
</template>
|
|
</v-tooltip>
|
|
|
|
<!-- 訊息 -->
|
|
<v-tooltip
|
|
location="bottom"
|
|
:text="toolbarActions.messagesLabel"
|
|
:aria-label="toolbarActions.messagesLabel"
|
|
>
|
|
<template #activator="{ props: activatorProps }">
|
|
<v-btn
|
|
v-bind="activatorProps"
|
|
:aria-label="toolbarActions.messagesLabel"
|
|
icon
|
|
size="small"
|
|
variant="text"
|
|
@click="emit('action', 'messages')"
|
|
>
|
|
<v-badge
|
|
v-if="toolbarCounts.messages"
|
|
color="warning"
|
|
:content="toolbarCounts.messages"
|
|
offset-x="4"
|
|
offset-y="-2"
|
|
>
|
|
<v-icon :icon="mdiMessageTextOutline" />
|
|
</v-badge>
|
|
<v-icon v-else :icon="mdiMessageTextOutline" />
|
|
</v-btn>
|
|
</template>
|
|
</v-tooltip>
|
|
|
|
<!-- 說明 -->
|
|
<v-tooltip v-if="false" location="bottom" :text="toolbarActions.helpLabel">
|
|
<template #activator="{ props: activatorProps }">
|
|
<v-btn
|
|
v-bind="activatorProps"
|
|
:aria-label="toolbarActions.helpLabel"
|
|
icon
|
|
size="small"
|
|
variant="text"
|
|
@click="emit('action', 'help')"
|
|
>
|
|
<v-icon :icon="mdiHelp" />
|
|
</v-btn>
|
|
</template>
|
|
</v-tooltip>
|
|
|
|
<!-- 設定 -->
|
|
<v-menu :close-on-content-click="false" location="bottom end">
|
|
<template #activator="{ props: menuProps }">
|
|
<v-tooltip
|
|
location="bottom"
|
|
:text="toolbarActions.settingsLabel"
|
|
:aria-label="toolbarActions.settingsLabel"
|
|
>
|
|
<template #activator="{ props: tooltipProps }">
|
|
<v-btn
|
|
v-bind="{ ...menuProps, ...tooltipProps }"
|
|
:aria-label="toolbarActions.settingsLabel"
|
|
icon
|
|
size="small"
|
|
variant="text"
|
|
>
|
|
<v-icon :icon="mdiCogOutline" />
|
|
</v-btn>
|
|
</template>
|
|
</v-tooltip>
|
|
</template>
|
|
<v-list role="none" density="compact" width="180">
|
|
<v-list-subheader class="text-subtitle-1 py-2">顯示設定</v-list-subheader>
|
|
<v-list-item>
|
|
<v-switch
|
|
v-model="showFavoritesBarModel"
|
|
color="primary"
|
|
density="comfortable"
|
|
hide-details
|
|
>
|
|
<template #label>
|
|
<span class="text-body-2" style="width: 8ch">常用功能</span>
|
|
</template>
|
|
</v-switch>
|
|
</v-list-item>
|
|
<v-list-item>
|
|
<v-switch
|
|
v-model="breadcrumbBarVisibleModel"
|
|
color="primary"
|
|
density="comfortable"
|
|
hide-details
|
|
>
|
|
<template #label>
|
|
<span class="text-body-2" style="width: 8ch">路徑</span>
|
|
</template>
|
|
</v-switch>
|
|
</v-list-item>
|
|
</v-list>
|
|
</v-menu>
|
|
|
|
<!-- 登出 -->
|
|
<v-tooltip location="bottom" :text="logoutLabel" :aria-label="logoutLabel">
|
|
<template #activator="{ props: activatorProps }">
|
|
<v-btn
|
|
v-bind="activatorProps"
|
|
:aria-label="logoutLabel"
|
|
icon
|
|
size="small"
|
|
variant="text"
|
|
@click="emit('logout')"
|
|
>
|
|
<v-icon :icon="mdiLogout" />
|
|
</v-btn>
|
|
</template>
|
|
</v-tooltip>
|
|
|
|
<v-tooltip
|
|
v-if="features.showThemeToggle"
|
|
location="bottom"
|
|
:text="themeToggleLabel"
|
|
:aria-label="themeToggleLabel"
|
|
>
|
|
<template #activator="{ props: activatorProps }">
|
|
<v-btn
|
|
v-bind="activatorProps"
|
|
:aria-label="themeToggleLabel"
|
|
icon
|
|
variant="text"
|
|
@click="emit('toggle-theme')"
|
|
>
|
|
<v-icon :icon="mdiPaletteOutline" />
|
|
</v-btn>
|
|
</template>
|
|
</v-tooltip>
|
|
</slot>
|
|
</div>
|
|
</v-col>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type {
|
|
AdminLayoutActionType,
|
|
AdminLayoutFeatures,
|
|
AdminLayoutSearchConfig,
|
|
AdminLayoutToolbarActions,
|
|
AdminLayoutToolbarCounts,
|
|
} from './types'
|
|
import {
|
|
mdiBellOutline,
|
|
mdiCogOutline,
|
|
mdiHelp,
|
|
mdiLogout,
|
|
mdiMagnify,
|
|
mdiMenu,
|
|
mdiMessageTextOutline,
|
|
mdiPaletteOutline,
|
|
} from '@mdi/js'
|
|
import { computed } from 'vue'
|
|
|
|
interface Props {
|
|
isMobile?: boolean
|
|
features?: AdminLayoutFeatures
|
|
searchValue?: string
|
|
searchConfig?: AdminLayoutSearchConfig
|
|
toolbarActions?: AdminLayoutToolbarActions
|
|
toolbarCounts?: AdminLayoutToolbarCounts
|
|
logoutLabel?: string
|
|
themeToggleLabel?: string
|
|
showFavoritesBar?: boolean
|
|
breadcrumbBarVisible?: boolean
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
isMobile: false,
|
|
features: () => ({
|
|
showThemeToggle: false,
|
|
showFavorites: true,
|
|
showBreadcrumb: true,
|
|
showSearch: true,
|
|
showToolbarActions: true,
|
|
showUserInfo: true,
|
|
}),
|
|
searchValue: '',
|
|
searchConfig: () => ({
|
|
placeholder: '',
|
|
label: '',
|
|
}),
|
|
toolbarActions: () => ({
|
|
notificationsLabel: '',
|
|
messagesLabel: '',
|
|
helpLabel: '',
|
|
settingsLabel: '',
|
|
}),
|
|
toolbarCounts: () => ({
|
|
notifications: 0,
|
|
messages: 0,
|
|
}),
|
|
logoutLabel: '',
|
|
themeToggleLabel: '',
|
|
showFavoritesBar: true,
|
|
breadcrumbBarVisible: true,
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
'toggle-drawer': []
|
|
'update:searchValue': [value: string]
|
|
search: []
|
|
action: [type: AdminLayoutActionType]
|
|
logout: []
|
|
'toggle-theme': []
|
|
'update:show-favorites-bar': [value: boolean]
|
|
'update:breadcrumb-bar-visible': [value: boolean]
|
|
}>()
|
|
|
|
const searchValueModel = computed({
|
|
get: () => props.searchValue,
|
|
set: (value) => emit('update:searchValue', value),
|
|
})
|
|
|
|
const showFavoritesBarModel = computed({
|
|
get: () => props.showFavoritesBar,
|
|
set: (value) => emit('update:show-favorites-bar', value),
|
|
})
|
|
|
|
const breadcrumbBarVisibleModel = computed({
|
|
get: () => props.breadcrumbBarVisible,
|
|
set: (value) => emit('update:breadcrumb-bar-visible', value),
|
|
})
|
|
|
|
function triggerSearch() {
|
|
emit('search')
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.search-input-wrapper {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
:deep(.search-input-wrapper .v-field--appended) {
|
|
padding-inline-end: 4px;
|
|
}
|
|
|
|
.top-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.sr-only {
|
|
position: absolute;
|
|
width: 1px;
|
|
height: 1px;
|
|
padding: 0;
|
|
margin: -1px;
|
|
overflow: hidden;
|
|
clip: rect(0, 0, 0, 0);
|
|
white-space: nowrap;
|
|
border: 0;
|
|
}
|
|
</style>
|