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 = `
-`;
- 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 = `
-`;
- 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 = `
-`;
- 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 = `
-`;
- 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(/