Skip to content
0

右键菜单组件

新增组件

  • docs\.vitepress\theme\components下创建ContextMenu文件夹,分别创建以下文件
vue
<template>
  <div
    v-show="isVisible"
    ref="menuDom"
    class="context-menu-var context-menu"
    :style="{
      top: `${y}px`,
      left: `${x}px`,
    }"
  >
    <div class="menu-container">
      <!-- 头部区域 -->
      <div class="menu-header">
        <div class="menu-title-icon" v-html="menuData.header?.svg"></div>
        <span style="color: var(--title-text-color)">{{
          menuData.header?.title
        }}</span>

        <!-- 全屏svg -->
        <div
          class="menu-title-icon screen"
          @click="
            toggleFullscreen();
            hideMenu();
          "
        >
          <SvgRender v-show="isFullScreen" :svg="FullScreen" />
          <SvgRender v-show="!isFullScreen" :svg="NonFullScreen" />
        </div>
      </div>

      <!-- 菜单项列表 -->
      <ul class="menu-items">
        <li
          :class="item.subMenu ? 'menu-item has-submenu' : 'menu-item'"
          v-for="item in menuData.body"
          :key="item.text"
          @click="!item.subMenu && handleClick(item, frontmatter, navigateTo)"
        >
          <div class="menu-item-icon" v-html="item.svg"></div>
          <span>{{ item.text }}</span>

          <!-- 如果有子菜单则显示箭头 -->
          <div class="menu-item-arrow" v-if="item.subMenu" v-html="Arrow"></div>

          <ul class="submenu" v-if="item.subMenu">
            <li
              class="submenu-item"
              v-for="subItem in item?.subMenu"
              :key="subItem.text"
              @click.stop="handleClick(subItem, frontmatter, navigateTo)"
            >
              <div class="submenu-item-icon" v-html="subItem.svg"></div>
              <span>{{ subItem.text }}</span>
            </li>
          </ul>
        </li>

        <!-- 分割线 -->
        <li class="menu-divider"></li>

        <!-- 刷新页面 - 特殊样式 -->
        <li
          v-if="menuData.footer.copy"
          class="menu-item menu-footer-item"
          @click="copy"
        >
          <SvgRender class="menu-item-icon" :svg="Copy" />
          <span>复制文本</span>
        </li>

        <li
          v-if="menuData.footer.refresh"
          class="menu-item menu-footer-item"
          @click="handleRefresh"
        >
          <SvgRender class="menu-item-icon" :svg="Refresh" />
          <span>刷新页面</span>
        </li>
      </ul>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import { useData, useRouter, withBase } from "vitepress"; // 引入 VitePress 路由
import { menuData } from "./MenuData";
import { useFullscreen } from "./useFullscreen.js";
import SvgRender from "./SvgRender.vue";
import { Arrow, Copy, FullScreen, NonFullScreen, Refresh } from "./SvgList";
import { TkMessage } from "vitepress-theme-teek";
import { useCopyEvent, triggerCopyEvent } from "../../composables/useCopyEvent";

const { frontmatter } = useData();
// 自定义处理全屏事件hook
const { isFullScreen, toggleFullscreen } = useFullscreen();

const menuDom = ref<HTMLDivElement | null>(null);

const router = useRouter(); // 获取 VitePress 路由实例
const isVisible = ref(false);
const x = ref(0);
const y = ref(0);

// 检查是否在客户端环境
const isClient =
  typeof window !== "undefined" || typeof document !== "undefined";

// 显示菜单
const showMenu = (event: MouseEvent) => {
  if (!isClient) return null;

  // 阻止默认菜单行为
  event.preventDefault();

  const element = menuDom.value;
  if (!element) return;

  // 先显示菜单以获取准确尺寸(但不可见)
  element.style.visibility = "hidden";

  // 获取菜单和子菜单的精确尺寸
  const menuRect = element.getBoundingClientRect();
  const submenuWidth = 180; // 子菜单宽度(假设固定)

  // 初始位置为鼠标位置
  let posX = event.clientX;
  let posY = event.clientY;

  // 计算安全区域内的位置
  const rightEdge = window.innerWidth - 10; // 右侧安全边距
  const bottomEdge = window.innerHeight - 10; // 底部安全边距

  // 处理右侧溢出
  if (posX + menuRect.width + submenuWidth > rightEdge) {
    posX = Math.max(10, rightEdge - menuRect.width);
  }

  // 处理底部溢出
  if (posY + menuRect.height > bottomEdge) {
    posY = Math.max(10, bottomEdge - menuRect.height);
  }

  // 应用位置样式
  x.value = posX;
  y.value = posY;
  isVisible.value = true;

  // 延迟显示以确保位置计算完成
  setTimeout(() => {
    element.style.visibility = "visible";
  }, 0);

  // 确保子菜单位置正确显示
  adjustSubmenuPositions();
};

