diff --git a/core/layouts/TopMenu/Topnav/index.vue b/core/layouts/TopMenu/Topnav/index.vue
new file mode 100644
index 0000000..5316c07
--- /dev/null
+++ b/core/layouts/TopMenu/Topnav/index.vue
@@ -0,0 +1,664 @@
+<script setup lang="ts">
+import { ref, computed } from 'vue';
+import { storeToRefs } from 'pinia';
+import { useMenuStore, type TreeMenuItem } from '@core/stores/menu';
+import { useUserStore } from '@core/stores/user';
+import { getConfig } from '@core/configure';
+import { openMenuTab } from '@core/utils/menuTab';
+import { useTheme, THEMES } from '@core/composables/useTheme';
+
+const menuStore = useMenuStore();
+const userStore = useUserStore();
+const { treeMenus, topLevelActiveMenu } = storeToRefs(menuStore);
+
+const title = getConfig().base.title;
+const openMenuId = ref<string | null>(null);
+
+const currentUser = computed(() => userStore.userInfo);
+const userInitial = computed(() =>
+ (currentUser.value?.displayName || currentUser.value?.name || 'U').charAt(0).toUpperCase(),
+);
+const userName = computed(() => currentUser.value?.displayName || currentUser.value?.name || '');
+
+const isMenuActive = (menu: TreeMenuItem) => topLevelActiveMenu.value?.id === menu.id;
+const hasChildren = (menu: TreeMenuItem) =>
+ Array.isArray(menu.children) && menu.children.length > 0;
+
+const closeMenu = () => {
+ openMenuId.value = null;
+};
+
+const handleNavItemClick = (menu: TreeMenuItem) => {
+ if (!hasChildren(menu)) {
+ openMenuTab({ url: menu.path, title: menu.name });
+ menuStore.setActiveMenu(menu);
+ closeMenu();
+ } else {
+ openMenuId.value = openMenuId.value === menu.id ? null : menu.id;
+ }
+};
+
+const handleSubItemClick = (menu: TreeMenuItem) => {
+ openMenuTab({ url: menu.path, title: menu.name });
+ menuStore.setActiveMenu(menu);
+ closeMenu();
+};
+
+const handleMouseEnter = (menu: TreeMenuItem) => {
+ if (hasChildren(menu)) {
+ openMenuId.value = menu.id;
+ }
+};
+
+const handleMouseLeave = () => {
+ openMenuId.value = null;
+};
+
+const handleMegaMouseEnter = () => {
+ // 保持在菜单展开状态
+};
+
+const handleMegaMouseLeave = () => {
+ openMenuId.value = null;
+};
+
+const handleLogout = () => {
+ userStore.logout();
+};
+
+const { currentTheme, setTheme } = useTheme();
+const themeOpen = ref(false);
+
+// 点击外部关闭主题下拉
+const handleOutsideClick = () => {
+ themeOpen.value = false;
+};
+</script>
+
+<template>
+ <header class="topnav" @click="handleOutsideClick">
+ <!-- Logo -->
+ <div class="tn-logo">
+ <div class="tn-mark">
+ <svg viewBox="0 0 17 17" xmlns="http://www.w3.org/2000/svg">
+ <path
+ d="M2 1C1.45 1 1 1.45 1 2v5h3V4h3V1H2zm6 0v3h3v3h3V2c0-.55-.45-1-1-1H8zM1 8v6c0 .55.45 1 1 1h6v-3H5V8H1zm11 3v3H9v3h5c.55 0 1-.45 1-1v-5h-3z"
+ />
+ </svg>
+ </div>
+ <div class="tn-name">{{ title }}</div>
+ </div>
+
+ <!-- 导航菜单 -->
+ <nav class="tn-nav">
+ <div
+ v-for="menu in treeMenus"
+ :key="menu.id"
+ class="tn-item"
+ :class="{ open: openMenuId === menu.id }"
+ @mouseenter="handleMouseEnter(menu)"
+ @mouseleave="handleMouseLeave"
+ >
+ <!-- 导航链接包装器 -->
+ <div class="tn-link-wrap">
+ <span
+ class="tn-link"
+ :class="{ active: isMenuActive(menu) && openMenuId !== menu.id }"
+ @click="handleNavItemClick(menu)"
+ >
+ {{ menu.title || menu.name }}
+ <svg
+ v-if="hasChildren(menu)"
+ class="chv"
+ width="10"
+ height="10"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ stroke-width="2.5"
+ >
+ <path d="M6 9l6 6 6-6" />
+ </svg>
+ </span>
+
+ <!-- 悬浮桥梁 - 连接导航和下拉菜单 -->
+ <div
+ v-if="hasChildren(menu) && openMenuId === menu.id"
+ class="mega-bridge"
+ @mouseenter="handleMegaMouseEnter"
+ @mouseleave="handleMegaMouseLeave"
+ ></div>
+
+ <!-- 巨型下拉菜单 -->
+ <div
+ v-show="hasChildren(menu) && openMenuId === menu.id"
+ class="mega"
+ @mouseenter="handleMegaMouseEnter"
+ @mouseleave="handleMegaMouseLeave"
+ >
+ <div class="mega-cols">
+ <template v-for="(col, colIdx) in menu.children" :key="col.id">
+ <div v-if="colIdx > 0" class="mega-col-sep"></div>
+ <div class="mega-col">
+ <div class="mc-title">{{ col.title || col.name }}</div>
+ <template v-if="col.children && col.children.length > 0">
+ <div
+ v-for="item in col.children"
+ :key="item.id"
+ class="mc-item"
+ @click="handleSubItemClick(item)"
+ >
+ {{ item.title || item.name }}
+ </div>
+ </template>
+ <div v-else class="mc-item" @click="handleSubItemClick(col)">
+ {{ col.title || col.name }}
+ </div>
+ </div>
+ </template>
+ </div>
+ </div>
+ </div>
+ </div>
+ </nav>
+
+ <!-- 右侧操作区 -->
+ <div class="tn-acts">
+ <!-- 主题切换 -->
+ <div class="tn-theme" @click.stop>
+ <button
+ class="tn-btn tn-theme-icon-btn"
+ :title="'当前主题:' + THEMES.find((t) => t.id === currentTheme)?.label"
+ @click="themeOpen = !themeOpen"
+ >
+ <svg
+ width="15"
+ height="15"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ stroke-width="2"
+ >
+ <circle cx="12" cy="12" r="3" />
+ <path
+ d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"
+ />
+ </svg>
+ </button>
+ <div v-if="themeOpen" class="tn-theme-dropdown">
+ <div
+ v-for="theme in THEMES"
+ :key="theme.id"
+ class="tn-theme-item"
+ :class="{ active: currentTheme === theme.id }"
+ @click="
+ setTheme(theme.id);
+ themeOpen = false;
+ "
+ >
+ <span class="tn-ti-icon">{{ theme.icon }}</span>
+ <span class="tn-ti-label">{{ theme.label }}</span>
+ <svg
+ v-if="currentTheme === theme.id"
+ class="tn-ti-check"
+ width="13"
+ height="13"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ stroke-width="2.5"
+ >
+ <polyline points="20 6 9 17 4 12" />
+ </svg>
+ </div>
+ </div>
+ </div>
+
+ <div class="tn-dvd"></div>
+ <button class="tn-btn" title="全局搜索">
+ <svg
+ width="14"
+ height="14"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ stroke-width="2"
+ >
+ <circle cx="11" cy="11" r="8" />
+ <path d="m21 21-4.35-4.35" />
+ </svg>
+ </button>
+ <button class="tn-btn" title="消息通知">
+ <svg
+ width="14"
+ height="14"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ stroke-width="2"
+ >
+ <path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
+ <path d="M13.73 21a2 2 0 0 1-3.46 0" />
+ </svg>
+ </button>
+ <div class="tn-dvd"></div>
+ <div class="tn-user">
+ <div class="tn-av">{{ userInitial }}</div>
+ <span class="tn-un">{{ userName }}</span>
+ </div>
+ <button class="tn-btn tn-logout" title="退出登录" @click="handleLogout">
+ <svg
+ width="14"
+ height="14"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ stroke-width="2"
+ >
+ <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
+ <polyline points="16 17 21 12 16 7" />
+ <line x1="21" y1="12" x2="9" y2="12" />
+ </svg>
+ </button>
+ </div>
+ </header>
+
+ <!-- 遮罩层 -->
+ <div v-if="openMenuId" class="mega-overlay" @click="closeMenu"></div>
+</template>
+
+<style lang="scss" scoped>
+$tn-h: 60px;
+
+.topnav {
+ height: $tn-h;
+ background: var(--tn, #1a3328);
+ display: flex;
+ align-items: center;
+ padding: 0 18px 0 14px;
+ position: relative;
+ z-index: 100;
+ box-shadow: 0 2px 20px rgba(0, 0, 0, 0.22);
+}
+
+// Logo
+.tn-logo {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding-right: 18px;
+ border-right: 1px solid var(--tn-b, #224538);
+ margin-right: 6px;
+ flex-shrink: 0;
+}
+
+.tn-mark {
+ width: 32px;
+ height: 32px;
+ background: var(--tn-ac, #4ec685);
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+
+ svg {
+ width: 17px;
+ height: 17px;
+ fill: var(--tn, #1a3328);
+ }
+}
+
+.tn-name {
+ font-size: 17px;
+ font-weight: 700;
+ color: #fff;
+ letter-spacing: 0.01em;
+ white-space: nowrap;
+}
+
+// 导航菜单
+.tn-nav {
+ display: flex;
+ align-items: stretch;
+ flex: 1;
+ height: 100%;
+ overflow: hidden;
+}
+
+.tn-item {
+ position: relative;
+ display: flex;
+ align-items: center;
+ height: 100%;
+
+ &:hover {
+ .tn-link {
+ color: var(--tn-ac, #4ec685);
+ background: rgba(255, 255, 255, 0.06);
+ }
+ }
+
+ &.open {
+ .tn-link {
+ color: var(--tn-ac, #4ec685);
+ background: rgba(255, 255, 255, 0.08);
+ }
+ }
+}
+
+.tn-link {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ padding: 0 13px;
+ height: 100%;
+ cursor: pointer;
+ user-select: none;
+ color: var(--tn-t, #74b898);
+ font-size: 13.5px;
+ font-weight: 500;
+ white-space: nowrap;
+ border-bottom: 2px solid transparent;
+ transition:
+ color 0.15s,
+ background 0.15s;
+
+ &:hover {
+ color: var(--tn-ac, #4ec685);
+ background: rgba(255, 255, 255, 0.06);
+ }
+
+ &.active {
+ color: var(--tn-ac, #4ec685);
+ border-bottom-color: var(--tn-ac, #4ec685);
+ }
+}
+
+.tn-item.open .tn-link {
+ color: var(--tn-ac, #4ec685);
+ background: rgba(255, 255, 255, 0.08);
+}
+
+.chv {
+ transition: transform 0.2s;
+ opacity: 0.6;
+}
+
+.tn-item.open .chv {
+ transform: rotate(180deg);
+}
+
+// 巨型下拉菜单
+.mega {
+ position: fixed;
+ left: 0;
+ right: 0;
+ top: $tn-h;
+ background: #fff;
+ border-top: 2px solid var(--tn-ac, #4ec685);
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.12);
+ padding: 22px 28px 24px;
+ display: flex;
+ z-index: 200;
+ opacity: 0;
+ transform: translateY(-8px);
+ pointer-events: none;
+ transition:
+ opacity 0.2s ease,
+ transform 0.2s ease;
+}
+
+.tn-item.open .mega,
+.tn-item:hover .mega {
+ opacity: 1;
+ transform: translateY(0);
+ pointer-events: auto;
+}
+
+// 导航链接包装器
+.tn-link-wrap {
+ position: relative;
+ height: 100%;
+ display: flex;
+ align-items: center;
+}
+
+// 悬浮桥梁 - 连接导航和下拉菜单
+.mega-bridge {
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 100%;
+ height: 20px;
+ background: transparent;
+ z-index: 199;
+ pointer-events: auto;
+}
+
+.mega-cols {
+ display: flex;
+ flex-wrap: wrap;
+ flex: 1;
+ gap: 0;
+}
+
+.mega-col {
+ min-width: 150px;
+ padding: 0 24px 0 0;
+ margin: 0 0 14px;
+
+ &:first-child {
+ padding-left: 0;
+ }
+}
+
+.mega-col-sep {
+ width: 1px;
+ background: #e0e6da;
+ margin: 0 10px 14px;
+ flex-shrink: 0;
+ align-self: stretch;
+}
+
+.mc-title {
+ font-size: 10.5px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ color: #87a080;
+ margin-bottom: 9px;
+ padding-bottom: 7px;
+ border-bottom: 1px solid #e0e6da;
+}
+
+.mc-item {
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ padding: 5px 8px;
+ border-radius: 5px;
+ color: #4a604a;
+ font-size: 13px;
+ cursor: pointer;
+ transition:
+ background 0.12s,
+ color 0.12s;
+ white-space: nowrap;
+
+ &::before {
+ content: '';
+ width: 4px;
+ height: 4px;
+ border-radius: 50%;
+ background: #e0e6da;
+ flex-shrink: 0;
+ transition: background 0.12s;
+ }
+
+ &:hover {
+ background: #e8f5ee;
+ color: #1d7040;
+
+ &::before {
+ background: #1d7040;
+ }
+ }
+}
+
+// 遮罩
+.mega-overlay {
+ position: fixed;
+ inset: 0;
+ top: $tn-h;
+ background: rgba(0, 0, 0, 0.06);
+ z-index: 150;
+ pointer-events: none;
+}
+
+// 右侧操作区
+.tn-acts {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin-left: auto;
+ flex-shrink: 0;
+}
+
+.tn-btn {
+ width: 34px;
+ height: 34px;
+ border-radius: 8px;
+ border: none;
+ background: rgba(255, 255, 255, 0.06);
+ color: var(--tn-t, #74b898);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition:
+ background 0.15s,
+ color 0.15s;
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.12);
+ color: var(--tn-ac, #4ec685);
+ }
+}
+
+.tn-logout:hover {
+ background: rgba(220, 38, 38, 0.15) !important;
+ color: #f87171 !important;
+}
+
+.tn-dvd {
+ width: 1px;
+ height: 20px;
+ background: var(--tn-b, #224538);
+ margin: 0 3px;
+}
+
+// 主题切换
+.tn-theme {
+ position: relative;
+}
+
+.tn-theme-icon-btn {
+ color: var(--tn-t, #74b898) !important;
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.12) !important;
+ color: var(--tn-ac, #4ec685) !important;
+ }
+}
+
+.tn-theme-dropdown {
+ position: absolute;
+ right: 0;
+ top: calc(100% + 8px);
+ background: #ffffff;
+ border: 1px solid var(--bd, #e0e6da);
+ border-radius: 10px;
+ box-shadow: 0 8px 24px rgba(25, 36, 24, 0.12);
+ min-width: 140px;
+ overflow: hidden;
+ z-index: 300;
+ padding: 4px;
+}
+
+.tn-theme-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 12px;
+ border-radius: 7px;
+ cursor: pointer;
+ color: var(--t2, #4a604a);
+ font-size: 13px;
+ font-weight: 500;
+ transition:
+ background 0.12s,
+ color 0.12s;
+
+ &:hover {
+ background: var(--ac-l, #e8f5ee);
+ color: var(--ac, #1d7040);
+ }
+
+ &.active {
+ color: var(--ac, #1d7040);
+ font-weight: 600;
+ }
+}
+
+.tn-ti-icon {
+ font-size: 14px;
+ flex-shrink: 0;
+}
+
+.tn-ti-label {
+ flex: 1;
+}
+
+.tn-ti-check {
+ color: var(--ac, #1d7040);
+ flex-shrink: 0;
+}
+
+.tn-user {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 4px 10px 4px 8px;
+ border-radius: 8px;
+ cursor: pointer;
+ color: var(--tn-t, #74b898);
+ transition:
+ background 0.15s,
+ color 0.15s;
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.07);
+ color: var(--tn-ac, #4ec685);
+ }
+}
+
+.tn-av {
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, var(--tn-ac, #4ec685) 0%, #a8e6c8 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 11px;
+ font-weight: 700;
+ color: var(--tn, #1a3328);
+ flex-shrink: 0;
+}
+
+.tn-un {
+ font-size: 13px;
+ font-weight: 500;
+}
+</style>