- 顶栏改柔和阴影,Logo 区与侧栏连成一体(同宽+右边框) - 侧栏菜单项圆角、主题色高亮、hover 过渡 - 内容区灰底衬白,footer 加顶边框,多页签激活态配色统一 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
271 lines
7.3 KiB
Vue
271 lines
7.3 KiB
Vue
<template>
|
||
<header class="ty-header" :class="{ 'is-fixed': fixed }">
|
||
<div class="ty-header__left">
|
||
<div
|
||
class="ty-header__logo"
|
||
:class="{ 'is-icon': !showTitle }"
|
||
:style="logoWidth != null ? { width: `${logoWidth}px` } : undefined"
|
||
>
|
||
<img v-if="logo" class="ty-header__logo-img" :src="logo" alt="logo" />
|
||
<span v-if="showTitle" class="ty-header__title">{{ title }}</span>
|
||
</div>
|
||
|
||
<button v-if="hasSidebar" class="ty-header__action" type="button" title="折叠/展开菜单" @click="emit('toggle-collapse')">
|
||
<icon type="iconify" :icon="collapsed ? 'mdi:menu-open' : 'mdi:menu'" />
|
||
</button>
|
||
|
||
<!-- 顶部导航:top 显示全树;horizontal 显示一级 -->
|
||
<el-menu
|
||
v-if="layout !== 'sidebar'"
|
||
class="ty-header__nav"
|
||
mode="horizontal"
|
||
:ellipsis="false"
|
||
:default-active="activePath"
|
||
@select="onNavSelect"
|
||
>
|
||
<template v-for="item in menus" :key="item.code">
|
||
<el-sub-menu v-if="layout === 'top' && item.children?.length" :index="item.code">
|
||
<template #title>
|
||
<icon type="iconify" :icon="item.icon ?? 'mdi:menu'" class="menu-icon" />
|
||
<span>{{ item.name }}</span>
|
||
</template>
|
||
<el-menu-item v-for="child in item.children" :key="child.code" :index="child.path ?? child.code">
|
||
{{ child.name }}
|
||
</el-menu-item>
|
||
</el-sub-menu>
|
||
<el-menu-item v-else :index="item.path ?? item.code">
|
||
<icon type="iconify" :icon="item.icon ?? 'mdi:menu'" class="menu-icon" />
|
||
<span>{{ item.name }}</span>
|
||
</el-menu-item>
|
||
</template>
|
||
</el-menu>
|
||
</div>
|
||
|
||
<ty-breadcrumb v-if="showBreadcrumb" :items="breadcrumbs" class="ty-header__breadcrumb" />
|
||
|
||
<div class="ty-header__right">
|
||
<button v-if="showFullscreen" class="ty-header__action" type="button" title="全屏" @click="emit('toggle-fullscreen')">
|
||
<icon type="iconify" :icon="isFullscreen ? 'mdi:fullscreen-exit' : 'mdi:fullscreen'" />
|
||
</button>
|
||
<button class="ty-header__action" type="button" title="暗黑模式" @click="emit('toggle-dark')">
|
||
<icon type="iconify" :icon="isDark ? 'mdi:white-balance-sunny' : 'mdi:weather-night'" />
|
||
</button>
|
||
<button class="ty-header__action" type="button" title="布局设置" @click="emit('open-settings')">
|
||
<icon type="iconify" icon="mdi:cog-outline" />
|
||
</button>
|
||
<el-dropdown trigger="click" @command="onUserCommand">
|
||
<span class="ty-header__user">
|
||
<icon type="iconify" icon="mdi:account-circle" />
|
||
<span>{{ username }}</span>
|
||
</span>
|
||
<template #dropdown>
|
||
<el-dropdown-menu>
|
||
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</template>
|
||
</el-dropdown>
|
||
</div>
|
||
</header>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import type { ResourceNode } from '@/types/resource'
|
||
import type { LayoutType } from '@/config/layout'
|
||
import type { BreadcrumbItem } from './ty-breadcrumb.vue'
|
||
|
||
defineOptions({ name: 'TyHeader' })
|
||
|
||
withDefaults(
|
||
defineProps<{
|
||
/** 应用标题。 */
|
||
title?: string
|
||
/** Logo 图片地址。 */
|
||
logo?: string
|
||
/** 是否显示标题文字(侧栏折叠或纯图标时仅显 Logo)。 */
|
||
showTitle?: boolean
|
||
/** Logo 区宽度(px,与左侧菜单/菜单+MixBar 对齐);null 表示自适应。 */
|
||
logoWidth?: number | null
|
||
/** 是否有侧边栏(决定是否显示折叠按钮)。 */
|
||
hasSidebar?: boolean
|
||
/** 侧边栏是否折叠。 */
|
||
collapsed?: boolean
|
||
/** 顶栏是否固定。 */
|
||
fixed?: boolean
|
||
/** 是否显示面包屑。 */
|
||
showBreadcrumb?: boolean
|
||
/** 面包屑项。 */
|
||
breadcrumbs?: BreadcrumbItem[]
|
||
/** 是否显示全屏按钮。 */
|
||
showFullscreen?: boolean
|
||
/** 当前是否全屏。 */
|
||
isFullscreen?: boolean
|
||
/** 当前是否暗黑。 */
|
||
isDark?: boolean
|
||
/** 布局类型(决定顶部导航渲染方式)。 */
|
||
layout?: LayoutType
|
||
/** 顶部导航菜单。 */
|
||
menus?: ResourceNode[]
|
||
/** 当前激活路径。 */
|
||
activePath?: string
|
||
/** 当前用户名。 */
|
||
username?: string
|
||
}>(),
|
||
{
|
||
title: 'sysmgr',
|
||
logo: '',
|
||
showTitle: true,
|
||
logoWidth: null,
|
||
hasSidebar: true,
|
||
collapsed: false,
|
||
fixed: true,
|
||
showBreadcrumb: true,
|
||
breadcrumbs: () => [],
|
||
showFullscreen: true,
|
||
isFullscreen: false,
|
||
isDark: false,
|
||
layout: 'horizontal',
|
||
menus: () => [],
|
||
activePath: '',
|
||
username: '',
|
||
},
|
||
)
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'toggle-collapse'): void
|
||
(e: 'toggle-dark'): void
|
||
(e: 'toggle-fullscreen'): void
|
||
(e: 'open-settings'): void
|
||
(e: 'navigate', path: string): void
|
||
(e: 'logout'): void
|
||
}>()
|
||
|
||
function onNavSelect(index: string): void {
|
||
emit('navigate', index)
|
||
}
|
||
|
||
function onUserCommand(command: string): void {
|
||
if (command === 'logout') emit('logout')
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.ty-header {
|
||
display: flex;
|
||
align-items: center;
|
||
height: var(--ty-header-height, 56px);
|
||
padding: 0 12px 0 0;
|
||
background: var(--wm-bg-color-base);
|
||
box-shadow: 0 1px 4px rgb(0 21 41 / 6%);
|
||
z-index: 100;
|
||
|
||
&.is-fixed {
|
||
position: sticky;
|
||
top: 0;
|
||
}
|
||
|
||
&__left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
&__logo {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-shrink: 0;
|
||
overflow: hidden;
|
||
height: var(--ty-header-height, 56px);
|
||
padding: 0 12px;
|
||
background: var(--wm-bg-color-base);
|
||
border-right: 1px solid var(--wm-border-color-light);
|
||
|
||
&.is-icon {
|
||
justify-content: center;
|
||
gap: 0;
|
||
}
|
||
}
|
||
|
||
&__logo-img {
|
||
width: 30px;
|
||
height: 30px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
&__title {
|
||
font-size: var(--wm-font-size-lg);
|
||
font-weight: var(--wm-font-weight-semibold);
|
||
letter-spacing: 0.5px;
|
||
color: var(--wm-color-text-primary);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
&__action {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 34px;
|
||
height: 34px;
|
||
border: none;
|
||
border-radius: var(--wm-border-radius-base);
|
||
background: transparent;
|
||
font-size: 18px;
|
||
color: var(--wm-color-text-regular);
|
||
cursor: pointer;
|
||
transition: background-color 0.2s, color 0.2s;
|
||
|
||
&:hover {
|
||
background: var(--wm-bg-color-page);
|
||
color: var(--el-color-primary);
|
||
}
|
||
}
|
||
|
||
&__nav {
|
||
border-bottom: none;
|
||
margin-left: 4px;
|
||
|
||
:deep(.el-menu-item),
|
||
:deep(.el-sub-menu__title) {
|
||
height: var(--ty-header-height, 56px);
|
||
line-height: var(--ty-header-height, 56px);
|
||
color: var(--wm-color-text-regular);
|
||
}
|
||
|
||
:deep(.el-menu-item.is-active) {
|
||
color: var(--el-color-primary);
|
||
border-bottom-color: var(--el-color-primary);
|
||
}
|
||
|
||
:deep(.menu-icon) {
|
||
margin-right: 4px;
|
||
font-size: 16px;
|
||
}
|
||
}
|
||
|
||
&__breadcrumb {
|
||
margin-left: 12px;
|
||
}
|
||
|
||
&__right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 2px;
|
||
margin-left: auto;
|
||
}
|
||
|
||
&__user {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 0 8px;
|
||
font-size: 16px;
|
||
color: var(--wm-color-text-regular);
|
||
cursor: pointer;
|
||
|
||
&:hover {
|
||
color: var(--el-color-primary);
|
||
}
|
||
}
|
||
}
|
||
</style>
|