// 调整子菜单位置,防止溢出屏幕
const adjustSubmenuPositions = () => {
  if (!isClient) return null;

  const submenus = document.querySelectorAll(
    ".submenu"
  ) as NodeListOf<HTMLElement>;

  // 使用 requestAnimationFrame 批量处理样式更新
  requestAnimationFrame(() => {
    submenus.forEach((submenu) => {
      const parentItem = submenu.closest(".has-submenu") as HTMLElement;

      // 重置样式以便获取初始位置
      submenu.style.left = "";
      submenu.style.right = "";
      submenu.style.top = "";

      // 获取父菜单项和子菜单的边界矩形
      const parentRect = parentItem.getBoundingClientRect();
      const submenuRect = submenu.getBoundingClientRect();

      // 计算可用空间
      const availableRight = window.innerWidth - parentRect.right;
      const availableLeft = parentRect.left;
      const availableBottom = window.innerHeight - parentRect.bottom;
      const availableTop = parentRect.top;

      // 水平方向定位
      if (
        submenuRect.width > availableRight &&
        availableLeft > availableRight
      ) {
        // 右侧空间不足且左侧空间更大,向左显示
        submenu.style.right = "100%";
        submenu.style.left = "auto";
      } else {
        // 默认向右显示
        submenu.style.left = "100%";
        submenu.style.right = "auto";
      }

      // 垂直方向定位
      if (
        submenuRect.height > availableBottom &&
        availableTop > availableBottom
      ) {
        // 底部空间不足且顶部空间更大,向上显示
        submenu.style.top = `-${submenuRect.height - parentRect.height}px`;
      } else {
        // 默认向下显示
        submenu.style.top = "0";
      }
    });
  });
};

// 窗口大小变化时重新定位菜单
const handleResize = () => {
  if (isVisible.value) {
    adjustSubmenuPositions();
  }
};

// 隐藏菜单
const hideMenu = () => {
  isVisible.value = false;
};

// 导航到指定路由
const navigateTo = (path: string) => {
  if (!isClient) return null;

  const targetPath = withBase(path);
  const currentPath = router.route.path;

  console.log(currentPath, "===>", targetPath);
  // 如果当前路径和目标路径相同,则不执行导航
  if (currentPath === targetPath) {
    hideMenu();
    return;
  }

  router.go(targetPath); // 使用 VitePress 路由的 goTo 方法
  hideMenu();
};

// 复制选中的文本内容到剪贴板
const copy = async () => {
  try {
    // 获取用户选中的文本
    const selectedText = window.getSelection()?.toString();

    if (!selectedText) {
      // 没有选中内容时显示提示
      TkMessage.info({
        message: "没有选中任何内容",
        offset: 16, // 距离窗口顶部偏移量
      });
      return false;
    }

    // 使用 Clipboard API 复制文本
    await navigator.clipboard.writeText(selectedText);

    // 显示顶部复制提示横幅
    triggerCopyEvent();

    // 复制成功提示(保留原有的消息提示)
    TkMessage.success({
      message: `复制内容: ${selectedText}`,
      offset: 76, // 距离窗口顶部偏移量
    });
    return true;
  } catch (err) {
    // 处理复制失败的情况(如浏览器不支持或无权限)
    TkMessage.error({
      message: `复制失败: ${err instanceof Error ? err.message : String(err)}`,
      offset: 76, // 距离窗口顶部偏移量
    });
    return false;
  } finally {
    // 隐藏菜单
    hideMenu();
  }
};

// 刷新页面
const handleRefresh = () => {
  if (!isClient) return null;
  window.location.reload();
  hideMenu();
};

// 点击其他地方时隐藏菜单
const clickOutsideHideMenu = (event: MouseEvent) => {
  const element = menuDom.value;
  if (isVisible.value && element && !element.contains(event.target as Node)) {
    hideMenu();
  }
};

// 键盘ESC键关闭菜单
const handleKeydown = (event: KeyboardEvent) => {
  if (event.key === "Escape") {
    hideMenu();
  }
};

// 统一的菜单点击处理函数(包装器)
const handleClick = (item: any, frontmatter: any, navigateTo: Function) => {
  try {
    // 1. 执行子项原有的点击逻辑
    if (typeof item.click === "function") {
      item.click(frontmatter, navigateTo);
    }
  } finally {
    // 2. 执行统一的后置操作:隐藏菜单
    hideMenu();
  }
};

