From d3e0eddff74d67e02ef93eedc8f8dae086fb9edf Mon Sep 17 00:00:00 2001 From: lsy Date: Sat, 3 May 2025 19:50:03 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=86=E4=BB=A3=E7=A0=81=E9=AB=98=E4=BA=AE?= =?UTF-8?q?=E6=94=B9=E4=B8=BAastro=E9=9B=86=E6=88=90=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=EF=BC=8Cmermaid=E6=94=B9=E4=B8=BA=E5=AE=A2=E6=88=B7=E7=AB=AF?= =?UTF-8?q?=E7=94=9F=E6=88=90=EF=BC=8C=E4=BC=98=E5=8C=96=E5=AF=BC=E8=88=AA?= =?UTF-8?q?=E6=A0=8F=E6=A0=B7=E5=BC=8F=EF=BC=8C=E5=B0=86=E5=9C=B0=E5=9B=BE?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=8A=A8=E6=80=81=E5=AF=BC=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astro.config.mjs | 39 +- package-lock.json | 11 +- package.json | 2 +- {src/assets/map => public/maps}/china.json | 0 {src/assets/map => public/maps}/world.zh.json | 0 src/components/Breadcrumb.astro | 2 +- src/components/Header.astro | 6 +- src/components/Layout.astro | 8 +- src/components/ThemeToggle.astro | 7 +- src/components/WorldHeatmap.tsx | 1091 ++++++------- src/content/理解计算机/rust/基础语法.md | 3 + src/pages/articles/[...id].astro | 410 +++-- src/plugins/custom-code-blocks.js | 1377 ----------------- src/styles/articles-code.css | 200 +++ src/styles/articles-mermaid.css | 150 ++ .../{table-styles.css => articles-table.css} | 0 .../{content-styles.css => articles.css} | 44 +- src/styles/code-blocks.css | 537 ------- src/styles/mermaid-themes.css | 231 --- 19 files changed, 1266 insertions(+), 2852 deletions(-) rename {src/assets/map => public/maps}/china.json (100%) rename {src/assets/map => public/maps}/world.zh.json (100%) delete mode 100644 src/plugins/custom-code-blocks.js create mode 100644 src/styles/articles-code.css create mode 100644 src/styles/articles-mermaid.css rename src/styles/{table-styles.css => articles-table.css} (100%) rename src/styles/{content-styles.css => articles.css} (87%) delete mode 100644 src/styles/code-blocks.css delete mode 100644 src/styles/mermaid-themes.css diff --git a/astro.config.mjs b/astro.config.mjs index f980396..abe6bab 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -14,7 +14,6 @@ import { SITE_URL } from "./src/consts"; import compressor from "astro-compressor"; import vercel from "@astrojs/vercel"; import { articleIndexerIntegration } from "./src/plugins/build-article-index.js"; -import customCodeBlocksIntegration from "./src/plugins/custom-code-blocks.js"; function getArticleDate(articleId) { try { @@ -52,21 +51,15 @@ export default defineConfig({ }, integrations: [ - // 使用我们自己的代码块集成替代expressiveCode - customCodeBlocksIntegration(), - - // MDX 集成配置 + // 使用Astro官方的MDX支持 mdx(), swup({ cache: true, preload: true, - } - - ), + }), react(), - // 使用我们自己的文章索引生成器(替换pagefind) + // 使用文章索引生成器 articleIndexerIntegration(), - sitemap({ filter: (page) => !page.includes("/api/"), serialize(item) { @@ -109,9 +102,24 @@ export default defineConfig({ compressor() ], - // Markdown 配置 + // Markdown 配置 - 使用官方语法高亮 markdown: { - syntaxHighlight: false, // 禁用默认的语法高亮,使用我们自定义的高亮 + // 配置语法高亮 + syntaxHighlight: { + // 使用shiki作为高亮器 + type: 'shiki', + // 排除mermaid语言,不进行高亮处理 + excludeLangs: ['mermaid'] + }, + // Shiki主题配置 + shikiConfig: { + theme: 'github-light', + themes: { + light: 'github-light', + dark: 'github-dark' + }, + wrap: true + }, remarkPlugins: [ [remarkEmoji, { emoticon: false, padded: true }] ], @@ -119,13 +127,6 @@ export default defineConfig({ [rehypeExternalLinks, { target: '_blank', rel: ['nofollow', 'noopener', 'noreferrer'] }] ], gfm: true, - // 设置 remark-rehype 选项,以控制HTML处理 - remarkRehype: { - // 保留原始HTML格式,但仅在非代码块区域 - allowDangerousHtml: true, - // 确保代码块内容不被解析 - passThrough: ['code'] - }, }, adapter: vercel(), diff --git a/package-lock.json b/package-lock.json index f51c467..5475cce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ "astro": "^5.7.5", "astro-expressive-code": "^0.41.2", "cheerio": "^1.0.0", - "highlight.js": "^11.11.1", + "mermaid": "^11.6.0", "node-fetch": "^3.3.2", "octokit": "^4.1.3", "puppeteer": "^23.11.1", @@ -9955,15 +9955,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/highlight.js": { - "version": "11.11.1", - "resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz", - "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/html-escaper": { "version": "3.0.3", "resolved": "https://registry.npmmirror.com/html-escaper/-/html-escaper-3.0.3.tgz", diff --git a/package.json b/package.json index 6337071..4412e76 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "astro": "^5.7.5", "astro-expressive-code": "^0.41.2", "cheerio": "^1.0.0", - "highlight.js": "^11.11.1", + "mermaid": "^11.6.0", "node-fetch": "^3.3.2", "octokit": "^4.1.3", "puppeteer": "^23.11.1", diff --git a/src/assets/map/china.json b/public/maps/china.json similarity index 100% rename from src/assets/map/china.json rename to public/maps/china.json diff --git a/src/assets/map/world.zh.json b/public/maps/world.zh.json similarity index 100% rename from src/assets/map/world.zh.json rename to public/maps/world.zh.json diff --git a/src/components/Breadcrumb.astro b/src/components/Breadcrumb.astro index 2152e60..fce9d71 100644 --- a/src/components/Breadcrumb.astro +++ b/src/components/Breadcrumb.astro @@ -29,7 +29,7 @@ const breadcrumbs: Breadcrumb[] = pathSegments }); --- -
+
@@ -130,7 +130,7 @@ const navSelectorClassName = "mr-4";
-
+
{item.items.map((subItem) => ( diff --git a/src/components/ThemeToggle.astro b/src/components/ThemeToggle.astro index 039b1ad..c8b49b1 100644 --- a/src/components/ThemeToggle.astro +++ b/src/components/ThemeToggle.astro @@ -454,7 +454,7 @@ const { callback(); // 确保DOM已更新 - document.documentElement.classList.toggle('dark', toTheme === 'dark'); + document.documentElement.dataset.theme = toTheme; }); // 生成动画需要的SVG资源 @@ -632,10 +632,6 @@ const { } else { document.documentElement.dataset.theme = "light"; } - - // 确保同步类名 - document.documentElement.classList.toggle('dark', - document.documentElement.dataset.theme === 'dark'); }; // 切换主题 @@ -714,7 +710,6 @@ const { if (!localStorage.getItem("theme")) { const newTheme = e.matches ? "dark" : "light"; document.documentElement.dataset.theme = newTheme; - document.documentElement.classList.toggle('dark', e.matches); } }; diff --git a/src/components/WorldHeatmap.tsx b/src/components/WorldHeatmap.tsx index 8388af0..7450744 100644 --- a/src/components/WorldHeatmap.tsx +++ b/src/components/WorldHeatmap.tsx @@ -1,7 +1,29 @@ import React, { useEffect, useRef, useState } from "react"; -import * as THREE from "three"; -import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; -import { CSS2DRenderer } from "three/examples/jsm/renderers/CSS2DRenderer.js"; +import { + Scene, + PerspectiveCamera, + WebGLRenderer, + SphereGeometry, + MeshBasicMaterial, + Mesh, + AmbientLight, + DirectionalLight, + Vector2, + Vector3, + Raycaster, + Group, + BufferGeometry, + LineBasicMaterial, + Line, + FrontSide +} from "three"; +import type { Side } from "three"; + +// 需要懒加载的模块 +const loadControlsAndRenderers = () => Promise.all([ + import("three/examples/jsm/controls/OrbitControls.js"), + import("three/examples/jsm/renderers/CSS2DRenderer.js") +]); // WASM模块接口 interface GeoWasmModule { @@ -45,25 +67,26 @@ const WorldHeatmap: React.FC = ({ visitedPlaces }) => { const [mapLoading, setMapLoading] = useState(true); const [mapError, setMapError] = useState(null); + // 修改场景引用类型以适应新的导入方式 const sceneRef = useRef<{ - scene: THREE.Scene; - camera: THREE.PerspectiveCamera; - renderer: THREE.WebGLRenderer; - labelRenderer: CSS2DRenderer; - controls: OrbitControls; - earth: THREE.Mesh; - countries: Map; - raycaster: THREE.Raycaster; - mouse: THREE.Vector2; + scene: Scene; + camera: PerspectiveCamera; + renderer: WebGLRenderer; + labelRenderer: any; // 后面会动态设置具体类型 + controls: any; // 后面会动态设置具体类型 + earth: Mesh; + countries: Map; + raycaster: Raycaster; + mouse: Vector2; animationId: number | null; - lastCameraPosition: THREE.Vector3 | null; + lastCameraPosition: Vector3 | null; lastMouseEvent: MouseEvent | null; lastClickedCountry: string | null; lastMouseX: number | null; lastMouseY: number | null; lastHoverTime: number | null; - lineToCountryMap: Map; - allLineObjects: THREE.Line[]; + lineToCountryMap: Map; + allLineObjects: Line[]; } | null>(null); // 监听主题变化 @@ -104,22 +127,29 @@ const WorldHeatmap: React.FC = ({ visitedPlaces }) => { }; }, []); - // 动态加载地图数据 + // 从公共目录加载地图数据 useEffect(() => { const loadMapData = async () => { try { setMapLoading(true); setMapError(null); - // 并行加载两个地图数据 - const [worldDataModule, chinaDataModule] = await Promise.all([ - import("@/assets/map/world.zh.json"), - import("@/assets/map/china.json") + // 从公共目录加载地图数据 + const [worldDataResponse, chinaDataResponse] = await Promise.all([ + fetch('/maps/world.zh.json'), + fetch('/maps/china.json') ]); + if (!worldDataResponse.ok || !chinaDataResponse.ok) { + throw new Error('无法获取地图数据'); + } + + const worldData = await worldDataResponse.json(); + const chinaData = await chinaDataResponse.json(); + setMapData({ - worldData: worldDataModule.default || worldDataModule, - chinaData: chinaDataModule.default || chinaDataModule + worldData, + chinaData }); setMapLoading(false); @@ -196,509 +226,546 @@ const WorldHeatmap: React.FC = ({ visitedPlaces }) => { containerRef.current.innerHTML = ""; } - // 检查当前是否为暗色模式 - const isDarkMode = - document.documentElement.classList.contains("dark") || - document.documentElement.getAttribute("data-theme") === "dark"; - - // 根据当前模式设置颜色 - const getColors = () => { - return { - earthBase: isDarkMode ? "#111827" : "#2a4d69", // 深色模式保持深色,浅色模式改为更柔和的蓝色 - visited: isDarkMode ? "#065f46" : "#34d399", // 访问过的颜色更鲜明 - border: isDarkMode ? "#6b7280" : "#e0e0e0", // 边界颜色调整为更亮的浅灰色 - visitedBorder: isDarkMode ? "#10b981" : "#0d9488", // 访问过的边界颜色 - chinaBorder: isDarkMode ? "#f87171" : "#ef4444", // 中国边界使用红色 - text: isDarkMode ? "#f9fafb" : "#1f2937", // 文本颜色对比更强 - highlight: isDarkMode ? "#fcd34d" : "#60a5fa", // 高亮颜色改为浅蓝色,更配合背景 - }; - }; - - const colors = getColors(); - - // 创建场景 - const scene = new THREE.Scene(); - scene.background = null; - - // 创建材质的辅助函数 - const createMaterial = ( - color: string, - side: THREE.Side = THREE.FrontSide, - opacity: number = 1.0, - ) => { - return new THREE.MeshBasicMaterial({ - color: color, - side: side, - transparent: true, - opacity: opacity, - }); - }; - - // 创建地球几何体 - const earthGeometry = new THREE.SphereGeometry(2.0, 64, 64); - const earthMaterial = createMaterial( - colors.earthBase, - THREE.FrontSide, - isDarkMode ? 0.9 : 0.9, // 调整明亮模式下的不透明度 - ); - const earth = new THREE.Mesh(earthGeometry, earthMaterial); - earth.renderOrder = 1; - scene.add(earth); - - // 添加光源 - const ambientLight = new THREE.AmbientLight( - 0xffffff, - isDarkMode ? 0.7 : 0.85, // 微调明亮模式下的光照强度 - ); - scene.add(ambientLight); - - const directionalLight = new THREE.DirectionalLight( - isDarkMode ? 0xeeeeff : 0xffffff, // 恢复明亮模式下的纯白光源 - isDarkMode ? 0.6 : 0.65, // 微调明亮模式下的定向光强度 - ); - directionalLight.position.set(5, 3, 5); - scene.add(directionalLight); - - // 创建相机 - const camera = new THREE.PerspectiveCamera( - 45, - containerRef.current.clientWidth / containerRef.current.clientHeight, - 0.1, - 1000, - ); - camera.position.z = 8; - - // 创建渲染器 - const renderer = new THREE.WebGLRenderer({ - antialias: true, - alpha: true, - logarithmicDepthBuffer: true, - preserveDrawingBuffer: true, - precision: "highp", - }); - renderer.sortObjects = true; - renderer.setClearColor(0x000000, 0); - renderer.setPixelRatio(window.devicePixelRatio); - renderer.setSize( - containerRef.current.clientWidth, - containerRef.current.clientHeight, - ); - containerRef.current.appendChild(renderer.domElement); - - // 创建CSS2D渲染器用于标签 - const labelRenderer = new CSS2DRenderer(); - labelRenderer.setSize( - containerRef.current.clientWidth, - containerRef.current.clientHeight, - ); - labelRenderer.domElement.style.position = "absolute"; - labelRenderer.domElement.style.top = "0"; - labelRenderer.domElement.style.pointerEvents = "none"; - containerRef.current.appendChild(labelRenderer.domElement); - - // 添加控制器 - const controls = new OrbitControls(camera, renderer.domElement); - controls.enableDamping = true; - controls.dampingFactor = 0.25; // 大幅增加阻尼因子,从0.1到0.25提高稳定性 - controls.rotateSpeed = 0.2; // 降低旋转速度,提高稳定性 - controls.autoRotate = true; - controls.autoRotateSpeed = 0.3; // 降低自动旋转速度 - controls.minDistance = 5; - controls.maxDistance = 15; - - controls.minPolarAngle = Math.PI * 0.1; - controls.maxPolarAngle = Math.PI * 0.9; - - controls.addEventListener("change", () => { - if (sceneRef.current) { - renderer.render(scene, camera); - labelRenderer.render(scene, camera); - } - }); - - // 保存所有线条对象的引用,用于快速检测 - const allLineObjects: THREE.Line[] = []; - const lineToCountryMap = new Map(); + // 创建一个引用,用于清理函数 + let mounted = true; + let cleanupFunctions: Array<() => void> = []; - // 创建国家边界组 - const countryGroup = new THREE.Group(); - earth.add(countryGroup); - - // 创建国家边界 - const countries = new Map(); - - // 从WASM获取边界线数据 - const boundaryLines = geoProcessor.get_boundary_lines(); - - // 处理边界线数据 - if (boundaryLines) { - // 遍历所有边界线 - for (const boundaryLine of boundaryLines) { - const { points, region_name, is_visited } = boundaryLine; + // 动态加载Three.js控制器和渲染器 + const initThreeScene = async () => { + try { + // 加载OrbitControls和CSS2DRenderer + const [{ OrbitControls }, { CSS2DRenderer }] = await loadControlsAndRenderers(); - // 创建区域组 - const regionObject = new THREE.Group(); - regionObject.userData = { name: region_name, isVisited: is_visited }; - - // 转换点数组为THREE.Vector3数组 - const threePoints = points.map((p: { x: number; y: number; z: number }) => new THREE.Vector3(p.x, p.y, p.z)); - - // 创建边界线 - if (threePoints.length > 1) { - const lineGeometry = new THREE.BufferGeometry().setFromPoints(threePoints); - - // 确定线条颜色 - const isChina = region_name === "中国" || region_name.startsWith("中国-"); - let borderColor; - - if (is_visited) { - // 已访问的地区,包括中国城市,都使用绿色边界 - borderColor = colors.visitedBorder; - } else if (isChina) { - // 未访问的中国和中国区域使用红色边界 - borderColor = colors.chinaBorder; - } else { - // 其他未访问区域使用默认边界颜色 - borderColor = colors.border; - } - - const lineMaterial = new THREE.LineBasicMaterial({ - color: borderColor, - linewidth: is_visited ? 1.8 : 1.2, // 微调线条宽度,保持已访问区域更明显 - transparent: true, - opacity: is_visited ? 0.95 : 0.85, // 调整不透明度,使边界明显但不突兀 - }); - - const line = new THREE.Line(lineGeometry, lineMaterial); - line.userData = { - name: region_name, - isVisited: is_visited, - originalColor: borderColor, - highlightColor: colors.highlight, + // 如果组件已卸载,退出初始化 + if (!mounted || !containerRef.current) return; + + // 检查当前是否为暗色模式 + const isDarkMode = + document.documentElement.classList.contains("dark") || + document.documentElement.getAttribute("data-theme") === "dark"; + + // 根据当前模式设置颜色 + const getColors = () => { + return { + earthBase: isDarkMode ? "#111827" : "#2a4d69", // 深色模式保持深色,浅色模式改为更柔和的蓝色 + visited: isDarkMode ? "#065f46" : "#34d399", // 访问过的颜色更鲜明 + border: isDarkMode ? "#6b7280" : "#e0e0e0", // 边界颜色调整为更亮的浅灰色 + visitedBorder: isDarkMode ? "#10b981" : "#0d9488", // 访问过的边界颜色 + chinaBorder: isDarkMode ? "#f87171" : "#ef4444", // 中国边界使用红色 + text: isDarkMode ? "#f9fafb" : "#1f2937", // 文本颜色对比更强 + highlight: isDarkMode ? "#fcd34d" : "#60a5fa", // 高亮颜色改为浅蓝色,更配合背景 }; - - // 设置渲染顺序 - line.renderOrder = is_visited ? 3 : 2; - regionObject.add(line); - - // 保存线条对象引用和对应的区域名称 - allLineObjects.push(line); - lineToCountryMap.set(line, region_name); - } + }; + + const colors = getColors(); + + // 创建场景 + const scene = new Scene(); + scene.background = null; + + // 创建材质的辅助函数 + const createMaterial = ( + color: string, + side: Side = FrontSide, + opacity: number = 1.0, + ) => { + return new MeshBasicMaterial({ + color: color, + side: side, + transparent: true, + opacity: opacity, + }); + }; + + // 创建地球几何体 + const earthGeometry = new SphereGeometry(2.0, 64, 64); + const earthMaterial = createMaterial( + colors.earthBase, + FrontSide, + isDarkMode ? 0.9 : 0.9, // 调整明亮模式下的不透明度 + ); + const earth = new Mesh(earthGeometry, earthMaterial); + earth.renderOrder = 1; + scene.add(earth); + + // 添加光源 + const ambientLight = new AmbientLight( + 0xffffff, + isDarkMode ? 0.7 : 0.85, // 微调明亮模式下的光照强度 + ); + scene.add(ambientLight); + + const directionalLight = new DirectionalLight( + isDarkMode ? 0xeeeeff : 0xffffff, // 恢复明亮模式下的纯白光源 + isDarkMode ? 0.6 : 0.65, // 微调明亮模式下的定向光强度 + ); + directionalLight.position.set(5, 3, 5); + scene.add(directionalLight); + + // 创建相机 + const camera = new PerspectiveCamera( + 45, + containerRef.current.clientWidth / containerRef.current.clientHeight, + 0.1, + 1000, + ); + camera.position.z = 8; + + // 创建渲染器 + const renderer = new WebGLRenderer({ + antialias: true, + alpha: true, + logarithmicDepthBuffer: true, + preserveDrawingBuffer: true, + precision: "highp", + }); + renderer.sortObjects = true; + renderer.setClearColor(0x000000, 0); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize( + containerRef.current.clientWidth, + containerRef.current.clientHeight, + ); + containerRef.current.appendChild(renderer.domElement); + + // 创建CSS2D渲染器用于标签 + const labelRenderer = new CSS2DRenderer(); + labelRenderer.setSize( + containerRef.current.clientWidth, + containerRef.current.clientHeight, + ); + labelRenderer.domElement.style.position = "absolute"; + labelRenderer.domElement.style.top = "0"; + labelRenderer.domElement.style.pointerEvents = "none"; + containerRef.current.appendChild(labelRenderer.domElement); + + // 添加控制器 + const controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = true; + controls.dampingFactor = 0.25; // 大幅增加阻尼因子,从0.1到0.25提高稳定性 + controls.rotateSpeed = 0.2; // 降低旋转速度,提高稳定性 + controls.autoRotate = true; + controls.autoRotateSpeed = 0.3; // 降低自动旋转速度 + controls.minDistance = 5; + controls.maxDistance = 15; + + controls.minPolarAngle = Math.PI * 0.1; + controls.maxPolarAngle = Math.PI * 0.9; + + controls.addEventListener("change", () => { + if (sceneRef.current) { + renderer.render(scene, camera); + labelRenderer.render(scene, camera); + } + }); + + // 保存所有线条对象的引用,用于快速检测 + const allLineObjects: Line[] = []; + const lineToCountryMap = new Map(); - // 添加区域对象到国家组 - countryGroup.add(regionObject); - countries.set(region_name, regionObject); - } - } - - // 将视图旋转到中国位置 - const positionCameraToFaceChina = () => { - // 检查是否为小屏幕 - const isSmallScreen = - containerRef.current && containerRef.current.clientWidth < 640; - - // 根据屏幕大小设置不同的相机初始位置 - let fixedPosition; - if (isSmallScreen) { - // 小屏幕显示距离更远,以便看到更多地球 - fixedPosition = new THREE.Vector3(-2.1, 3.41, -8.0); - } else { - // 大屏幕使用原来的位置 - fixedPosition = new THREE.Vector3(-2.1, 3.41, -6.5); - } - - // 应用位置 - camera.position.copy(fixedPosition); - camera.lookAt(0, 0, 0); - controls.update(); - - // 确保自动旋转始终开启 - controls.autoRotate = true; - - // 渲染 - renderer.render(scene, camera); - labelRenderer.render(scene, camera); - }; - - // 应用初始相机位置 - positionCameraToFaceChina(); - - // 创建射线投射器用于鼠标交互 - const raycaster = new THREE.Raycaster(); - const mouse = new THREE.Vector2(); - - // 添加节流函数,限制鼠标移动事件的触发频率 - const throttle = (func: Function, limit: number) => { - let inThrottle: boolean = false; - let lastFunc: number | null = null; - let lastRan: number | null = null; - - return function (this: any, ...args: any[]) { - if (!inThrottle) { - func.apply(this, args); - inThrottle = true; - lastRan = Date.now(); - setTimeout(() => (inThrottle = false), limit); - } else { - // 取消之前的延迟调用 - if (lastFunc) clearTimeout(lastFunc); - - // 如果距离上次执行已经接近阈值,确保我们能及时处理下一个事件 - const sinceLastRan = Date.now() - (lastRan || 0); - if (sinceLastRan >= limit * 0.8) { - lastFunc = window.setTimeout(() => { - if (lastRan && Date.now() - lastRan >= limit) { - func.apply(this, args); - lastRan = Date.now(); + // 创建国家边界组 + const countryGroup = new Group(); + earth.add(countryGroup); + + // 创建国家边界 + const countries = new Map(); + + // 从WASM获取边界线数据 + const boundaryLines = geoProcessor.get_boundary_lines(); + + // 处理边界线数据 + if (boundaryLines) { + // 遍历所有边界线 + for (const boundaryLine of boundaryLines) { + const { points, region_name, is_visited } = boundaryLine; + + // 创建区域组 + const regionObject = new Group(); + regionObject.userData = { name: region_name, isVisited: is_visited }; + + // 转换点数组为Vector3数组 + const threePoints = points.map((p: { x: number; y: number; z: number }) => new Vector3(p.x, p.y, p.z)); + + // 创建边界线 + if (threePoints.length > 1) { + const lineGeometry = new BufferGeometry().setFromPoints(threePoints); + + // 确定线条颜色 + const isChina = region_name === "中国" || region_name.startsWith("中国-"); + let borderColor; + + if (is_visited) { + // 已访问的地区,包括中国城市,都使用绿色边界 + borderColor = colors.visitedBorder; + } else if (isChina) { + // 未访问的中国和中国区域使用红色边界 + borderColor = colors.chinaBorder; + } else { + // 其他未访问区域使用默认边界颜色 + borderColor = colors.border; } - }, Math.max(limit - sinceLastRan, 0)); + + const lineMaterial = new LineBasicMaterial({ + color: borderColor, + linewidth: is_visited ? 1.8 : 1.2, // 微调线条宽度,保持已访问区域更明显 + transparent: true, + opacity: is_visited ? 0.95 : 0.85, // 调整不透明度,使边界明显但不突兀 + }); + + const line = new Line(lineGeometry, lineMaterial); + line.userData = { + name: region_name, + isVisited: is_visited, + originalColor: borderColor, + highlightColor: colors.highlight, + }; + + // 设置渲染顺序 + line.renderOrder = is_visited ? 3 : 2; + regionObject.add(line); + + // 保存线条对象引用和对应的区域名称 + allLineObjects.push(line); + lineToCountryMap.set(line, region_name); + } + + // 添加区域对象到国家组 + countryGroup.add(regionObject); + countries.set(region_name, regionObject); } } - }; - }; - - // 简化的鼠标移动事件处理函数 - const onMouseMove = throttle((event: MouseEvent) => { - if (!containerRef.current || !sceneRef.current || !geoProcessor) { - return; - } - - // 获取鼠标在球面上的点 - const result = getPointOnSphere( - event.clientX, - event.clientY, - camera, - 2.01, - ); - - // 重置所有线条颜色 - allLineObjects.forEach((line) => { - if (line.material instanceof THREE.LineBasicMaterial) { - line.material.color.set(line.userData.originalColor); - } - }); - - // 如果找到点和对应的国家/地区 - if (result && result.countryName) { - // 高亮显示该国家/地区的线条 - allLineObjects.forEach((line) => { - if ( - lineToCountryMap.get(line) === result.countryName && - line.material instanceof THREE.LineBasicMaterial - ) { - line.material.color.set(line.userData.highlightColor); + + // 将视图旋转到中国位置 + const positionCameraToFaceChina = () => { + // 检查是否为小屏幕 + const isSmallScreen = + containerRef.current && containerRef.current.clientWidth < 640; + + // 根据屏幕大小设置不同的相机初始位置 + let fixedPosition; + if (isSmallScreen) { + // 小屏幕显示距离更远,以便看到更多地球 + fixedPosition = new Vector3(-2.1, 3.41, -8.0); + } else { + // 大屏幕使用原来的位置 + fixedPosition = new Vector3(-2.1, 3.41, -6.5); } - }); - - // 更新悬停国家 - if (result.countryName !== hoveredCountry) { - setHoveredCountry(result.countryName); - } - - // 不禁用自动旋转,保持地球旋转 - } else { - // 如果没有找到国家/地区,清除悬停状态 - if (hoveredCountry) { + + // 应用位置 + camera.position.copy(fixedPosition); + camera.lookAt(0, 0, 0); + controls.update(); + + // 确保自动旋转始终开启 + controls.autoRotate = true; + + // 渲染 + renderer.render(scene, camera); + labelRenderer.render(scene, camera); + }; + + // 应用初始相机位置 + positionCameraToFaceChina(); + + // 创建射线投射器用于鼠标交互 + const raycaster = new Raycaster(); + const mouse = new Vector2(); + + // 添加节流函数,限制鼠标移动事件的触发频率 + const throttle = (func: Function, limit: number) => { + let inThrottle: boolean = false; + let lastFunc: number | null = null; + let lastRan: number | null = null; + + return function (this: any, ...args: any[]) { + if (!inThrottle) { + func.apply(this, args); + inThrottle = true; + lastRan = Date.now(); + setTimeout(() => (inThrottle = false), limit); + } else { + // 取消之前的延迟调用 + if (lastFunc) clearTimeout(lastFunc); + + // 如果距离上次执行已经接近阈值,确保我们能及时处理下一个事件 + const sinceLastRan = Date.now() - (lastRan || 0); + if (sinceLastRan >= limit * 0.8) { + lastFunc = window.setTimeout(() => { + if (lastRan && Date.now() - lastRan >= limit) { + func.apply(this, args); + lastRan = Date.now(); + } + }, Math.max(limit - sinceLastRan, 0)); + } + } + }; + }; + + // 简化的鼠标移动事件处理函数 + const onMouseMove = throttle((event: MouseEvent) => { + if (!containerRef.current || !sceneRef.current || !geoProcessor) { + return; + } + + // 获取鼠标在球面上的点 + const result = getPointOnSphere( + event.clientX, + event.clientY, + camera, + 2.01, + ); + + // 重置所有线条颜色 + allLineObjects.forEach((line) => { + if (line.material instanceof LineBasicMaterial) { + line.material.color.set(line.userData.originalColor); + } + }); + + // 如果找到点和对应的国家/地区 + if (result && result.countryName) { + // 高亮显示该国家/地区的线条 + allLineObjects.forEach((line) => { + if ( + lineToCountryMap.get(line) === result.countryName && + line.material instanceof LineBasicMaterial + ) { + line.material.color.set(line.userData.highlightColor); + } + }); + + // 更新悬停国家 + if (result.countryName !== hoveredCountry) { + setHoveredCountry(result.countryName); + } + + // 不禁用自动旋转,保持地球旋转 + } else { + // 如果没有找到国家/地区,清除悬停状态 + if (hoveredCountry) { + setHoveredCountry(null); + } + } + + // 保存鼠标事件和位置 + sceneRef.current.lastMouseEvent = event; + sceneRef.current.lastMouseX = event.clientX; + sceneRef.current.lastMouseY = event.clientY; + sceneRef.current.lastHoverTime = Date.now(); + }, 100); + + // 清除选择的函数 + const clearSelection = () => { + // 恢复所有线条的原始颜色 + allLineObjects.forEach((line) => { + if (line.material instanceof LineBasicMaterial) { + line.material.color.set(line.userData.originalColor); + } + }); + setHoveredCountry(null); - } - } - - // 保存鼠标事件和位置 - sceneRef.current.lastMouseEvent = event; - sceneRef.current.lastMouseX = event.clientX; - sceneRef.current.lastMouseY = event.clientY; - sceneRef.current.lastHoverTime = Date.now(); - }, 100); - - // 清除选择的函数 - const clearSelection = () => { - // 恢复所有线条的原始颜色 - allLineObjects.forEach((line) => { - if (line.material instanceof THREE.LineBasicMaterial) { - line.material.color.set(line.userData.originalColor); - } - }); - - setHoveredCountry(null); - if (sceneRef.current) { - sceneRef.current.lastClickedCountry = null; - sceneRef.current.lastHoverTime = null; - } - // 确保自动旋转始终开启 - controls.autoRotate = true; - }; - - // 简化的鼠标点击事件处理函数 - const onClick = (event: MouseEvent) => { - if (!containerRef.current || !sceneRef.current || !geoProcessor) { - return; - } - - // 获取鼠标在球面上的点 - const result = getPointOnSphere( - event.clientX, - event.clientY, - camera, - 2.01, - ); - - // 如果找到点和对应的国家/地区 - if (result && result.countryName) { - // 重置所有线条颜色 - allLineObjects.forEach((line) => { - if (line.material instanceof THREE.LineBasicMaterial) { - line.material.color.set(line.userData.originalColor); + if (sceneRef.current) { + sceneRef.current.lastClickedCountry = null; + sceneRef.current.lastHoverTime = null; + } + // 确保自动旋转始终开启 + controls.autoRotate = true; + }; + + // 简化的鼠标点击事件处理函数 + const onClick = (event: MouseEvent) => { + if (!containerRef.current || !sceneRef.current || !geoProcessor) { + return; + } + + // 获取鼠标在球面上的点 + const result = getPointOnSphere( + event.clientX, + event.clientY, + camera, + 2.01, + ); + + // 如果找到点和对应的国家/地区 + if (result && result.countryName) { + // 重置所有线条颜色 + allLineObjects.forEach((line) => { + if (line.material instanceof LineBasicMaterial) { + line.material.color.set(line.userData.originalColor); + } + }); + + // 高亮显示该国家/地区的线条 + allLineObjects.forEach((line) => { + if ( + lineToCountryMap.get(line) === result.countryName && + line.material instanceof LineBasicMaterial + ) { + line.material.color.set(line.userData.highlightColor); + } + }); + + // 更新选中国家 + setHoveredCountry(result.countryName); + sceneRef.current.lastClickedCountry = result.countryName; + // 不禁用自动旋转,保持地球始终旋转 + } else { + // 如果没有找到国家/地区,清除选择 + clearSelection(); + } + + // 更新最后的鼠标位置和点击时间 + sceneRef.current.lastMouseX = event.clientX; + sceneRef.current.lastMouseY = event.clientY; + sceneRef.current.lastHoverTime = Date.now(); + }; + + // 鼠标双击事件处理 + const onDoubleClick = (event: MouseEvent) => { + clearSelection(); + event.preventDefault(); + event.stopPropagation(); + }; + + // 添加事件监听器 + containerRef.current.addEventListener("mousemove", onMouseMove, { passive: true }); + containerRef.current.addEventListener("click", onClick, { passive: false }); + containerRef.current.addEventListener("dblclick", onDoubleClick, { passive: false }); + + // 保存清理函数 + cleanupFunctions.push(() => { + if (containerRef.current) { + containerRef.current.removeEventListener("mousemove", onMouseMove); + containerRef.current.removeEventListener("click", onClick); + containerRef.current.removeEventListener("dblclick", onDoubleClick); } }); - - // 高亮显示该国家/地区的线条 - allLineObjects.forEach((line) => { - if ( - lineToCountryMap.get(line) === result.countryName && - line.material instanceof THREE.LineBasicMaterial - ) { - line.material.color.set(line.userData.highlightColor); + + // 获取球面上的点对应的国家/地区 + const getPointOnSphere = ( + mouseX: number, + mouseY: number, + camera: PerspectiveCamera, + radius: number, + ): { point: Vector3, countryName: string | null } | null => { + // 计算鼠标在画布中的归一化坐标 + const rect = containerRef.current!.getBoundingClientRect(); + const x = ((mouseX - rect.left) / rect.width) * 2 - 1; + const y = -((mouseY - rect.top) / rect.height) * 2 + 1; + + // 创建射线 + const ray = new Raycaster(); + ray.setFromCamera(new Vector2(x, y), camera); + + // 检测射线与实际地球模型的相交 + const earthIntersects = ray.intersectObject(earth, false); + if (earthIntersects.length > 0) { + const point = earthIntersects[0].point; + + // 使用WASM查找最近的国家/地区 + const countryName = geoProcessor.find_nearest_country( + point.x, point.y, point.z, radius + ); + + return { point, countryName }; } + + // 如果没有直接相交,使用球体辅助检测 + const sphereGeom = new SphereGeometry(radius, 32, 32); + const sphereMesh = new Mesh(sphereGeom); + + const intersects = ray.intersectObject(sphereMesh); + if (intersects.length > 0) { + const point = intersects[0].point; + + // 使用WASM查找最近的国家/地区 + const countryName = geoProcessor.find_nearest_country( + point.x, point.y, point.z, radius + ); + + return { point, countryName }; + } + + return null; + }; + + // 简化的动画循环函数 + const animate = () => { + if (!sceneRef.current) return; + + // 更新控制器 + controls.update(); + + // 渲染 + renderer.render(scene, camera); + labelRenderer.render(scene, camera); + + // 请求下一帧 + sceneRef.current.animationId = requestAnimationFrame(animate); + }; + + // 处理窗口大小变化 + const handleResize = () => { + if (!containerRef.current || !sceneRef.current) return; + + const width = containerRef.current.clientWidth; + const height = containerRef.current.clientHeight; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + renderer.setSize(width, height); + labelRenderer.setSize(width, height); + + // 立即渲染一次 + renderer.render(scene, camera); + labelRenderer.render(scene, camera); + }; + + window.addEventListener("resize", handleResize, { passive: true }); + cleanupFunctions.push(() => { + window.removeEventListener("resize", handleResize); }); - - // 更新选中国家 - setHoveredCountry(result.countryName); - sceneRef.current.lastClickedCountry = result.countryName; - // 不禁用自动旋转,保持地球始终旋转 - } else { - // 如果没有找到国家/地区,清除选择 - clearSelection(); + + // 保存场景引用 + sceneRef.current = { + scene, + camera, + renderer, + labelRenderer, + controls, + earth, + countries, + raycaster, + mouse, + animationId: null, + lastCameraPosition: null, + lastMouseEvent: null, + lastClickedCountry: null, + lastMouseX: null, + lastMouseY: null, + lastHoverTime: null, + lineToCountryMap, + allLineObjects, + }; + + // 开始动画 + sceneRef.current.animationId = requestAnimationFrame(animate); + + } catch (error) { + console.error("Three.js初始化失败:", error); } - - // 更新最后的鼠标位置和点击时间 - sceneRef.current.lastMouseX = event.clientX; - sceneRef.current.lastMouseY = event.clientY; - sceneRef.current.lastHoverTime = Date.now(); }; - - // 鼠标双击事件处理 - const onDoubleClick = (event: MouseEvent) => { - clearSelection(); - event.preventDefault(); - event.stopPropagation(); - }; - - // 添加事件监听器 - containerRef.current.addEventListener("mousemove", onMouseMove, { passive: true }); - containerRef.current.addEventListener("click", onClick, { passive: false }); - containerRef.current.addEventListener("dblclick", onDoubleClick, { passive: false }); - - // 简化的动画循环函数 - const animate = () => { - if (!sceneRef.current) return; - - // 更新控制器 - sceneRef.current.controls.update(); - - // 渲染 - sceneRef.current.renderer.render(scene, camera); - sceneRef.current.labelRenderer.render(scene, camera); - - // 请求下一帧 - sceneRef.current.animationId = requestAnimationFrame(animate); - }; - - // 保存场景引用 - sceneRef.current = { - scene, - camera, - renderer, - labelRenderer, - controls, - earth, - countries, - raycaster, - mouse, - animationId: null, - lastCameraPosition: null, - lastMouseEvent: null, - lastClickedCountry: null, - lastMouseX: null, - lastMouseY: null, - lastHoverTime: null, - lineToCountryMap, - allLineObjects, - }; - - // 开始动画 - sceneRef.current.animationId = requestAnimationFrame(animate); - - // 获取球面上的点对应的国家/地区 - const getPointOnSphere = ( - mouseX: number, - mouseY: number, - camera: THREE.Camera, - radius: number, - ): { point: THREE.Vector3, countryName: string | null } | null => { - // 计算鼠标在画布中的归一化坐标 - const rect = containerRef.current!.getBoundingClientRect(); - const x = ((mouseX - rect.left) / rect.width) * 2 - 1; - const y = -((mouseY - rect.top) / rect.height) * 2 + 1; - - // 创建射线 - const ray = new THREE.Raycaster(); - ray.setFromCamera(new THREE.Vector2(x, y), camera); - - // 检测射线与实际地球模型的相交 - const earthIntersects = ray.intersectObject(earth, false); - if (earthIntersects.length > 0) { - const point = earthIntersects[0].point; - - // 使用WASM查找最近的国家/地区 - const countryName = geoProcessor.find_nearest_country( - point.x, point.y, point.z, radius - ); - - return { point, countryName }; - } - - // 如果没有直接相交,使用球体辅助检测 - const sphereGeom = new THREE.SphereGeometry(radius, 32, 32); - const sphereMesh = new THREE.Mesh(sphereGeom); - - const intersects = ray.intersectObject(sphereMesh); - if (intersects.length > 0) { - const point = intersects[0].point; - - // 使用WASM查找最近的国家/地区 - const countryName = geoProcessor.find_nearest_country( - point.x, point.y, point.z, radius - ); - - return { point, countryName }; - } - - return null; - }; - - // 处理窗口大小变化 - const handleResize = () => { - if (!containerRef.current || !sceneRef.current) return; - - const { camera, renderer, labelRenderer } = sceneRef.current; - const width = containerRef.current.clientWidth; - const height = containerRef.current.clientHeight; - - camera.aspect = width / height; - camera.updateProjectionMatrix(); - renderer.setSize(width, height); - labelRenderer.setSize(width, height); - - // 立即渲染一次 - renderer.render(sceneRef.current.scene, camera); - labelRenderer.render(sceneRef.current.scene, camera); - }; - - window.addEventListener("resize", handleResize, { passive: true }); + + // 执行初始化 + initThreeScene(); // 清理函数 return () => { + mounted = false; + + // 执行所有保存的清理函数 + cleanupFunctions.forEach(fn => fn()); + // 清理资源和事件监听器 if (sceneRef.current) { // 取消动画帧 @@ -721,16 +788,6 @@ const WorldHeatmap: React.FC = ({ visitedPlaces }) => { sceneRef.current.controls.dispose(); } } - - // 移除事件监听器 - if (containerRef.current) { - containerRef.current.removeEventListener("mousemove", onMouseMove); - containerRef.current.removeEventListener("click", onClick); - containerRef.current.removeEventListener("dblclick", onDoubleClick); - } - - // 移除窗口事件监听器 - window.removeEventListener("resize", handleResize); }; }, [visitedPlaces, theme, wasmReady, geoProcessor]); // 添加geoProcessor依赖 diff --git a/src/content/理解计算机/rust/基础语法.md b/src/content/理解计算机/rust/基础语法.md index 012c374..e42d71f 100644 --- a/src/content/理解计算机/rust/基础语法.md +++ b/src/content/理解计算机/rust/基础语法.md @@ -14,7 +14,10 @@ tags: [] 3. 如果变量没有使用可以前置下划线,消除警告 4. 强制类型转换 + ```rust let a = 3.1;let b = a as i32 + ``` + 5. 打印变量 ```rust diff --git a/src/pages/articles/[...id].astro b/src/pages/articles/[...id].astro index e9aceff..36c4632 100644 --- a/src/pages/articles/[...id].astro +++ b/src/pages/articles/[...id].astro @@ -4,7 +4,7 @@ import { getSpecialPath } from "@/content.config"; import Layout from "@/components/Layout.astro"; import Breadcrumb from "@/components/Breadcrumb.astro"; import { ARTICLE_EXPIRY_CONFIG } from "@/consts"; -import "@/styles/content-styles.css"; +import "@/styles/articles.css"; // 定义文章类型 interface ArticleEntry { @@ -499,106 +499,197 @@ const tableOfContents = generateTableOfContents(headings); listeners.length = 0; } - - // 1. 应用高亮代码 - function applyCodeHighlighting() { - const codeElements = document.querySelectorAll('code[data-highlighted]'); - - codeElements.forEach(codeElement => { - const highlightedCode = codeElement.getAttribute('data-highlighted'); - if (highlightedCode) { - codeElement.innerHTML = highlightedCode; - codeElement.removeAttribute('data-highlighted'); - } - }); - } - // 2. 加载Mermaid SVG内容 - function loadMermaidSvg() { - const mermaidContainers = document.querySelectorAll('.mermaid-figure [data-mermaid-src], .mermaid-svg-container[data-mermaid-src]'); + // 1. 增强代码块功能 - 添加标题栏、语言显示和复制按钮 + function enhanceCodeBlocks() { + // 查找所有代码块元素 + const codeBlocks = document.querySelectorAll('pre > code'); + if (codeBlocks.length === 0) return; - mermaidContainers.forEach(container => { - const svgSrc = container.getAttribute('data-mermaid-src'); - if (svgSrc) { - fetch(svgSrc) - .then(response => { - if (!response.ok) { - throw new Error('SVG加载失败: ' + response.status); - } - return response.text(); - }) - .then(svgContent => { - // 直接插入SVG内容,而不是作为图像 - container.innerHTML = svgContent; - - // 确保SVG元素正确应用样式 - const svgElement = container.querySelector('svg'); - if (svgElement) { - // 设置高度为auto以允许CSS控制 - svgElement.setAttribute('height', 'auto'); - - // 确保viewBox属性存在并正确设置 - if (!svgElement.hasAttribute('viewBox') || - !svgElement.getAttribute('viewBox')?.match(/^\d+\s+\d+\s+\d+\s+\d+$/)) { - // 如果没有有效的viewBox,尝试从宽高创建一个 - const width = svgElement.getAttribute('width'); - const height = svgElement.getAttribute('height'); - if (width && height && !isNaN(parseFloat(width)) && !isNaN(parseFloat(height))) { - svgElement.setAttribute('viewBox', `0 0 ${parseFloat(width)} ${parseFloat(height)}`); - } else { - // 使用默认viewBox - svgElement.setAttribute('viewBox', '0 0 100 100'); - } - } - - // 确保具有mermaid-svg类 - svgElement.classList.add('mermaid-svg'); - } - - // 移除data属性避免重复加载 - container.removeAttribute('data-mermaid-src'); - }) - .catch(error => { - console.error('加载SVG失败:', error); - container.innerHTML = '
图表加载失败
'; - }); + codeBlocks.forEach(codeBlock => { + const pre = codeBlock.parentElement; + // 如果已经处理过,跳过 + if (pre.parentElement.classList.contains('code-block-container')) return; + + // 获取语言类 - 简化语言提取逻辑 + let language = ''; + + // 从data-language属性中提取(最常见的方式) + language = pre.getAttribute('data-language') || codeBlock.getAttribute('data-language') || ''; + + // 如果没有找到语言,则默认为 'text' + if (!language || language === 'mermaid') { + // 跳过 mermaid 图表 + if (language === 'mermaid') return; + language = 'text'; } - }); - } - - // 3. 处理代码复制功能 - function setupCodeCopy() { - const handleCopyClick = (e) => { - const target = e.target; - const copyButton = target.closest('[data-copy]'); - if (!copyButton) return; - const container = copyButton.closest('.code-block-container'); - if (!container) return; + // 获取原始代码文本,保留换行和格式 + let originalCode = ''; + const hasLineElements = codeBlock.querySelectorAll('.line').length > 0 || + codeBlock.innerHTML.includes(''); - const codeElement = container.querySelector('pre code'); - if (!codeElement) return; + if (hasLineElements) { + // 从行元素中提取文本 + const lines = codeBlock.querySelectorAll('.line'); + if (lines.length > 0) { + originalCode = Array.from(lines) + .map(line => line.textContent) + .join('\n'); + } else { + // 尝试解析HTML中的行 + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = codeBlock.innerHTML; + const lineSpans = tempDiv.querySelectorAll('.line'); + originalCode = Array.from(lineSpans) + .map(span => span.textContent) + .join('\n'); + } + } else { + // 直接使用textContent (这种情况下可能没有特殊格式化) + originalCode = codeBlock.textContent || ''; + } - const code = codeElement.textContent || ''; + // 创建代码块容器 + const container = document.createElement('div'); + container.className = 'code-block-container'; - navigator.clipboard.writeText(code) - .then(() => { - const originalText = copyButton.textContent || ''; - copyButton.textContent = '已复制!'; - copyButton.disabled = true; + // 创建标题栏 + const header = document.createElement('div'); + header.className = 'code-block-header'; + + // 语言标签 + const langDiv = document.createElement('div'); + langDiv.className = 'code-block-lang'; + langDiv.innerHTML = ` + + + + + ${language.toUpperCase()} + `; + + // 复制按钮 + const copyButton = document.createElement('button'); + copyButton.className = 'code-block-copy'; + copyButton.innerHTML = ` + + + + + 复制 + `; + + // 代码内容容器 + const codeContent = document.createElement('div'); + codeContent.className = 'code-block-content'; + + // 添加复制功能 + addListener(copyButton, 'click', async () => { + try { + // 使用优化后的方法获取代码文本 + await navigator.clipboard.writeText(originalCode); + copyButton.classList.add('copied'); + copyButton.innerHTML = ` + + + + 已复制 + `; setTimeout(() => { - copyButton.textContent = originalText; - copyButton.disabled = false; + copyButton.classList.remove('copied'); + copyButton.innerHTML = ` + + + + + 复制 + `; }, 2000); - }) - .catch(err => { + } catch (err) { console.error('复制失败:', err); - alert('复制失败,请手动复制'); - }); - }; - - addListener(document, 'click', handleCopyClick); + copyButton.innerHTML = ` + + + + + + 失败 + `; + setTimeout(() => { + copyButton.innerHTML = ` + + + + + 复制 + `; + }, 2000); + } + }); + + // 组装标题栏 + header.appendChild(langDiv); + header.appendChild(copyButton); + + // 添加行号 + const preHasLineNumbers = pre.classList.contains('line-numbers') || pre.classList.contains('has-line-numbers'); + + // 不改变原有结构,只在外层包装 + // 保留原有的代码块,将其移入新容器 + pre.parentNode.insertBefore(container, pre); + container.appendChild(header); + container.appendChild(codeContent); + codeContent.appendChild(pre); + + // 始终添加行号 - 修改此部分,确保所有代码块都有行号 + if (!preHasLineNumbers) { + pre.classList.add('line-numbers'); + + // 如果代码块内部没有行号结构,则添加 + const hasLineElements = codeBlock.querySelectorAll('.line').length > 0 || + codeBlock.innerHTML.includes(''); + + if (!hasLineElements) { + // 特殊处理已经高亮的代码,防止破坏高亮效果 + const isPreHighlighted = codeBlock.innerHTML.includes('span style=') || + pre.classList.contains('shiki') || + pre.classList.contains('astro-code'); + + if (isPreHighlighted) { + // 已经高亮的代码,只添加行号类,不修改结构 + // 在这种情况下依赖CSS来显示行号,而不是修改DOM结构 + return; + } + + // 只处理未高亮的代码块 + // 将代码按行分割 + const lines = originalCode.split('\n'); + let newHtml = ''; + + // 优化行处理,确保正确处理行内容 + lines.forEach((line, index) => { + // 避免末尾空行 + if (index === lines.length - 1 && line.trim() === '') return; + + // 处理HTML实体,防止XSS并保留格式 + const escapedLine = line + .replace(/&/g, '&') + .replace(//g, '>'); + + // 添加行标记(不添加换行符) + newHtml += `${escapedLine}`; + }); + + // 更新代码块内容,仅在有内容时更新 + if (newHtml) { + codeBlock.innerHTML = newHtml; + } + } + } + }); } // 4. 设置阅读进度条 @@ -655,7 +746,7 @@ const tableOfContents = generateTableOfContents(headings); updateReadingProgress(); } - + // 5. 管理目录交互 function setupTableOfContents() { const tocContent = document.getElementById("toc-content"); @@ -773,24 +864,137 @@ const tableOfContents = generateTableOfContents(headings); updateActiveHeading(); } + // 6. 处理Mermaid图表渲染 + function setupMermaid() { + // 查找所有mermaid代码块 - 支持多种可能的类名和选择器 + const mermaidBlocks = document.querySelectorAll( + 'pre.language-mermaid, pre > code.language-mermaid, .mermaid' + ); + + if (mermaidBlocks.length === 0) return; + + console.log('找到Mermaid代码块:', mermaidBlocks.length); + + // 动态加载mermaid库 + const script = document.createElement('script'); + script.src = 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js'; + + script.onload = function() { + console.log('Mermaid库加载完成,开始渲染图表'); + + // 初始化mermaid配置 - 始终使用默认主题,通过CSS控制样式 + window.mermaid.initialize({ + startOnLoad: false, + theme: 'default', + securityLevel: 'loose' + }); + + // 将所有mermaid代码块转换为可渲染的格式 + mermaidBlocks.forEach((block, index) => { + // 获取mermaid代码 + let code = ''; + + // 检查元素类型并相应处理 + if (block.tagName === 'CODE' && block.classList.contains('language-mermaid')) { + // 处理 code.language-mermaid 元素 + code = block.textContent || ''; + const pre = block.closest('pre'); + if (pre) { + // 创建新的div元素替换整个pre + const div = document.createElement('div'); + div.className = 'mermaid'; + div.id = 'mermaid-diagram-' + index; + div.textContent = code; + pre.parentNode.replaceChild(div, pre); + } + } else if (block.tagName === 'PRE' && block.classList.contains('language-mermaid')) { + // 处理 pre.language-mermaid 元素 + code = block.textContent || ''; + const div = document.createElement('div'); + div.className = 'mermaid'; + div.id = 'mermaid-diagram-' + index; + div.textContent = code; + block.parentNode.replaceChild(div, block); + } else if (block.classList.contains('mermaid') && block.tagName !== 'DIV') { + // 如果是其他带mermaid类的元素但不是div,转换为div + code = block.textContent || ''; + const div = document.createElement('div'); + div.className = 'mermaid'; + div.id = 'mermaid-diagram-' + index; + div.textContent = code; + block.parentNode.replaceChild(div, block); + } + }); + + // 初始化渲染 + try { + console.log('开始渲染Mermaid图表'); + window.mermaid.run().catch(err => { + console.error('Mermaid渲染出错:', err); + }); + } catch (error) { + console.error('初始化Mermaid渲染失败:', error); + } + }; + + script.onerror = function() { + console.error('加载Mermaid库失败'); + // 显示错误信息 + mermaidBlocks.forEach(block => { + if (block.tagName === 'CODE') block = block.closest('pre'); + if (block) { + block.innerHTML = '
无法加载Mermaid图表库
'; + } + }); + }; + + document.head.appendChild(script); + + // 添加到清理列表,确保后续页面跳转时能删除脚本 + listeners.push({ + element: script, + eventType: 'remove', + handler: () => { + if (script.parentNode) { + script.parentNode.removeChild(script); + } + + // 清除全局mermaid对象 + if (window.mermaid) { + window.mermaid = undefined; + } + + // 移除页面上可能留下的mermaid相关元素 + try { + // 移除所有可能的mermaid样式和元素 + const mermaidElements = [ + '#mermaid-style', + '#mermaid-cloned-styles', + '.mermaid-svg-reference', + 'style[id^="mermaid-"]' + ]; + + document.querySelectorAll(mermaidElements.join(', ')).forEach(el => { + if (el && el.parentNode) { + el.parentNode.removeChild(el); + } + }); + } catch (e) { + console.error('清理Mermaid元素时出错:', e); + } + }, + options: null + }); + } + // 初始化所有功能 function init() { if (!document.querySelector("article")) return; - // 应用代码高亮 - applyCodeHighlighting(); - - // 加载Mermaid SVG图表 - loadMermaidSvg(); - - // 设置代码复制功能 - setupCodeCopy(); - - // 设置阅读进度条 + enhanceCodeBlocks(); // 添加新的代码块增强函数 setupProgressBar(); - - // 设置目录交互 setupTableOfContents(); + setupMermaid(); } // 注册清理函数 diff --git a/src/plugins/custom-code-blocks.js b/src/plugins/custom-code-blocks.js deleted file mode 100644 index 2fef660..0000000 --- a/src/plugins/custom-code-blocks.js +++ /dev/null @@ -1,1377 +0,0 @@ -import hljs from 'highlight.js'; -import path from 'path'; -import fs from 'node:fs/promises'; -import fsSync from 'node:fs'; -import crypto from 'crypto'; -import os from 'os'; -import * as childProcess from 'child_process'; -import util from 'util'; - -// 将exec转换为Promise版本 -const execPromise = util.promisify(childProcess.exec); - -// 终端相关语言列表 -const TERMINAL_LANGUAGES = ['bash', 'shell', 'sh', 'zsh', 'console', 'terminal', 'cmd', 'powershell', 'ps', 'batch']; - -// 常用语言别名映射 -const LANGUAGE_ALIASES = { - 'js': 'javascript', - 'ts': 'typescript', - 'py': 'python', - 'yml': 'yaml', - 'git': 'diff', - 'text': 'plaintext', - 'docker-compose': 'yaml', - 'npm': 'bash', - 'docker': 'dockerfile', - 'node': 'javascript', - 'nginx': 'ini', - 'mdx': 'markdown', - '.gitignore': 'plaintext', - 'astro': 'html', - 'jsx': 'javascript', - 'tsx': 'typescript', - 'vue': 'html', - 'cron': 'plaintext', - 'mindmap': 'plaintext', - 'batch': 'plaintext' -}; - -// 用于存储全局状态的对象 -const globalStore = { - mermaidDefinitions: new Map(), - pendingMermaidGraphs: [] -}; - -/** - * 检查Mermaid CLI是否可用 - */ -async function checkMermaidCliAvailable() { - try { - // 检测是否在Vercel环境中 - const isVercelEnv = process.env.VERCEL === '1'; - if (isVercelEnv) { - console.log(`[Mermaid检测] 在Vercel环境中运行, Node版本: ${process.version}`); - } - - // 首先检查npx是否可用 - try { - const { stdout: npxVersion } = await execPromise('npx --version'); - console.log(`[Mermaid检测] NPX可用,版本: ${npxVersion.trim()}`); - } catch (npxError) { - console.error(`[Mermaid检测] NPX不可用: ${npxError.message}`); - // 在Vercel环境中,我们假设NPX始终可用 - if (!isVercelEnv) { - return false; - } - } - - // 检查mermaid-cli是否已安装 - try { - const { stdout, stderr } = await execPromise('npx mmdc --version'); - console.log(`[Mermaid检测] Mermaid CLI已安装,版本: ${stdout.trim() || stderr.trim()}`); - // 版本输出成功即可认为已安装,无需后续测试 - return true; - } catch (mmdcError) { - // 版本检查失败,可能是命令不存在或参数不兼容 - console.log(`[Mermaid检测] Mermaid CLI版本检查失败: ${mmdcError.message}`); - // 继续创建测试文件进行测试 - } - - // 创建一个简单的测试文件 - const tempDir = isVercelEnv ? path.join(process.cwd(), 'dist', 'client', 'mermaid-svg') : os.tmpdir(); - - // 确保目录存在 - try { - if (!fsSync.existsSync(tempDir)) { - fsSync.mkdirSync(tempDir, { recursive: true }); - } - } catch (mkdirError) { - console.error(`[Mermaid检测] 创建临时目录失败: ${mkdirError.message}`); - // 在Vercel环境中,尝试使用项目根目录 - if (isVercelEnv) { - try { - console.log(`[Mermaid检测] 尝试使用项目根目录`); - } catch (e) { - // 忽略错误 - } - } - } - - // 生成唯一的测试文件名 - const testId = Date.now() + '-' + Math.random().toString(36).substring(2, 10); - const testFile = path.join(tempDir, `test-mermaid-${testId}.mmd`); - const testSvgFile = path.join(tempDir, `test-mermaid-${testId}.svg`); - - // 写入一个简单的图表定义 - try { - await fs.writeFile(testFile, 'graph TD;\nA-->B;', 'utf8'); - console.log(`[Mermaid检测] 测试文件已创建: ${testFile}`); - } catch (writeError) { - console.error(`[Mermaid检测] 创建测试文件失败: ${writeError.message}`); - if (isVercelEnv) { - console.log(`[Mermaid检测] 在Vercel环境中假设CLI已安装`); - return true; - } - return false; - } - - try { - // 尝试执行命令 - 在Vercel环境中使用基本参数 - const testCmd = isVercelEnv - ? `npx mmdc -i "${testFile}" -o "${testSvgFile}"` - : `npx mmdc -i "${testFile}" -o "${testSvgFile}" -t default`; - - console.log(`[Mermaid检测] 执行测试命令: ${testCmd}`); - const { stdout, stderr } = await execPromise(testCmd); - - if (stdout) console.log(`[Mermaid检测] 命令输出: ${stdout}`); - if (stderr) console.log(`[Mermaid检测] 命令错误: ${stderr}`); - - // 清理测试文件 - try { - await fs.unlink(testFile); - if (fsSync.existsSync(testSvgFile)) { - await fs.unlink(testSvgFile); - } - console.log(`[Mermaid检测] 测试文件已清理`); - } catch (cleanupError) { - console.log(`[Mermaid检测] 清理测试文件失败: ${cleanupError.message}`); - // 忽略清理错误 - } - - return true; - } catch (testError) { - console.error(`[Mermaid检测] Mermaid CLI测试失败: ${testError.message}`); - - // 输出标准输出和错误输出,帮助调试 - if (testError.stdout) console.log(`[Mermaid检测] 命令输出: ${testError.stdout}`); - if (testError.stderr) console.log(`[Mermaid检测] 命令错误: ${testError.stderr}`); - - // 尝试清理测试文件 - try { - await fs.unlink(testFile); - console.log(`[Mermaid检测] 测试源文件已清理`); - } catch (e) { - // 忽略清理错误 - } - - // 尝试安装Mermaid CLI - 在Vercel环境中使用项目安装而非全局安装 - try { - if (isVercelEnv) { - console.log(`[Mermaid检测] 在Vercel环境中安装mermaid-cli作为项目依赖`); - await execPromise('npm install @mermaid-js/mermaid-cli --no-save'); - } else { - console.log(`[Mermaid检测] 尝试全局安装mermaid-cli`); - await execPromise('npm install -g @mermaid-js/mermaid-cli'); - } - console.log(`[Mermaid检测] Mermaid CLI安装成功`); - return true; - } catch (installError) { - console.error(`[Mermaid检测] 安装Mermaid CLI失败: ${installError.message}`); - - // 在Vercel环境中,我们假设可以使用CLI - if (isVercelEnv) { - console.log(`[Mermaid检测] 在Vercel环境中继续执行,假设已安装`); - return true; - } - - return false; - } - } - } catch (generalError) { - console.error(`[Mermaid检测] 检查过程发生错误: ${generalError.message}`); - - // 在Vercel环境中,我们假设可以继续 - if (process.env.VERCEL === '1') { - console.log(`[Mermaid检测] 在Vercel环境中继续执行`); - return true; - } - - return false; - } -} - -// 注册语言别名 -function registerAliases() { - Object.entries(LANGUAGE_ALIASES).forEach(([alias, language]) => { - try { - if (alias !== language && hljs.getLanguage(language)) { - hljs.registerAliases(alias, { languageName: language }); - } - } catch (e) { - // 移除详细日志 - } - }); -} - -// 检测是否为终端命令类型 -function isTerminalLike(lang) { - return TERMINAL_LANGUAGES.includes(lang); -} - -/** - * 检查语言是否可用 - */ -function isLanguageAvailable(lang) { - if (!lang) return false; - - try { - const normalizedLang = normalizeLanguage(lang); - return !!hljs.getLanguage(normalizedLang); - } catch (e) { - return false; - } -} - -/** - * 代码高亮处理函数 - */ -function highlightCode(code, language) { - if (!code) return escape(code || ''); - - try { - const normalizedLang = normalizeLanguage(language); - - const highlightResult = hljs.highlight(code, { - language: normalizedLang, - ignoreIllegals: true, - }); - - return highlightResult.value; - } catch (e) { - // 移除详细警告日志 - return escape(code); - } -} - -/** - * 规范化语言名称 - */ -function normalizeLanguage(lang) { - return LANGUAGE_ALIASES[lang?.toLowerCase()] || lang || 'plaintext'; -} - -/** - * 清理文件名,确保不会有连续的短横线 - */ -function sanitizeFileName(name) { - if (!name) return 'default'; - - // 先替换所有非字母数字字符为短横线 - let result = name.replace(/[^a-zA-Z0-9]/g, '-'); - - // 然后去除连续的短横线 - result = result.replace(/-+/g, '-'); - - // 去除开头和结尾的短横线 - result = result.replace(/^-+|-+$/g, ''); - - return result || 'default'; -} - -/** - * 自定义代码块集成 - */ -export default function customCodeBlocksIntegration() { - // 使用外部定义的globalStore,不再在此重新定义一个新的 - - /** - * 扫描内容目录检查Mermaid代码块 - * @param {Object} logger - 日志记录器 - */ - async function scanContentForMermaid(logger) { - try { - const contentDir = path.join(process.cwd(), 'src/content'); - - // 检查目录是否存在 - try { - await fs.access(contentDir); - } catch (error) { - return []; - } - - // 递归获取所有 .md 和 .mdx 文件 - async function findMarkdownFiles(dir) { - const entries = await fs.readdir(dir, { withFileTypes: true }); - const files = await Promise.all(entries.map(async (entry) => { - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory()) { - return findMarkdownFiles(fullPath); - } else if (/\.(md|mdx)$/.test(entry.name)) { - return [fullPath]; - } else { - return []; - } - })); - return files.flat(); - } - - const markdownFiles = await findMarkdownFiles(contentDir); - logger.info(`找到 ${markdownFiles.length} 个 Markdown/MDX 文件`); - - // 搜索Mermaid代码块 - const mermaidFiles = []; - - for (const file of markdownFiles) { - try { - const content = await fs.readFile(file, 'utf-8'); - // 简单检查是否包含 ```mermaid 标记 - if (content.includes('```mermaid') || content.includes('~~~mermaid')) { - const matches = content.match(/```mermaid\s+([\s\S]*?)```|~~~mermaid\s+([\s\S]*?)~~~/g) || []; - const relativePath = path.relative(contentDir, file); - mermaidFiles.push({ - path: relativePath, - count: matches.length - }); - - // 移除详细日志,只保留关键信息 - if (matches.length > 0) { - logger.info(`文件 "${relativePath}" 包含 ${matches.length} 个 Mermaid 代码块`); - } - - // 尝试提取并处理这些Mermaid代码块 - matches.forEach((match, index) => { - const code = match.replace(/```mermaid\s+|~~~mermaid\s+|```$|~~~$/g, '').trim(); - if (code) { - // 移除详细日志 - - // 标准化并计算哈希 - const normalizedCode = code.trim(); - const hash = crypto.createHash('md5').update(normalizedCode).digest('hex'); - - // 使用文件名和索引生成一个简洁安全的文件名 - const baseName = path.basename(relativePath, path.extname(relativePath)); - // 使用安全处理函数 - const safeBaseName = sanitizeFileName(baseName); - const safeFilename = `mermaid-diagram-${safeBaseName}-${index}`; - const svgFileName = `${safeFilename}-${hash}.svg`; - - globalStore.pendingMermaidGraphs.push({ - definition: normalizedCode, - svgFileName: svgFileName, - fromScan: true - }); - } - }); - } - } catch (error) { - console.error(`读取或处理文件时出错`); - } - } - - return mermaidFiles; - } catch (error) { - logger.error(`扫描内容目录时出错`); - return []; - } - } - - /** - * 处理Mermaid图形,生成SVG - * @param {string} graphDefinition - Mermaid图形定义 - * @param {string} filename - 可选,文件名 - * @param {boolean} [inBuildMode=false] - 是否在构建模式下运行 - * @returns {string} SVG文件的公共路径 - */ - async function processMermaidGraph(graphDefinition, filename, inBuildMode = false) { - if (typeof window !== 'undefined') { - console.error('Mermaid处理必须在服务器端执行'); - return ''; - } - - try { - // 标准化图表定义(处理空白) - const normalizedDefinition = graphDefinition.trim(); - - // 使用MD5哈希创建唯一ID - const hash = crypto.createHash('md5').update(normalizedDefinition).digest('hex'); - const safeFilename = sanitizeFileName(filename || 'mermaid-diagram-default'); - const svgFileName = `${safeFilename}-${hash}.svg`; - - // SVG的公共路径 - const svgPublicPath = `/mermaid-svg/${svgFileName}`; - - // 添加到待处理列表,但只在构建模式下实际生成 - if (inBuildMode) { - // 保存图表定义到内存 - if (!globalStore.mermaidDefinitions.has(svgPublicPath)) { - globalStore.mermaidDefinitions.set(svgPublicPath, { - definition: normalizedDefinition, - svgFileName: svgFileName - }); - } - - // 确保不会重复添加 - const exists = globalStore.pendingMermaidGraphs.some(graph => - graph.svgFileName === svgFileName - ); - - if (!exists) { - globalStore.pendingMermaidGraphs.push({ - definition: normalizedDefinition, - svgFileName: svgFileName - }); - } - } - - return svgPublicPath; - } catch (error) { - console.error(`Mermaid处理错误`); - return ''; - } - } - - /** - * 在构建完成后,生成所有Mermaid SVG文件 - * @param {string} outDir - 输出目录路径 - */ - async function generatePendingMermaidGraphs(outDir) { - try { - const mermaidGraphs = globalStore.pendingMermaidGraphs; - - if (!mermaidGraphs || mermaidGraphs.length === 0) { - return; - } - - // 检测Vercel环境 - const isVercelEnv = process.env.VERCEL === '1'; - if (isVercelEnv) { - console.log(`[Mermaid] 检测到Vercel环境,将使用适配的渲染逻辑`); - } - - // 处理输出目录路径 - 如果是URL对象,获取pathname - if (typeof outDir === 'object' && outDir instanceof URL) { - outDir = outDir.pathname; - // Windows路径修复 (如果以/C:开头) - if (process.platform === 'win32' && outDir.startsWith('/') && /^\/[A-Z]:/i.test(outDir)) { - outDir = outDir.substring(1); - } - } - - // 创建SVG输出目录 - const svgOutDir = path.join(outDir, 'mermaid-svg'); - - try { - // 使用同步方法确保目录创建成功 - if (!fsSync.existsSync(svgOutDir)) { - fsSync.mkdirSync(svgOutDir, { recursive: true }); - } - } catch (dirError) { - console.error(`创建SVG目录失败: ${dirError.message}`); - - // 如果是Vercel环境,创建临时处理目录 - if (isVercelEnv) { - console.log(`[Mermaid] 尝试在Vercel环境创建备用目录`); - try { - const tmpOutDir = path.join(process.cwd(), 'dist', 'client', 'mermaid-svg'); - if (!fsSync.existsSync(tmpOutDir)) { - fsSync.mkdirSync(tmpOutDir, { recursive: true }); - } - console.log(`[Mermaid] 成功创建备用目录: ${tmpOutDir}`); - } catch (tmpDirError) { - console.error(`创建备用目录失败: ${tmpDirError.message}`); - } - } - - // 继续尝试生成,可能目录已存在 - } - - // 每个图表的处理 - let successCount = 0; - let failCount = 0; - let vercelFallbackCount = 0; - - // Vercel环境下限制处理的图表数量以避免超时 - const graphsToProcess = isVercelEnv ? - mermaidGraphs.slice(0, Math.min(mermaidGraphs.length, 10)) : // 限制为最多10个图表 - mermaidGraphs; - - if (isVercelEnv && graphsToProcess.length < mermaidGraphs.length) { - console.log(`[Mermaid] Vercel环境下图表过多,限制处理前 ${graphsToProcess.length} 个,共 ${mermaidGraphs.length} 个`); - } - - for (const [index, graph] of graphsToProcess.entries()) { - try { - // 获取图表定义 - const graphDefinition = graph.graphDefinition || graph.definition; - - if (!graphDefinition) { - failCount++; - continue; - } - - // 获取图表文件名 - const svgFileName = graph.svgFileName; - if (svgFileName) { - // 构建SVG文件路径 - const svgPath = path.join(svgOutDir, svgFileName); - - console.log(`[Mermaid] 处理图表 ${index + 1}/${graphsToProcess.length}: ${svgFileName}`); - const startTime = Date.now(); - - const success = await generateThemeAwareSvg(graphDefinition, svgPath, index, graphsToProcess.length); - const timeUsed = Date.now() - startTime; - - if (success) { - // 检查文件是否真的存在 - if (fsSync.existsSync(svgPath)) { - const fileSize = fsSync.statSync(svgPath).size; - console.log(`[Mermaid] 成功生成SVG (${fileSize} 字节), 耗时: ${timeUsed}ms`); - successCount++; - - // 如果生成的是占位SVG - if (isVercelEnv && fileSize < 500) { - vercelFallbackCount++; - } - } else { - console.log(`[Mermaid] 生成SVG报告成功但文件不存在: ${svgPath}`); - failCount++; - } - } else { - failCount++; - console.log(`[Mermaid] 生成SVG失败, 耗时: ${timeUsed}ms`); - - // 在Vercel环境中尝试使用占位符 - if (isVercelEnv) { - try { - const fallbackSvg = ` - - - - Mermaid图表占位符 (生成失败后使用) - -`; - await fs.writeFile(svgPath, fallbackSvg, 'utf8'); - console.log(`[Mermaid] 生成失败后补充占位SVG`); - vercelFallbackCount++; - // 将失败转为成功,因为提供了替代方案 - successCount++; - failCount--; - } catch (fallbackError) { - console.error(`[Mermaid] 生成占位SVG失败: ${fallbackError.message}`); - } - } - } - } else { - failCount++; - } - } catch (graphError) { - console.error(`处理图表失败: ${graphError.message}`); - failCount++; - } - } - - // 输出汇总信息 - console.log(`[Mermaid] 图表处理完成: 总共 ${graphsToProcess.length} 个, 成功 ${successCount} 个, 失败 ${failCount} 个`); - if (isVercelEnv && vercelFallbackCount > 0) { - console.log(`[Mermaid] 在Vercel环境中使用了 ${vercelFallbackCount} 个占位SVG`); - } - - if (isVercelEnv && graphsToProcess.length < mermaidGraphs.length) { - console.log(`[Mermaid] 警告: 由于Vercel环境限制,${mermaidGraphs.length - graphsToProcess.length} 个图表未处理`); - } - } catch (error) { - console.error(`生成Mermaid SVG文件出错: ${error.message}`); - if (error.stack) { - console.error(`错误堆栈: ${error.stack}`); - } - } - } - - // 生成支持主题切换的SVG - async function generateThemeAwareSvg(graphDefinition, svgPath, index, total) { - try { - // 检测Vercel环境并添加警告日志 - const isVercelEnv = process.env.VERCEL === '1'; - if (isVercelEnv) { - console.log(`[Mermaid警告] 在Vercel环境中运行,可能会遇到限制`); - } - - // 标准化图表定义 - const normalizedDefinition = graphDefinition.trim(); - - // 确保目标目录存在 - const svgDir = path.dirname(svgPath); - if (!fsSync.existsSync(svgDir)) { - fsSync.mkdirSync(svgDir, { recursive: true }); - } - - // 直接在目标目录中创建文件,避免使用临时目录 - const uniqueId = Date.now() + '-' + Math.random().toString(36).substring(2, 10); - const mmdFilePath = path.join(svgDir, `source-${uniqueId}.mmd`); - const rawSvgPath = path.join(svgDir, `raw-${uniqueId}.svg`); - const puppeteerConfigPath = path.join(svgDir, `puppeteer-config-${uniqueId}.json`); - - // 输出详细的环境信息帮助调试 - console.log(`[Mermaid调试] 当前工作目录: ${process.cwd()}`); - console.log(`[Mermaid调试] 图表文件路径: ${mmdFilePath}`); - console.log(`[Mermaid调试] 原始SVG路径: ${rawSvgPath}`); - console.log(`[Mermaid调试] 最终SVG路径: ${svgPath}`); - console.log(`[Mermaid调试] 操作系统: ${process.platform}`); - console.log(`[Mermaid调试] Node版本: ${process.version}`); - - // 写入Mermaid定义到文件 - await fs.writeFile(mmdFilePath, normalizedDefinition, 'utf8'); - console.log(`[Mermaid调试] 已写入图表定义文件`); - - // 写入Puppeteer配置文件 - const puppeteerConfig = { - args: ['--no-sandbox', '--disable-setuid-sandbox'] - }; - await fs.writeFile(puppeteerConfigPath, JSON.stringify(puppeteerConfig), 'utf8'); - console.log(`[Mermaid调试] 已写入Puppeteer配置文件`); - - // 构建Mermaid命令 - 使用puppeteerConfigFile而不是puppeteerConfig - let mermaidCmd = `npx mmdc -i "${mmdFilePath}" -o "${rawSvgPath}" -t default --puppeteerConfigFile "${puppeteerConfigPath}"`; - console.log(`[Mermaid调试] 执行命令: ${mermaidCmd}`); - - try { - const { stdout, stderr } = await execPromise(mermaidCmd); - if (stdout) console.log(`[Mermaid调试] 命令输出: ${stdout}`); - if (stderr) console.log(`[Mermaid调试] 命令错误: ${stderr}`); - } catch (execError) { - console.error(`执行Mermaid命令失败: ${execError.message}`); - if (execError.stdout) console.log(`命令标准输出: ${execError.stdout}`); - if (execError.stderr) console.log(`命令错误输出: ${execError.stderr}`); - - // 尝试使用简化版命令,不使用puppeteer配置 - try { - console.log(`[Mermaid调试] 尝试使用基本命令不带配置`); - const basicCmd = `npx mmdc -i "${mmdFilePath}" -o "${rawSvgPath}" -t default`; - const { stdout, stderr } = await execPromise(basicCmd); - if (stdout) console.log(`[Mermaid调试] 基本命令输出: ${stdout}`); - if (stderr) console.log(`[Mermaid调试] 基本命令错误: ${stderr}`); - } catch (basicExecError) { - console.error(`使用基本命令失败: ${basicExecError.message}`); - - // 如果是Vercel环境,我们提供一个默认的替代SVG - if (isVercelEnv) { - console.log(`[Mermaid调试] 在Vercel环境中提供默认占位SVG`); - const fallbackSvg = ` - - - - Mermaid图表占位符 - -`; - await fs.writeFile(svgPath, fallbackSvg, 'utf8'); - console.log(`[Mermaid调试] 已写入占位SVG`); - return true; - } - - throw new Error(`Mermaid渲染失败: ${basicExecError.message}`); - } - } - - // 验证SVG文件是否生成 - try { - await fs.access(rawSvgPath); - console.log(`[Mermaid调试] 成功生成原始SVG文件`); - } catch (e) { - console.error(`SVG文件生成失败或无法访问: ${e.message}`); - - // 如果是Vercel环境,我们提供一个默认的替代SVG - if (isVercelEnv) { - console.log(`[Mermaid调试] 在Vercel环境中提供默认占位SVG`); - const fallbackSvg = ` - - - - Mermaid图表占位符 - -`; - await fs.writeFile(svgPath, fallbackSvg, 'utf8'); - console.log(`[Mermaid调试] 已写入占位SVG`); - return true; - } - - throw new Error(`无法访问生成的SVG文件: ${e.message}`); - } - - // 读取SVG文件内容 - const lightSvgContent = await fs.readFile(rawSvgPath, 'utf8'); - console.log(`[Mermaid调试] 成功读取SVG内容,长度: ${lightSvgContent.length}`); - - // 添加主题类 - const themeAwareSvg = createThemeAwareSvg(lightSvgContent); - - // 写入最终SVG文件 - await fs.writeFile(svgPath, themeAwareSvg); - console.log(`[Mermaid调试] 成功写入最终SVG文件`); - - // 清理中间文件 - try { - await fs.unlink(mmdFilePath); - await fs.unlink(rawSvgPath); - await fs.unlink(puppeteerConfigPath); - console.log(`[Mermaid调试] 成功清理中间文件`); - } catch (unlinkError) { - console.log(`[Mermaid调试] 清理中间文件时出错: ${unlinkError.message}`); - // 忽略清理错误,不影响主流程 - } - - return true; - } catch (error) { - console.error(`SVG生成失败: ${error.message}`); - // 提供堆栈跟踪以便更好地调试 - if (error.stack) { - console.error(`错误堆栈: ${error.stack}`); - } - - // 如果是Vercel环境,写入一个占位符SVG而不是失败 - if (process.env.VERCEL === '1') { - try { - console.log(`[Mermaid调试] 在Vercel环境中提供默认占位SVG (错误恢复)`); - const fallbackSvg = ` - - - - Mermaid图表占位符 (渲染失败) - -`; - await fs.writeFile(svgPath, fallbackSvg, 'utf8'); - console.log(`[Mermaid调试] 已写入错误恢复占位SVG`); - return true; - } catch (fallbackError) { - console.error(`无法写入占位SVG: ${fallbackError.message}`); - } - } - - return false; - } - } - - // 辅助函数:创建支持主题切换的SVG - function createThemeAwareSvg(lightSvg) { - // 从SVG中提取重要部分 - const lightSvgMatch = lightSvg.match(/]*>([\s\S]*?)<\/svg>/i); - - if (!lightSvgMatch) { - throw new Error('无法解析SVG内容'); - } - - // 从亮色SVG中提取宽度、高度和viewBox - const widthMatch = lightSvg.match(/width="([^"]*)"/); - const heightMatch = lightSvg.match(/height="([^"]*)"/); - const viewBoxMatch = lightSvg.match(/viewBox="([^"]*)"/); - - const width = widthMatch ? widthMatch[1] : '100%'; - // 始终设置高度为auto,让CSS控制高度 - const height = 'auto'; - - // 确保viewBox存在且格式正确 - let viewBox = viewBoxMatch ? viewBoxMatch[1] : '0 0 100 100'; - if (viewBox.split(' ').length !== 4) { - // 如果viewBox格式不正确,尝试从宽高创建 - const widthValue = parseFloat(width); - const originalHeight = heightMatch ? heightMatch[1] : '100%'; - const heightValue = parseFloat(originalHeight); - - if (!isNaN(widthValue) && !isNaN(heightValue) && widthValue > 0 && heightValue > 0) { - viewBox = `0 0 ${widthValue} ${heightValue}`; - } else { - // 使用默认viewBox - viewBox = '0 0 100 100'; - } - } - - // 从亮色SVG中提取内容,并移除样式标签 - let lightContent = lightSvgMatch[1]; - - // 移除内联样式 - lightContent = lightContent.replace(/