00:00:00
右键菜单组件
新增组件
- 在
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>