onMounted(() => {
  document.addEventListener("contextmenu", showMenu);
  document.addEventListener("click", clickOutsideHideMenu);
  document.addEventListener("keydown", handleKeydown);
  window.addEventListener("resize", handleResize);
});

onUnmounted(() => {
  document.removeEventListener("contextmenu", showMenu);
  document.removeEventListener("click", clickOutsideHideMenu);
  document.removeEventListener("keydown", handleKeydown);
  window.removeEventListener("resize", handleResize);
});
</script>

<style scoped lang="scss">
.context-menu-var {
  // 主题色
  --theme-color: #a78bfa;
  // 标题颜色
  --title-text-color: #8b5cf6;
  // 头部背景色
  --header-bg-color: linear-gradient(to right, #f9f5ff, #f5f3ff);
  // 文本颜色
  --text-color: #4a4158;
  // 菜单背景颜色
  --menu-bg-color: #ffffff;
  // 菜单项鼠标悬停背景颜色
  --menu-hover-bg-color: #f5f3ff;
  // 分割线颜色
  --divider-bg-color: #f0e6ff;
}

html.dark .context-menu-var {
  // 主题色
  --theme-color: #8b5cf6;
  // 标题颜色
  --title-text-color: #8b5cf6;
  // 头部背景色
  --header-bg-color: linear-gradient(to right, #312a48, #2d2644);
  // 文本颜色
  --text-color: #ffffff;
  // 菜单背景颜色
  --menu-bg-color: #1e1b2d;
  // 菜单项鼠标悬停背景颜色
  --menu-hover-bg-color: #352e54;
  // 分割线颜色
  --divider-bg-color: #372f52;
}

.context-menu {
  position: fixed;
  z-index: 100000;
  user-select: none;
  animation: fadeIn 0.2s ease-out;
  filter: drop-shadow(0 4px 20px rgba(0, 0, 0, 0.15));
}

// 设置svg颜色
.context-menu :deep(svg) {
  stroke: var(--theme-color);
}

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: scale(0.98);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

/* 菜单容器 */
.menu-container {
  background-color: var(--menu-bg-color);
  border-radius: 12px;
  box-shadow: 0 4px 20px rgba(139, 92, 246, 0.15);
  border: none;
  min-width: 240px;
  color: var(--text-color);
  position: relative;
  transition: all 0.2s ease;
  animation: slideIn 0.2s ease;
  overflow: visible;
}

html.dark .menu-container {
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}

/* 菜单头部 */
.menu-header {
  display: flex;
  align-items: center;
  padding: 10px 16px;
  border-bottom: 1px solid var(--border-light-color);
  background: var(--header-bg-color);
  border-top-left-radius: 12px;
  border-top-right-radius: 12px;
  position: relative;
  font-weight: bold;
}

.menu-title-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  margin-right: 10px;
  width: 20px;
  height: 20px;
}

.screen {
  margin-left: auto;
  transition: transform 0.3s;
}

.screen:hover {
  stroke: var(--vp-c-brand-1);
  transform: scale(1.15);
}

/* 菜单项 */
.menu-items {
  list-style: none;
  margin: 0;
  padding: 10px;
}

/* 分隔线 */
.menu-divider {
  height: 1px;
  margin: 10px 0;
  background: var(--divider-bg-color);
}

.menu-item {
  display: flex;
  align-items: center;
  padding: 5px 14px;
  position: relative;
  font-size: 0.9rem;
  margin: 4px 0;
  border-radius: 10px;
  cursor: pointer;
  transition: all 0.2s ease;
  overflow: visible;
}

/* 子菜单箭头旋转 */
.has-submenu:hover .menu-item-arrow {
  transform: rotate(90deg);
}

/* 菜单hover背景等 */
.menu-item:hover {
  background-color: var(--menu-hover-bg-color);
  transform: translateX(2px);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.menu-item-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  margin-right: 12px;
  width: 18px;
  height: 18px;
  transition: transform 0.3s;
}

/* 图标放大加旋转 */
.menu-item:hover .menu-item-icon {
  transform: scale(1.15) rotate(8deg);
}

/* 鼠标悬停变色逻辑,带有refresh-item类的除外 */
.menu-item:not(.menu-footer-item):hover .menu-item-icon :deep(svg) {
  stroke: var(--vp-c-brand-1);
}

.submenu-item:hover .submenu-item-icon :deep(svg) {
  stroke: var(--vp-c-brand-1);
}

.menu-item-arrow {
  margin-left: auto;
  width: 16px;
  height: 16px;
  opacity: 0.7;
  transition: transform 0.2s ease;
}

/* 子菜单 */
.submenu {
  position: absolute;
  top: 0;
  left: 100%;
  background-color: var(--menu-bg-color);
  border-radius: 12px;
  box-shadow: 0 4px 16px rgba(139, 92, 246, 0.12);
  border: none;
  min-width: 180px;
  opacity: 0;
  transform: translateX(-5px);
  transition: all 0.2s ease;
  z-index: 1000;
  padding: 8px;
}

.has-submenu:hover > .submenu {
  opacity: 1;
  transform: translateX(0);
}

html.dark .submenu {
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}

.submenu-item {
  display: flex;
  align-items: center;
  padding: 10px 14px;
  cursor: pointer;
  font-size: 0.85rem;
  border-radius: 8px;
  margin: 3px 0;
  transition: all 0.2s ease;
}

.submenu-item:hover {
  background-color: var(--menu-hover-bg-color);
  transform: translateX(2px);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.submenu-item-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  margin-right: 10px;
  width: 16px;
  height: 16px;
}

.menu-footer-item {
  margin: 6px 8px;
  background: linear-gradient(135deg, #8b5cf6, #ec4899);
  box-shadow: 0 3px 12px rgba(139, 92, 246, 0.2);
  color: white;
  transition: all 0.2s ease;
  border: none;
  border-radius: 8px;
}

.menu-footer-item:hover {
  background: linear-gradient(135deg, #a78bfa, #f472b6);
  box-shadow: 0 5px 15px rgba(139, 92, 246, 0.4),
    0 2px 5px rgba(236, 72, 153, 0.3);
  //transform: translateY(-2px) translateX(2px);
}
</style>
ts
const Exclamation = `<svg
            viewBox="0 0 24 24"
            width="24"
            height="24"
            stroke-width="2"
            fill="none"
            stroke-linecap="round"
            stroke-linejoin="round"
          >
            <circle cx="12" cy="12" r="10"></circle>
            <line x1="12" y1="8" x2="12" y2="12"></line>
            <line x1="12" y1="16" x2="12.01" y2="16"></line>
          </svg>`;

const Home = `<svg
              viewBox="0 0 24 24"
              width="24"
              height="24"
              stroke-width="2"
              fill="none"
              stroke-linecap="round"
              stroke-linejoin="round"
            >
              <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
              <polyline points="9 22 9 12 15 12 15 22"></polyline>
            </svg>`;

const About = `<svg
              viewBox="0 0 24 24"
              width="24"
              height="24"
              stroke-width="2"
              fill="none"
              stroke-linecap="round"
              stroke-linejoin="round"
            >
              <path
                d="M12 19.2c-2.5 0-4.71-1.28-6-3.2c.03-2 4-3.1 6-3.1s5.97 1.1 6 3.1a7.23 7.23 0 0 1-6 3.2M12 5a3 3 0 0 1 3 3a3 3 0 0 1-3 3a3 3 0 0 1-3-3a3 3 0 0 1 3-3m0-3A10 10 0 0 0 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10c0-5.53-4.5-10-10-10"
              />
            </svg>`;

const Other = `<svg
              viewBox="0 0 24 24"
              width="24"
              height="24"
              stroke-width="2"
              fill="none"
              stroke-linecap="round"
              stroke-linejoin="round"
            >
              <path d="M4 5h13" />
              <path d="M4 10h10" />
              <path d="M4 15h16" />
              <path d="M4 20h13" />
            </svg>`;

const TreeHollow = `<svg
                  viewBox="0 0 24 24"
                  width="24"
                  height="24"
                  stroke-width="2"
                  fill="none"
                  stroke-linecap="round"
                  stroke-linejoin="round"
                >
                  <path d="M18 8h1a4 4 0 0 1 0 8h-1"></path>
                  <path d="M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"></path>
                  <line x1="6" y1="1" x2="6" y2="4"></line>
                  <line x1="10" y1="1" x2="10" y2="4"></line>
                  <line x1="14" y1="1" x2="14" y2="4"></line>
                </svg>`;

const MessageBoard = `<svg
                  viewBox="0 0 24 24"
                  width="24"
                  height="24"
                  stroke-width="2"
                  fill="none"
                  stroke-linecap="round"
                  stroke-linejoin="round"
                >
                  <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
                </svg>`;

const FriendshipLinks = `<svg
              viewBox="0 0 24 24"
              width="24"
              height="24"
              stroke-width="2"
              fill="none"
              stroke-linecap="round"
              stroke-linejoin="round"
            >
              <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
              <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
            </svg>`;

const Music = `<svg
              viewBox="0 0 24 24"
              width="24"
              height="24"
              stroke-width="2"
              fill="none"
              stroke-linecap="round"
              stroke-linejoin="round"
            >
              <path d="M9 18V5l12-2v13"></path>
              <circle cx="6" cy="18" r="3"></circle>
              <circle cx="18" cy="16" r="3"></circle>
            </svg>`;

const PhotoAlbum = `<svg
              viewBox="0 0 24 24"
              width="24"
              height="24"
              stroke-width="2"
              fill="none"
              stroke-linecap="round"
              stroke-linejoin="round"
            >
              <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
              <circle cx="8.5" cy="8.5" r="1.5"></circle>
              <polyline points="21 15 16 10 5 21"></polyline>
            </svg>`;

const FullScreen = `<svg
            viewBox="0 0 24 24"
            width="24"
            height="24"
            stroke-width="2"
            fill="none"
            stroke-linecap="round"
            stroke-linejoin="round"
          >
            <path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"></path>
          </svg>`;

const NonFullScreen = `<svg
            viewBox="0 0 24 24"
            width="24"
            height="24"
            stroke-width="2"
            fill="none"
            stroke-linecap="round"
            stroke-linejoin="round"
          >
            <path
              d="M20 9V6.616q0-.231-.192-.424T19.385 6H17V5h2.385q.69 0 1.152.463T21 6.616V9zM3 9V6.616q0-.691.463-1.153T4.615 5H7v1H4.616q-.231 0-.424.192T4 6.616V9zm14 10v-1h2.385q.23 0 .423-.192t.192-.424V15h1v2.385q0 .69-.462 1.152T19.385 19zM4.615 19q-.69 0-1.153-.462T3 17.384V15h1v2.385q0 .23.192.423t.423.192H7v1zm2.231-3.846V8.846h10.308v6.308zm1-1h8.308V9.846H7.846zm0 0V9.846z"
            />
          </svg>`;

const Arrow = `<svg
              viewBox="0 0 24 24"
              width="24"
              height="24"
              stroke-width="2"
              fill="none"
              stroke-linecap="round"
              stroke-linejoin="round"
            >
              <polyline points="9 18 15 12 9 6"></polyline>
            </svg>`;

const Refresh = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" >
              <rect width="7.33" height="7.33" x="1" y="1" fill="currentColor">
                <animate
                  id="svgSpinnersBlocksWave0"
                  attributeName="x"
                  begin="0;svgSpinnersBlocksWave1.end+0.3s"
                  dur="0.9s"
                  values="1;4;1"
                />
                <animate attributeName="y" begin="0;svgSpinnersBlocksWave1.end+0.3s" dur="0.9s" values="1;4;1" />
                <animate
                  attributeName="width"
                  begin="0;svgSpinnersBlocksWave1.end+0.3s"
                  dur="0.9s"
                  values="7.33;1.33;7.33"
                />
                <animate
                  attributeName="height"
                  begin="0;svgSpinnersBlocksWave1.end+0.3s"
                  dur="0.9s"
                  values="7.33;1.33;7.33"
                />
              </rect>
              <rect width="7.33" height="7.33" x="8.33" y="1" fill="currentColor">
                <animate
                  attributeName="x"
                  begin="svgSpinnersBlocksWave0.begin+0.15s"
                  dur="0.9s"
                  values="8.33;11.33;8.33"
                />
                <animate attributeName="y" begin="svgSpinnersBlocksWave0.begin+0.15s" dur="0.9s" values="1;4;1" />
                <animate
                  attributeName="width"
                  begin="svgSpinnersBlocksWave0.begin+0.15s"
                  dur="0.9s"
                  values="7.33;1.33;7.33"
                />
                <animate
                  attributeName="height"
                  begin="svgSpinnersBlocksWave0.begin+0.15s"
                  dur="0.9s"
                  values="7.33;1.33;7.33"
                />
              </rect>
              <rect width="7.33" height="7.33" x="1" y="8.33" fill="currentColor">
                <animate attributeName="x" begin="svgSpinnersBlocksWave0.begin+0.15s" dur="0.9s" values="1;4;1" />
                <animate
                  attributeName="y"
                  begin="svgSpinnersBlocksWave0.begin+0.15s"
                  dur="0.9s"
                  values="8.33;11.33;8.33"
                />
                <animate
                  attributeName="width"
                  begin="svgSpinnersBlocksWave0.begin+0.15s"
                  dur="0.9s"
                  values="7.33;1.33;7.33"
                />
                <animate
                  attributeName="height"
                  begin="svgSpinnersBlocksWave0.begin+0.15s"
                  dur="0.9s"
                  values="7.33;1.33;7.33"
                />
              </rect>
              <rect width="7.33" height="7.33" x="15.66" y="1" fill="currentColor">
                <animate
                  attributeName="x"
                  begin="svgSpinnersBlocksWave0.begin+0.3s"
                  dur="0.9s"
                  values="15.66;18.66;15.66"
                />
                <animate attributeName="y" begin="svgSpinnersBlocksWave0.begin+0.3s" dur="0.9s" values="1;4;1" />
                <animate
                  attributeName="width"
                  begin="svgSpinnersBlocksWave0.begin+0.3s"
                  dur="0.9s"
                  values="7.33;1.33;7.33"
                />
                <animate
                  attributeName="height"
                  begin="svgSpinnersBlocksWave0.begin+0.3s"
                  dur="0.9s"
                  values="7.33;1.33;7.33"
                />
              </rect>
              <rect width="7.33" height="7.33" x="8.33" y="8.33" fill="currentColor">
                <animate
                  attributeName="x"
                  begin="svgSpinnersBlocksWave0.begin+0.3s"
                  dur="0.9s"
                  values="8.33;11.33;8.33"
                />
                <animate
                  attributeName="y"
                  begin="svgSpinnersBlocksWave0.begin+0.3s"
                  dur="0.9s"
                  values="8.33;11.33;8.33"
                />
                <animate
                  attributeName="width"
                  begin="svgSpinnersBlocksWave0.begin+0.3s"
                  dur="0.9s"
                  values="7.33;1.33;7.33"
                />
                <animate
                  attributeName="height"
                  begin="svgSpinnersBlocksWave0.begin+0.3s"
                  dur="0.9s"
                  values="7.33;1.33;7.33"
                />
              </rect>
              <rect width="7.33" height="7.33" x="1" y="15.66" fill="currentColor">
                <animate attributeName="x" begin="svgSpinnersBlocksWave0.begin+0.3s" dur="0.9s" values="1;4;1" />
                <animate
                  attributeName="y"
                  begin="svgSpinnersBlocksWave0.begin+0.3s"
                  dur="0.9s"
                  values="15.66;18.66;15.66"
                />
                <animate
                  attributeName="width"
                  begin="svgSpinnersBlocksWave0.begin+0.3s"
                  dur="0.9s"
                  values="7.33;1.33;7.33"
                />
                <animate
                  attributeName="height"
                  begin="svgSpinnersBlocksWave0.begin+0.3s"
                  dur="0.9s"
                  values="7.33;1.33;7.33"
                />
              </rect>
              <rect width="7.33" height="7.33" x="15.66" y="8.33" fill="currentColor">
                <animate
                  attributeName="x"
                  begin="svgSpinnersBlocksWave0.begin+0.45s"
                  dur="0.9s"
                  values="15.66;18.66;15.66"
                />
                <animate
                  attributeName="y"
                  begin="svgSpinnersBlocksWave0.begin+0.45s"
                  dur="0.9s"
                  values="8.33;11.33;8.33"
                />
                <animate
                  attributeName="width"
                  begin="svgSpinnersBlocksWave0.begin+0.45s"
                  dur="0.9s"
                  values="7.33;1.33;7.33"
                />
                <animate
                  attributeName="height"
                  begin="svgSpinnersBlocksWave0.begin+0.45s"
                  dur="0.9s"
                  values="7.33;1.33;7.33"
                />
              </rect>
              <rect width="7.33" height="7.33" x="8.33" y="15.66" fill="currentColor">
                <animate
                  attributeName="x"
                  begin="svgSpinnersBlocksWave0.begin+0.45s"
                  dur="0.9s"
                  values="8.33;11.33;8.33"
                />
                <animate
                  attributeName="y"
                  begin="svgSpinnersBlocksWave0.begin+0.45s"
                  dur="0.9s"
                  values="15.66;18.66;15.66"
                />
                <animate
                  attributeName="width"
                  begin="svgSpinnersBlocksWave0.begin+0.45s"
                  dur="0.9s"
                  values="7.33;1.33;7.33"
                />
                <animate
                  attributeName="height"
                  begin="svgSpinnersBlocksWave0.begin+0.45s"
                  dur="0.9s"
                  values="7.33;1.33;7.33"
                />
              </rect>
              <rect width="7.33" height="7.33" x="15.66" y="15.66" fill="currentColor">
                <animate
                  id="svgSpinnersBlocksWave1"
                  attributeName="x"
                  begin="svgSpinnersBlocksWave0.begin+0.6s"
                  dur="0.9s"
                  values="15.66;18.66;15.66"
                />
                <animate
                  attributeName="y"
                  begin="svgSpinnersBlocksWave0.begin+0.6s"
                  dur="0.9s"
                  values="15.66;18.66;15.66"
                />
                <animate
                  attributeName="width"
                  begin="svgSpinnersBlocksWave0.begin+0.6s"
                  dur="0.9s"
                  values="7.33;1.33;7.33"
                />
                <animate
                  attributeName="height"
                  begin="svgSpinnersBlocksWave0.begin+0.6s"
                  dur="0.9s"
                  values="7.33;1.33;7.33"
                />
              </rect>
            </svg>`;

const Copy = `<svg  viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="#ffffff"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round">
<path d="M96.1 575.7a32.2 32.1 0 1 0 64.4 0 32.2 32.1 0 1 0-64.4 0Z"></path>
<path d="M742.1 450.7l-269.5-2.1c-14.3-0.1-26 13.8-26 31s11.7 31.3 26 31.4l269.5 2.1c14.3 0.1 26-13.8 26-31s-11.7-31.3-26-31.4zM742.1 577.7l-269.5-2.1c-14.3-0.1-26 13.8-26 31s11.7 31.3 26 31.4l269.5 2.1c14.3 0.2 26-13.8 26-31s-11.7-31.3-26-31.4z"></path>
<path d="M736.1 63.9H417c-70.4 0-128 57.6-128 128h-64.9c-70.4 0-128 57.6-128 128v128c-0.1 17.7 14.4 32 32.2 32 17.8 0 32.2-14.4 32.2-32.1V320c0-35.2 28.8-64 64-64H289v447.8c0 70.4 57.6 128 128 128h255.1c-0.1 35.2-28.8 63.8-64 63.8H224.5c-35.2 0-64-28.8-64-64V703.5c0-17.7-14.4-32.1-32.2-32.1-17.8 0-32.3 14.4-32.3 32.1v128.3c0 70.4 57.6 128 128 128h384.1c70.4 0 128-57.6 128-128h65c70.4 0 128-57.6 128-128V255.9l-193-192z m0.1 63.4l127.7 128.3H800c-35.2 0-64-28.8-64-64v-64.3h0.2z m64 641H416.1c-35.2 0-64-28.8-64-64v-513c0-35.2 28.8-64 64-64H671V191c0 70.4 57.6 128 128 128h65.2v385.3c0 35.2-28.8 64-64 64z"></path>
</svg>`;

export {
  Exclamation,
  Home,
  About,
  Other,
  TreeHollow,
  MessageBoard,
  FriendshipLinks,
  Music,
  PhotoAlbum,
  FullScreen,
  NonFullScreen,
  Arrow,
  Refresh,
  Copy,
};
vue
<template>
  <div class="svg-render">
    <!-- 检测是否为SVG字符串(包含<svg标签) -->
    <template v-if="isSvgString(svg)">
      <div v-html="svg"></div>
    </template>
    <!-- 否则视为图片地址 -->
    <template v-else>
      <img :src="svg" :alt="alt || '菜单图标'" class="svg-image" />
    </template>
  </div>
</template>

<script setup lang="ts">
import { defineProps } from "vue";

// 定义Props类型
defineProps<{
  svg: string; // 可能是SVG字符串或图片地址
  color?: string;
  alt?: string; // 图片替代文本(可选)
}>();

// 判断是否为SVG字符串(简单检测是否包含<svg标签)
const isSvgString = (content: string) => {
  if (!content) return false;
  // 检测是否包含<svg标签(不区分大小写)
  return content.trim().startsWith("<svg");
};
</script>

<style scoped>
.svg-render {
}

/* 确保v-html插入的SVG正确显示 */
.svg-render :deep(svg) {
  width: 100%;
  height: 100%;
}
</style>
js
import { ref, onMounted, onUnmounted } from "vue";

// 检查是否在客户端环境
const isClient =
  typeof window !== "undefined" && typeof document !== "undefined";

// 定义 fullscreenApi 结构
let fullscreenApi = null;

if (isClient) {
  fullscreenApi = {
    request:
      document.documentElement.requestFullscreen ||
      document.documentElement.webkitRequestFullscreen ||
      document.documentElement.mozRequestFullScreen ||
      document.documentElement.msRequestFullscreen,

    exit: (
      document.exitFullscreen ||
      document.documentElement.webkitExitFullscreen ||
      document.documentElement.mozCancelFullScreen ||
      document.documentElement.msExitFullscreen
    )?.bind(document),

    element: () =>
      document.fullscreenElement ||
      document.webkitFullscreenElement ||
      document.mozFullScreenElement ||
      document.msFullscreenElement,

    changeEvent: () => {
      if (document.fullscreenElement !== undefined) return "fullscreenchange";
      if (document.webkitFullscreenElement !== undefined)
        return "webkitfullscreenchange";
      if (document.mozFullScreenElement !== undefined)
        return "mozfullscreenchange";
      if (document.msFullscreenElement !== undefined)
        return "MSFullscreenChange";
      return "fullscreenchange";
    },
  };
}

export function useFullscreen() {
  const isFullScreen = ref(false);
  // 用组件内部变量存储事件引用(替代window挂载)
  let eventRefs = {
    changeEvent: null,
    handleFullscreenChange: null,
    handleKeydown: null,
  };

  const checkFullscreen = () => {
    if (!isClient || !fullscreenApi) return false;
    return !!fullscreenApi.element();
  };

  const updateStatus = () => {
    if (!isClient || !fullscreenApi) return;
    isFullScreen.value = checkFullscreen();
  };

  const toggle = () => {
    if (!isClient || !fullscreenApi) return;

    if (checkFullscreen()) {
      fullscreenApi.exit?.();
    } else {
      const element = document.documentElement;
      fullscreenApi.request?.call(element);
    }
  };

  // 定义事件处理函数(用变量存储以便卸载)
  const handleFullscreenChange = () => {
    updateStatus();
  };

  const handleKeydown = (e) => {
    if (e.key === "F11" || e.keyCode === 122) {
      requestAnimationFrame(updateStatus);
    }
  };

  onMounted(() => {
    updateStatus();

    if (isClient && fullscreenApi) {
      const changeEvent = fullscreenApi.changeEvent();
      // 存储事件引用到内部变量
      eventRefs = {
        changeEvent,
        handleFullscreenChange,
        handleKeydown,
      };
      // 绑定事件
      document.addEventListener(changeEvent, handleFullscreenChange);
      window.addEventListener("keydown", handleKeydown);
    }
  });

  onUnmounted(() => {
    if (isClient) {
      // 从内部变量获取事件引用并卸载
      const { changeEvent, handleFullscreenChange, handleKeydown } = eventRefs;
      if (changeEvent && handleFullscreenChange) {
        document.removeEventListener(changeEvent, handleFullscreenChange);
      }
      if (handleKeydown) {
        window.removeEventListener("keydown", handleKeydown);
      }
      // 清空内部变量
      eventRefs = {
        changeEvent: null,
        handleFullscreenChange: null,
        handleKeydown: null,
      };
    }
  });

  return {
    isFullScreen: isFullScreen,
    toggleFullscreen: isClient ? toggle : () => {},
    updateFullscreenStatus: isClient ? updateStatus : () => {},
  };
}
ts
export const Website = {
  webInfo: {
    websiteName: "Hyde Blog",
  },
  menuInfo: [
    {
      text: "首页",
      svg: `
            <svg viewBox="0 0 24 24" width="24" height="24" :stroke="themeColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round">
              <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
              <polyline points="9 22 9 12 15 12 15 22"></polyline>
            </svg>`,
      path: "/",
    },
    {
      text: "首页",
      svg: "",
      path: "/",
      subMenuInfo: [
        {
          text: "树洞",
          svg: "",
        },
        {
          text: "留言板",
          svg: "",
        },
      ],
    },
  ],
};

注册组件

  • docs\.vitepress\theme\components\TeekLayoutProvider.vue文件中注册组件
vue
<script setup lang="ts" name="TeekLayoutProvider">
import ContextMenu from "./ContextMenu/ContextMenu.vue";
// 配置复制钩子事件使用
import { useCopyEvent } from "../composables/useCopyEvent";

// 初始化复制事件监听
onMounted(() => {
  useCopyEvent();
});

<template>
  <Teek.Layout>
    <template #layout-top>
      <!-- 需要进行ssr优化 -->
      <ClientOnly>
        <!-- 添加全局右键菜单 -->
        <ContextMenu />
      </ClientOnly>
    </template>
  </Teek.Layout>
</template>
<script>
最近更新