ec-admin/src/components/ty-header.vue
liuyun c96151d2ae refactor: convert raw CSS to Tailwind @apply + search collapseCount=3
- ty-search-form 折叠默认数 2 → 3
- login/register/forgot/home/ty-header/ty-sidebar/ty-mixbar/ty-tabs/ty-footer/ty-setting-panel/icon/default 布局裸 CSS 转 @apply
- 修复背景色工具类误写(bg-base→bg-bg-base、bg-page→bg-bg-page)

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

239 lines
7.1 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" :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="activeIndex"
@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.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 { computed } from 'vue'
import type { ResourceNode } from '@/types/resource'
import type { LayoutType } from '@/config/layout'
import type { BreadcrumbItem } from './ty-breadcrumb.vue'
defineOptions({ name: 'TyHeader' })
const props = 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: '',
},
)
/** 当前激活的顶部一级菜单 code(高亮包含当前路由的一级菜单)。 */
const activeIndex = computed(() => {
const path = props.activePath
for (const m of props.menus) {
if ((m.path ?? m.code) === path) return m.code
if (m.children?.some((c) => (c.path ?? c.code) === path)) return m.code
}
return path
})
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 {
@apply flex items-center bg-bg-base pr-3;
height: var(--ty-header-height, 56px);
box-shadow: 0 1px 4px rgb(0 21 41 / 6%);
z-index: 100;
&.is-fixed {
@apply sticky top-0;
}
&__left {
@apply flex items-center gap-2;
}
&__logo {
@apply flex items-center justify-center gap-2 shrink-0 overflow-hidden bg-bg-base border-r border-solid px-2;
height: var(--ty-header-height, 56px);
border-color: var(--wm-border-color-light);
}
&__logo-img {
@apply w-[30px] h-[30px] shrink-0;
}
&__title {
@apply tracking-[0.5px] text-lg font-semibold text-text-primary whitespace-nowrap;
}
&__action {
@apply inline-flex items-center justify-center w-[34px] h-[34px] border-none rounded-base bg-transparent text-[18px] text-text-regular cursor-pointer transition-colors duration-200;
&:hover {
@apply bg-bg-page;
color: var(--el-color-primary);
}
}
&__nav {
@apply ml-1;
height: var(--ty-header-height, 56px);
// 覆盖 EP 横向菜单根元素默认的 1px 底边框(提高选择器优先级)
&.el-menu--horizontal {
@apply border-b-0;
}
:deep(.el-menu-item),
:deep(.el-sub-menu__title) {
@apply text-text-regular;
height: var(--ty-header-height, 56px);
line-height: var(--ty-header-height, 56px);
}
:deep(.el-menu-item.is-active) {
@apply border-b-0;
color: var(--el-color-primary);
}
:deep(.menu-icon) {
@apply mr-1 text-base;
}
}
&__breadcrumb {
@apply ml-3;
}
&__right {
@apply ml-auto flex items-center gap-0.5;
}
&__user {
@apply inline-flex items-center gap-1.5 px-2 text-base text-text-regular cursor-pointer;
&:hover {
color: var(--el-color-primary);
}
}
}
</style>