重命名CubeListPager和CubeListToolbarSearch
Yann authored at 2025-07-28 22:53:20 Yann committed at 2025-07-28 23:04:14
12.70 KiB
cube-front
<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 ThemeSwitcher from '@core/components/ThemeSwitcher.vue';
import LayoutSwitcher from '@core/components/LayoutSwitcher.vue';

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();
};


</script>

<template>
  <header class="topnav">
    <!-- 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">
      <!-- 主题切换 -->
      <ThemeSwitcher>
        <template #icon>
          <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>
        </template>
      </ThemeSwitcher>

      <!-- 布局切换(注册了多个布局时自动显示) -->
      <LayoutSwitcher>
        <template #icon>
          <svg
            width="15"
            height="15"
            viewBox="0 0 24 24"
            fill="none"
            stroke="currentColor"
            stroke-width="2"
          >
            <rect x="3" y="3" width="18" height="4" rx="1" />
            <rect x="3" y="10" width="7" height="11" rx="1" />
            <rect x="13" y="10" width="8" height="11" rx="1" />
          </svg>
        </template>
      </LayoutSwitcher>

      <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-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>