sysmgr-admin/src/components/ty-header.vue
liuyun 99650949c0 style: polish admin layout visuals
- 顶栏改柔和阴影,Logo 区与侧栏连成一体(同宽+右边框)
- 侧栏菜单项圆角、主题色高亮、hover 过渡
- 内容区灰底衬白,footer 加顶边框,多页签激活态配色统一

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 00:40:11 +08:00

271 lines
7.3 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>
<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>