- 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>
239 lines
7.1 KiB
Vue
239 lines
7.1 KiB
Vue
<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>
|