惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Y
Y Combinator Blog
美团技术团队
H
Hacker News: Front Page
Spread Privacy
Spread Privacy
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Tenable Blog
Simon Willison's Weblog
Simon Willison's Weblog
T
The Exploit Database - CXSecurity.com
Cisco Talos Blog
Cisco Talos Blog
A
Arctic Wolf
C
CXSECURITY Database RSS Feed - CXSecurity.com
Application and Cybersecurity Blog
Application and Cybersecurity Blog
A
About on SuperTechFans
F
Fortinet All Blogs
量子位
GbyAI
GbyAI
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
The Hacker News
The Hacker News
AWS News Blog
AWS News Blog
Forbes - Security
Forbes - Security
Help Net Security
Help Net Security
I
InfoQ
有赞技术团队
有赞技术团队
W
WeLiveSecurity
Google DeepMind News
Google DeepMind News
Engineering at Meta
Engineering at Meta
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
S
Secure Thoughts
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Webroot Blog
Webroot Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园_首页
C
Check Point Blog
T
Troy Hunt's Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
Latest news
Latest news
P
Proofpoint News Feed
Jina AI
Jina AI
Last Week in AI
Last Week in AI
Martin Fowler
Martin Fowler
雷峰网
雷峰网
博客园 - Franky
L
LangChain Blog
罗磊的独立博客
Blog — PlanetScale
Blog — PlanetScale
Google DeepMind News
Google DeepMind News
D
Docker
G
GRAHAM CLULEY
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC

Hyde Blog

Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog 关于我 关于我 Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog
Hyde Blog
Hyde · 2025-10-05 · via Hyde Blog

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,
};

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: "",
        },
      ],
    },
  ],
};