将代码高亮改为astro集成配置,mermaid改为客户端生成,优化导航栏样式,将地图数据动态导入
This commit is contained in:
parent
af0dee8d1d
commit
d3e0eddff7
@ -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(),
|
||||
|
11
package-lock.json
generated
11
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -29,7 +29,7 @@ const breadcrumbs: Breadcrumb[] = pathSegments
|
||||
});
|
||||
---
|
||||
|
||||
<div class="flex items-center justify-between w-full flex-wrap sm:flex-nowrap" transition:persist transition:name="breadcrumb">
|
||||
<div class="flex items-center justify-between w-full flex-wrap sm:flex-nowrap">
|
||||
<div class="flex items-center text-sm overflow-hidden">
|
||||
<!-- 文章列表链接 - 根据当前页面类型决定链接 -->
|
||||
<a href={'/articles/'} class="text-secondary-600 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400 flex items-center flex-shrink-0">
|
||||
|
@ -57,7 +57,7 @@ let secondaryHighlightStyle = activeSubItem ? "opacity: 1;" : "opacity: 0;";
|
||||
const activeClass = "font-medium";
|
||||
const itemClass = "px-4 py-2 text-sm font-medium";
|
||||
const primaryHighlightClass = "bg-primary-100 dark:bg-primary-800/30 rounded-xl shadow-md backdrop-blur-sm";
|
||||
const secondaryHighlightClass = "bg-primary-300/80 dark:bg-primary-700/60 rounded-md shadow-md backdrop-blur-sm";
|
||||
const secondaryHighlightClass = "bg-primary-300/80 dark:bg-primary-700/60 rounded-xl shadow-md backdrop-blur-sm";
|
||||
const transitionDuration = 300;
|
||||
const navSelectorClassName = "mr-4";
|
||||
---
|
||||
@ -98,7 +98,7 @@ const navSelectorClassName = "mr-4";
|
||||
class={`absolute z-0 hover:shadow-lg rounded-xl ${primaryHighlightClass}`}
|
||||
style={primaryHighlightStyle}></div>
|
||||
<div id="nav-secondary-highlight"
|
||||
class={`absolute z-10 hover:shadow-lg rounded-md ${secondaryHighlightClass}`}
|
||||
class={`absolute z-10 hover:shadow-lg rounded-xl ${secondaryHighlightClass}`}
|
||||
style={secondaryHighlightStyle}></div>
|
||||
|
||||
<!-- 导航菜单项 -->
|
||||
@ -130,7 +130,7 @@ const navSelectorClassName = "mr-4";
|
||||
|
||||
<!-- 二级菜单容器 -->
|
||||
<div class={`nav-group-items relative min-w-full ${activeGroupId === item.id ? 'menu-visible' : 'hidden'}`}>
|
||||
<div class="flex flex-row flex-wrap gap-2 justify-center items-center">
|
||||
<div class="flex flex-row flex-wrap justify-center items-center">
|
||||
{item.items.map((subItem) => (
|
||||
<a
|
||||
href={subItem.href}
|
||||
|
@ -165,8 +165,6 @@ const {
|
||||
|
||||
// 立即设置文档主题,在DOM渲染前应用,避免闪烁
|
||||
document.documentElement.dataset.theme = theme;
|
||||
// 确保同步classList,提高兼容性
|
||||
document.documentElement.classList.toggle('dark', theme === 'dark');
|
||||
|
||||
// 监听系统主题变化(只有当主题设为跟随系统时才响应)
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
@ -176,7 +174,6 @@ const {
|
||||
if (!localStorage.getItem("theme")) {
|
||||
const newTheme = e.matches ? "dark" : "light";
|
||||
document.documentElement.dataset.theme = newTheme;
|
||||
document.documentElement.classList.toggle('dark', e.matches);
|
||||
}
|
||||
};
|
||||
|
||||
@ -191,11 +188,9 @@ const {
|
||||
// 重新初始化主题
|
||||
if (storedTheme) {
|
||||
document.documentElement.dataset.theme = storedTheme;
|
||||
document.documentElement.classList.toggle('dark', storedTheme === 'dark');
|
||||
} else {
|
||||
const systemTheme = getSystemTheme();
|
||||
document.documentElement.dataset.theme = systemTheme;
|
||||
document.documentElement.classList.toggle('dark', systemTheme === 'dark');
|
||||
}
|
||||
}
|
||||
|
||||
@ -206,7 +201,6 @@ const {
|
||||
} catch (error) {
|
||||
// 出错时应用默认浅色主题,确保页面正常显示
|
||||
document.documentElement.dataset.theme = "light";
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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<WorldHeatmapProps> = ({ visitedPlaces }) => {
|
||||
const [mapLoading, setMapLoading] = useState(true);
|
||||
const [mapError, setMapError] = useState<string | null>(null);
|
||||
|
||||
// 修改场景引用类型以适应新的导入方式
|
||||
const sceneRef = useRef<{
|
||||
scene: THREE.Scene;
|
||||
camera: THREE.PerspectiveCamera;
|
||||
renderer: THREE.WebGLRenderer;
|
||||
labelRenderer: CSS2DRenderer;
|
||||
controls: OrbitControls;
|
||||
earth: THREE.Mesh;
|
||||
countries: Map<string, THREE.Object3D>;
|
||||
raycaster: THREE.Raycaster;
|
||||
mouse: THREE.Vector2;
|
||||
scene: Scene;
|
||||
camera: PerspectiveCamera;
|
||||
renderer: WebGLRenderer;
|
||||
labelRenderer: any; // 后面会动态设置具体类型
|
||||
controls: any; // 后面会动态设置具体类型
|
||||
earth: Mesh;
|
||||
countries: Map<string, Group>;
|
||||
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<THREE.Line, string>;
|
||||
allLineObjects: THREE.Line[];
|
||||
lineToCountryMap: Map<Line, string>;
|
||||
allLineObjects: Line[];
|
||||
} | null>(null);
|
||||
|
||||
// 监听主题变化
|
||||
@ -104,22 +127,29 @@ const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ 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,6 +226,19 @@ const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
|
||||
containerRef.current.innerHTML = "";
|
||||
}
|
||||
|
||||
// 创建一个引用,用于清理函数
|
||||
let mounted = true;
|
||||
let cleanupFunctions: Array<() => void> = [];
|
||||
|
||||
// 动态加载Three.js控制器和渲染器
|
||||
const initThreeScene = async () => {
|
||||
try {
|
||||
// 加载OrbitControls和CSS2DRenderer
|
||||
const [{ OrbitControls }, { CSS2DRenderer }] = await loadControlsAndRenderers();
|
||||
|
||||
// 如果组件已卸载,退出初始化
|
||||
if (!mounted || !containerRef.current) return;
|
||||
|
||||
// 检查当前是否为暗色模式
|
||||
const isDarkMode =
|
||||
document.documentElement.classList.contains("dark") ||
|
||||
@ -217,16 +260,16 @@ const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
|
||||
const colors = getColors();
|
||||
|
||||
// 创建场景
|
||||
const scene = new THREE.Scene();
|
||||
const scene = new Scene();
|
||||
scene.background = null;
|
||||
|
||||
// 创建材质的辅助函数
|
||||
const createMaterial = (
|
||||
color: string,
|
||||
side: THREE.Side = THREE.FrontSide,
|
||||
side: Side = FrontSide,
|
||||
opacity: number = 1.0,
|
||||
) => {
|
||||
return new THREE.MeshBasicMaterial({
|
||||
return new MeshBasicMaterial({
|
||||
color: color,
|
||||
side: side,
|
||||
transparent: true,
|
||||
@ -235,24 +278,24 @@ const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
|
||||
};
|
||||
|
||||
// 创建地球几何体
|
||||
const earthGeometry = new THREE.SphereGeometry(2.0, 64, 64);
|
||||
const earthGeometry = new SphereGeometry(2.0, 64, 64);
|
||||
const earthMaterial = createMaterial(
|
||||
colors.earthBase,
|
||||
THREE.FrontSide,
|
||||
FrontSide,
|
||||
isDarkMode ? 0.9 : 0.9, // 调整明亮模式下的不透明度
|
||||
);
|
||||
const earth = new THREE.Mesh(earthGeometry, earthMaterial);
|
||||
const earth = new Mesh(earthGeometry, earthMaterial);
|
||||
earth.renderOrder = 1;
|
||||
scene.add(earth);
|
||||
|
||||
// 添加光源
|
||||
const ambientLight = new THREE.AmbientLight(
|
||||
const ambientLight = new AmbientLight(
|
||||
0xffffff,
|
||||
isDarkMode ? 0.7 : 0.85, // 微调明亮模式下的光照强度
|
||||
);
|
||||
scene.add(ambientLight);
|
||||
|
||||
const directionalLight = new THREE.DirectionalLight(
|
||||
const directionalLight = new DirectionalLight(
|
||||
isDarkMode ? 0xeeeeff : 0xffffff, // 恢复明亮模式下的纯白光源
|
||||
isDarkMode ? 0.6 : 0.65, // 微调明亮模式下的定向光强度
|
||||
);
|
||||
@ -260,7 +303,7 @@ const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
|
||||
scene.add(directionalLight);
|
||||
|
||||
// 创建相机
|
||||
const camera = new THREE.PerspectiveCamera(
|
||||
const camera = new PerspectiveCamera(
|
||||
45,
|
||||
containerRef.current.clientWidth / containerRef.current.clientHeight,
|
||||
0.1,
|
||||
@ -269,7 +312,7 @@ const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
|
||||
camera.position.z = 8;
|
||||
|
||||
// 创建渲染器
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
const renderer = new WebGLRenderer({
|
||||
antialias: true,
|
||||
alpha: true,
|
||||
logarithmicDepthBuffer: true,
|
||||
@ -317,15 +360,15 @@ const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
|
||||
});
|
||||
|
||||
// 保存所有线条对象的引用,用于快速检测
|
||||
const allLineObjects: THREE.Line[] = [];
|
||||
const lineToCountryMap = new Map<THREE.Line, string>();
|
||||
const allLineObjects: Line[] = [];
|
||||
const lineToCountryMap = new Map<Line, string>();
|
||||
|
||||
// 创建国家边界组
|
||||
const countryGroup = new THREE.Group();
|
||||
const countryGroup = new Group();
|
||||
earth.add(countryGroup);
|
||||
|
||||
// 创建国家边界
|
||||
const countries = new Map<string, THREE.Object3D>();
|
||||
const countries = new Map<string, Group>();
|
||||
|
||||
// 从WASM获取边界线数据
|
||||
const boundaryLines = geoProcessor.get_boundary_lines();
|
||||
@ -337,15 +380,15 @@ const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
|
||||
const { points, region_name, is_visited } = boundaryLine;
|
||||
|
||||
// 创建区域组
|
||||
const regionObject = new THREE.Group();
|
||||
const regionObject = new 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));
|
||||
// 转换点数组为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 THREE.BufferGeometry().setFromPoints(threePoints);
|
||||
const lineGeometry = new BufferGeometry().setFromPoints(threePoints);
|
||||
|
||||
// 确定线条颜色
|
||||
const isChina = region_name === "中国" || region_name.startsWith("中国-");
|
||||
@ -362,14 +405,14 @@ const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
|
||||
borderColor = colors.border;
|
||||
}
|
||||
|
||||
const lineMaterial = new THREE.LineBasicMaterial({
|
||||
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 THREE.Line(lineGeometry, lineMaterial);
|
||||
const line = new Line(lineGeometry, lineMaterial);
|
||||
line.userData = {
|
||||
name: region_name,
|
||||
isVisited: is_visited,
|
||||
@ -402,10 +445,10 @@ const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
|
||||
let fixedPosition;
|
||||
if (isSmallScreen) {
|
||||
// 小屏幕显示距离更远,以便看到更多地球
|
||||
fixedPosition = new THREE.Vector3(-2.1, 3.41, -8.0);
|
||||
fixedPosition = new Vector3(-2.1, 3.41, -8.0);
|
||||
} else {
|
||||
// 大屏幕使用原来的位置
|
||||
fixedPosition = new THREE.Vector3(-2.1, 3.41, -6.5);
|
||||
fixedPosition = new Vector3(-2.1, 3.41, -6.5);
|
||||
}
|
||||
|
||||
// 应用位置
|
||||
@ -425,8 +468,8 @@ const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
|
||||
positionCameraToFaceChina();
|
||||
|
||||
// 创建射线投射器用于鼠标交互
|
||||
const raycaster = new THREE.Raycaster();
|
||||
const mouse = new THREE.Vector2();
|
||||
const raycaster = new Raycaster();
|
||||
const mouse = new Vector2();
|
||||
|
||||
// 添加节流函数,限制鼠标移动事件的触发频率
|
||||
const throttle = (func: Function, limit: number) => {
|
||||
@ -474,7 +517,7 @@ const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
|
||||
|
||||
// 重置所有线条颜色
|
||||
allLineObjects.forEach((line) => {
|
||||
if (line.material instanceof THREE.LineBasicMaterial) {
|
||||
if (line.material instanceof LineBasicMaterial) {
|
||||
line.material.color.set(line.userData.originalColor);
|
||||
}
|
||||
});
|
||||
@ -485,7 +528,7 @@ const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
|
||||
allLineObjects.forEach((line) => {
|
||||
if (
|
||||
lineToCountryMap.get(line) === result.countryName &&
|
||||
line.material instanceof THREE.LineBasicMaterial
|
||||
line.material instanceof LineBasicMaterial
|
||||
) {
|
||||
line.material.color.set(line.userData.highlightColor);
|
||||
}
|
||||
@ -515,7 +558,7 @@ const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
|
||||
const clearSelection = () => {
|
||||
// 恢复所有线条的原始颜色
|
||||
allLineObjects.forEach((line) => {
|
||||
if (line.material instanceof THREE.LineBasicMaterial) {
|
||||
if (line.material instanceof LineBasicMaterial) {
|
||||
line.material.color.set(line.userData.originalColor);
|
||||
}
|
||||
});
|
||||
@ -547,7 +590,7 @@ const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
|
||||
if (result && result.countryName) {
|
||||
// 重置所有线条颜色
|
||||
allLineObjects.forEach((line) => {
|
||||
if (line.material instanceof THREE.LineBasicMaterial) {
|
||||
if (line.material instanceof LineBasicMaterial) {
|
||||
line.material.color.set(line.userData.originalColor);
|
||||
}
|
||||
});
|
||||
@ -556,7 +599,7 @@ const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
|
||||
allLineObjects.forEach((line) => {
|
||||
if (
|
||||
lineToCountryMap.get(line) === result.countryName &&
|
||||
line.material instanceof THREE.LineBasicMaterial
|
||||
line.material instanceof LineBasicMaterial
|
||||
) {
|
||||
line.material.color.set(line.userData.highlightColor);
|
||||
}
|
||||
@ -589,21 +632,100 @@ const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取球面上的点对应的国家/地区
|
||||
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;
|
||||
|
||||
// 更新控制器
|
||||
sceneRef.current.controls.update();
|
||||
controls.update();
|
||||
|
||||
// 渲染
|
||||
sceneRef.current.renderer.render(scene, camera);
|
||||
sceneRef.current.labelRenderer.render(scene, camera);
|
||||
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);
|
||||
});
|
||||
|
||||
// 保存场景引用
|
||||
sceneRef.current = {
|
||||
scene,
|
||||
@ -629,76 +751,21 @@ const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
|
||||
// 开始动画
|
||||
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 };
|
||||
} catch (error) {
|
||||
console.error("Three.js初始化失败:", error);
|
||||
}
|
||||
|
||||
// 如果没有直接相交,使用球体辅助检测
|
||||
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<WorldHeatmapProps> = ({ 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依赖
|
||||
|
||||
|
@ -14,7 +14,10 @@ tags: []
|
||||
3. 如果变量没有使用可以前置下划线,消除警告
|
||||
4. 强制类型转换
|
||||
|
||||
```rust
|
||||
let a = 3.1;let b = a as i32
|
||||
```
|
||||
|
||||
5. 打印变量
|
||||
|
||||
```rust
|
||||
|
@ -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 {
|
||||
@ -500,105 +500,196 @@ const tableOfContents = generateTableOfContents(headings);
|
||||
listeners.length = 0;
|
||||
}
|
||||
|
||||
// 1. 应用高亮代码
|
||||
function applyCodeHighlighting() {
|
||||
const codeElements = document.querySelectorAll('code[data-highlighted]');
|
||||
// 1. 增强代码块功能 - 添加标题栏、语言显示和复制按钮
|
||||
function enhanceCodeBlocks() {
|
||||
// 查找所有代码块元素
|
||||
const codeBlocks = document.querySelectorAll('pre > code');
|
||||
if (codeBlocks.length === 0) return;
|
||||
|
||||
codeElements.forEach(codeElement => {
|
||||
const highlightedCode = codeElement.getAttribute('data-highlighted');
|
||||
if (highlightedCode) {
|
||||
codeElement.innerHTML = highlightedCode;
|
||||
codeElement.removeAttribute('data-highlighted');
|
||||
}
|
||||
});
|
||||
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';
|
||||
}
|
||||
|
||||
// 2. 加载Mermaid SVG内容
|
||||
function loadMermaidSvg() {
|
||||
const mermaidContainers = document.querySelectorAll('.mermaid-figure [data-mermaid-src], .mermaid-svg-container[data-mermaid-src]');
|
||||
// 获取原始代码文本,保留换行和格式
|
||||
let originalCode = '';
|
||||
const hasLineElements = codeBlock.querySelectorAll('.line').length > 0 ||
|
||||
codeBlock.innerHTML.includes('<span class="line">');
|
||||
|
||||
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)}`);
|
||||
if (hasLineElements) {
|
||||
// 从行元素中提取文本
|
||||
const lines = codeBlock.querySelectorAll('.line');
|
||||
if (lines.length > 0) {
|
||||
originalCode = Array.from(lines)
|
||||
.map(line => line.textContent)
|
||||
.join('\n');
|
||||
} else {
|
||||
// 使用默认viewBox
|
||||
svgElement.setAttribute('viewBox', '0 0 100 100');
|
||||
// 尝试解析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 || '';
|
||||
}
|
||||
|
||||
// 确保具有mermaid-svg类
|
||||
svgElement.classList.add('mermaid-svg');
|
||||
}
|
||||
// 创建代码块容器
|
||||
const container = document.createElement('div');
|
||||
container.className = 'code-block-container';
|
||||
|
||||
// 移除data属性避免重复加载
|
||||
container.removeAttribute('data-mermaid-src');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('加载SVG失败:', error);
|
||||
container.innerHTML = '<div class="mermaid-error-message">图表加载失败</div>';
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
// 创建标题栏
|
||||
const header = document.createElement('div');
|
||||
header.className = 'code-block-header';
|
||||
|
||||
// 3. 处理代码复制功能
|
||||
function setupCodeCopy() {
|
||||
const handleCopyClick = (e) => {
|
||||
const target = e.target;
|
||||
const copyButton = target.closest('[data-copy]');
|
||||
if (!copyButton) return;
|
||||
// 语言标签
|
||||
const langDiv = document.createElement('div');
|
||||
langDiv.className = 'code-block-lang';
|
||||
langDiv.innerHTML = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="16 18 22 12 16 6"></polyline>
|
||||
<polyline points="8 6 2 12 8 18"></polyline>
|
||||
</svg>
|
||||
${language.toUpperCase()}
|
||||
`;
|
||||
|
||||
const container = copyButton.closest('.code-block-container');
|
||||
if (!container) return;
|
||||
// 复制按钮
|
||||
const copyButton = document.createElement('button');
|
||||
copyButton.className = 'code-block-copy';
|
||||
copyButton.innerHTML = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
复制
|
||||
`;
|
||||
|
||||
const codeElement = container.querySelector('pre code');
|
||||
if (!codeElement) return;
|
||||
// 代码内容容器
|
||||
const codeContent = document.createElement('div');
|
||||
codeContent.className = 'code-block-content';
|
||||
|
||||
const code = codeElement.textContent || '';
|
||||
|
||||
navigator.clipboard.writeText(code)
|
||||
.then(() => {
|
||||
const originalText = copyButton.textContent || '';
|
||||
copyButton.textContent = '已复制!';
|
||||
copyButton.disabled = true;
|
||||
// 添加复制功能
|
||||
addListener(copyButton, 'click', async () => {
|
||||
try {
|
||||
// 使用优化后的方法获取代码文本
|
||||
await navigator.clipboard.writeText(originalCode);
|
||||
copyButton.classList.add('copied');
|
||||
copyButton.innerHTML = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4">
|
||||
<path d="M20 6L9 17l-5-5"></path>
|
||||
</svg>
|
||||
已复制
|
||||
`;
|
||||
|
||||
setTimeout(() => {
|
||||
copyButton.textContent = originalText;
|
||||
copyButton.disabled = false;
|
||||
copyButton.classList.remove('copied');
|
||||
copyButton.innerHTML = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
复制
|
||||
`;
|
||||
}, 2000);
|
||||
})
|
||||
.catch(err => {
|
||||
} catch (err) {
|
||||
console.error('复制失败:', err);
|
||||
alert('复制失败,请手动复制');
|
||||
copyButton.innerHTML = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="12" y1="8" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
||||
</svg>
|
||||
失败
|
||||
`;
|
||||
setTimeout(() => {
|
||||
copyButton.innerHTML = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
复制
|
||||
`;
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
addListener(document, 'click', handleCopyClick);
|
||||
// 组装标题栏
|
||||
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('<span class="line">');
|
||||
|
||||
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, '<')
|
||||
.replace(/>/g, '>');
|
||||
|
||||
// 添加行标记(不添加换行符)
|
||||
newHtml += `<span class="line">${escapedLine}</span>`;
|
||||
});
|
||||
|
||||
// 更新代码块内容,仅在有内容时更新
|
||||
if (newHtml) {
|
||||
codeBlock.innerHTML = newHtml;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 4. 设置阅读进度条
|
||||
@ -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 = '<div class="mermaid-error-message">无法加载Mermaid图表库</div>';
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
// 注册清理函数
|
||||
|
File diff suppressed because it is too large
Load Diff
200
src/styles/articles-code.css
Normal file
200
src/styles/articles-code.css
Normal file
@ -0,0 +1,200 @@
|
||||
/* 代码块容器样式 - 简化背景和阴影 */
|
||||
.code-block-container {
|
||||
margin: 1rem 0;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e2e8f0;
|
||||
background-color: transparent;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 代码块标题栏 */
|
||||
.code-block-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.4rem 0.8rem;
|
||||
background-color: #f1f5f9;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* 代码语言标签 */
|
||||
.code-block-lang {
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.code-block-lang svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
/* 复制按钮 */
|
||||
.code-block-copy {
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #475569;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.code-block-copy:hover {
|
||||
background-color: #e2e8f0;
|
||||
}
|
||||
|
||||
.code-block-copy.copied {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
/* 代码内容容器 - 移除背景 */
|
||||
.code-block-content {
|
||||
position: relative;
|
||||
overflow-x: auto;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* 基础代码块样式 - 减小内边距 */
|
||||
pre {
|
||||
margin: 0;
|
||||
padding: 0.2rem 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
pre code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.5rem;
|
||||
padding: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 行号样式 - 缩小间距 */
|
||||
.line-numbers {
|
||||
counter-reset: line;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 行号背景条 - 减小宽度 */
|
||||
.line-numbers::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 3rem;
|
||||
background-color: #f1f5f9;
|
||||
border-right: 1px solid #e2e8f0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 行样式 - 进一步减小行间距和缩进 */
|
||||
.line-numbers .line {
|
||||
position: relative;
|
||||
counter-increment: line;
|
||||
padding-left: 3.5rem;
|
||||
padding-right: 0.5rem;
|
||||
min-height: 1.5rem;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
/* 行号 - 调整位置 */
|
||||
.line-numbers .line::before {
|
||||
content: counter(line);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 3rem;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
color: #94a3b8;
|
||||
font-size: 0.95rem;
|
||||
user-select: none;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 暗色模式 */
|
||||
[data-theme='dark'] .code-block-container {
|
||||
border-color: #334155;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .code-block-header {
|
||||
background-color: #1e293b;
|
||||
border-bottom-color: #334155;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .code-block-lang {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .code-block-copy {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .code-block-copy:hover {
|
||||
background-color: #334155;
|
||||
}
|
||||
|
||||
/* 移除代码内容区域的黑暗模式背景 */
|
||||
[data-theme='dark'] .code-block-content {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* 暗色模式行号样式 */
|
||||
[data-theme='dark'] .line-numbers::before {
|
||||
background-color: #1e293b;
|
||||
border-right-color: #334155;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .line-numbers .line::before {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
/* 兼容原有高亮 - 确保不添加背景 */
|
||||
[data-theme='dark'] .shiki,
|
||||
[data-theme='dark'] .shiki span {
|
||||
color: var(--shiki-dark) !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .astro-code,
|
||||
[data-theme='dark'] .astro-code span {
|
||||
color: var(--shiki-dark) !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* 确保所有代码元素没有背景 */
|
||||
code, pre, .code-block-content,
|
||||
.code-block-content pre.shiki,
|
||||
.code-block-content pre.astro-code,
|
||||
.code-block-content pre code,
|
||||
.code-block-content pre code span,
|
||||
pre.shiki, pre.astro-code,
|
||||
.line, .line span {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* 高亮行样式 - 仅保留边框而不添加背景 */
|
||||
.line.highlighted {
|
||||
border-left: 2px solid #eab308;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .line.highlighted {
|
||||
border-left: 2px solid #fbbf24;
|
||||
background-color: transparent !important;
|
||||
}
|
150
src/styles/articles-mermaid.css
Normal file
150
src/styles/articles-mermaid.css
Normal file
@ -0,0 +1,150 @@
|
||||
:root {
|
||||
--mermaid-text-color: #1e293b;
|
||||
--mermaid-bg-color: #ffffff;
|
||||
--mermaid-primary-color: #4f46e5;
|
||||
--mermaid-secondary-color: #6366f1;
|
||||
--mermaid-border-color: #9ca3af;
|
||||
--mermaid-line-color: #4b5563;
|
||||
--mermaid-edge-label-bg: #ffffff;
|
||||
--mermaid-title-color: #0f172a;
|
||||
--mermaid-section-bg: #f8fafc;
|
||||
--mermaid-node-bg: #f1f5f9;
|
||||
}
|
||||
|
||||
/* 暗色模式样式变量 */
|
||||
[data-theme='dark'], .dark {
|
||||
--mermaid-text-color: #e2e8f0;
|
||||
--mermaid-bg-color: #1e293b;
|
||||
--mermaid-primary-color: #818cf8;
|
||||
--mermaid-secondary-color: #a5b4fc;
|
||||
--mermaid-border-color: #64748b;
|
||||
--mermaid-line-color: #94a3b8;
|
||||
--mermaid-edge-label-bg: #1e293b;
|
||||
--mermaid-title-color: #f8fafc;
|
||||
--mermaid-section-bg: #334155;
|
||||
--mermaid-node-bg: #475569;
|
||||
}
|
||||
|
||||
/* 基础Mermaid样式覆盖 */
|
||||
.mermaid {
|
||||
background-color: transparent !important;
|
||||
transition: color 0.3s ease, fill 0.3s ease, stroke 0.3s ease;
|
||||
}
|
||||
|
||||
/* Mermaid文本颜色统一 */
|
||||
.mermaid .label,
|
||||
.mermaid text,
|
||||
.mermaid span,
|
||||
.mermaid .messageText,
|
||||
.mermaid .loopText,
|
||||
.mermaid .noteText,
|
||||
.mermaid .taskText {
|
||||
color: var(--mermaid-text-color) !important;
|
||||
fill: var(--mermaid-text-color) !important;
|
||||
font-family: inherit !important;
|
||||
transition: color 0.3s ease, fill 0.3s ease;
|
||||
}
|
||||
|
||||
/* 节点样式设置 */
|
||||
.mermaid .node rect,
|
||||
.mermaid .node circle,
|
||||
.mermaid .node ellipse,
|
||||
.mermaid .node polygon,
|
||||
.mermaid .node path {
|
||||
fill: var(--mermaid-node-bg) !important;
|
||||
stroke: var(--mermaid-border-color) !important;
|
||||
stroke-width: 1px !important;
|
||||
transition: fill 0.3s ease, stroke 0.3s ease;
|
||||
}
|
||||
|
||||
/* 连接线样式 */
|
||||
.mermaid .edgePath .path,
|
||||
.mermaid .flowchart-link,
|
||||
.mermaid line,
|
||||
.mermaid .messageLine0,
|
||||
.mermaid .messageLine1 {
|
||||
stroke: var(--mermaid-line-color) !important;
|
||||
stroke-width: 1px !important;
|
||||
transition: stroke 0.3s ease;
|
||||
}
|
||||
|
||||
/* 箭头填充 */
|
||||
.mermaid .arrowheadPath,
|
||||
.mermaid marker path {
|
||||
fill: var(--mermaid-line-color) !important;
|
||||
stroke: none !important;
|
||||
transition: fill 0.3s ease;
|
||||
}
|
||||
|
||||
/* 文本标签背景 */
|
||||
.mermaid .edgeLabel rect,
|
||||
.mermaid .labelBox {
|
||||
fill: var(--mermaid-edge-label-bg) !important;
|
||||
background-color: var(--mermaid-edge-label-bg) !important;
|
||||
transition: fill 0.3s ease, background-color 0.3s ease;
|
||||
}
|
||||
|
||||
/* 标题样式 */
|
||||
.mermaid .titleText,
|
||||
.mermaid .classTitle,
|
||||
.mermaid .cluster-label text {
|
||||
fill: var(--mermaid-title-color) !important;
|
||||
color: var(--mermaid-title-color) !important;
|
||||
font-weight: bold !important;
|
||||
transition: fill 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
/* 集群/子图样式 */
|
||||
.mermaid .cluster rect,
|
||||
.mermaid .cluster polygon {
|
||||
fill: var(--mermaid-section-bg) !important;
|
||||
stroke: var(--mermaid-border-color) !important;
|
||||
stroke-width: 1px !important;
|
||||
opacity: 0.8 !important;
|
||||
transition: fill 0.3s ease, stroke 0.3s ease;
|
||||
}
|
||||
|
||||
/* 序列图特殊样式 */
|
||||
.mermaid .actor {
|
||||
fill: var(--mermaid-node-bg) !important;
|
||||
stroke: var(--mermaid-border-color) !important;
|
||||
stroke-width: 1px !important;
|
||||
transition: fill 0.3s ease, stroke 0.3s ease;
|
||||
}
|
||||
|
||||
.mermaid .note {
|
||||
fill: var(--mermaid-secondary-color) !important;
|
||||
opacity: 0.7 !important;
|
||||
transition: fill 0.3s ease;
|
||||
}
|
||||
|
||||
/* 甘特图特殊样式 */
|
||||
.mermaid .section0,
|
||||
.mermaid .section2 {
|
||||
fill: var(--mermaid-section-bg) !important;
|
||||
opacity: 0.5 !important;
|
||||
transition: fill 0.3s ease;
|
||||
}
|
||||
|
||||
.mermaid .section1,
|
||||
.mermaid .section3 {
|
||||
fill: var(--mermaid-node-bg) !important;
|
||||
opacity: 0.4 !important;
|
||||
transition: fill 0.3s ease;
|
||||
}
|
||||
|
||||
.mermaid .task0,
|
||||
.mermaid .task1,
|
||||
.mermaid .task2,
|
||||
.mermaid .task3 {
|
||||
fill: var(--mermaid-primary-color) !important;
|
||||
stroke: var(--mermaid-border-color) !important;
|
||||
transition: fill 0.3s ease, stroke 0.3s ease;
|
||||
}
|
||||
|
||||
/* 类图特殊样式 */
|
||||
.mermaid .classLabel .label {
|
||||
fill: var(--mermaid-text-color) !important;
|
||||
color: var(--mermaid-text-color) !important;
|
||||
transition: fill 0.3s ease, color 0.3s ease;
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
@import "./table-styles.css";
|
||||
@import "./code-blocks.css";
|
||||
@import "./mermaid-themes.css";
|
||||
@import "./articles-table.css";
|
||||
@import "./articles-mermaid.css";
|
||||
@import "./articles-code.css";
|
||||
|
||||
/* 增强列表样式 */
|
||||
.prose ul {
|
||||
@ -42,41 +42,6 @@
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* 行内代码样式 */
|
||||
.prose :not(pre) > code {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
color: var(--color-primary-700);
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 0.375em;
|
||||
font-weight: 500;
|
||||
font-family: "JetBrains Mono", Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
font-size: 0.875em;
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
letter-spacing: -0.025em;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.prose :not(pre) > code:hover {
|
||||
background-color: rgba(var(--color-primary-600-rgb), 0.08);
|
||||
border-color: rgba(var(--color-primary-400-rgb), 0.2);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .prose :not(pre) > code {
|
||||
background-color: rgba(255, 255, 255, 0.07);
|
||||
color: var(--color-primary-300);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .prose :not(pre) > code:hover {
|
||||
background-color: rgba(var(--color-primary-500-rgb), 0.15);
|
||||
border-color: rgba(var(--color-primary-300-rgb), 0.3);
|
||||
}
|
||||
|
||||
/* 标题样式 */
|
||||
.prose h1 {
|
||||
font-size: 2.25rem;
|
||||
@ -294,8 +259,7 @@
|
||||
.prose details > p,
|
||||
.prose details > ul,
|
||||
.prose details > ol,
|
||||
.prose details > div,
|
||||
.prose details > pre {
|
||||
.prose details > div {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1em;
|
||||
}
|
@ -1,537 +0,0 @@
|
||||
/* 代码块容器 */
|
||||
.code-block-container {
|
||||
margin: 1.5rem 0;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
background-color: #f5f7fa;
|
||||
border: 1px solid rgba(0,0,0,0.1);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .code-block-container {
|
||||
background-color: #282a36;
|
||||
border-color: rgba(255,255,255,0.1);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
/* 标题栏 */
|
||||
.code-block-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: #e2e8f0;
|
||||
border-bottom: 1px solid #cbd5e0;
|
||||
font-family: 'JetBrains Mono', Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .code-block-title {
|
||||
background-color: #343746;
|
||||
border-bottom: 1px solid #44475a;
|
||||
color: #f8f8f2;
|
||||
}
|
||||
|
||||
/* 标题栏左侧 */
|
||||
.code-title-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* 标题栏右侧 */
|
||||
.code-title-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 语言标识 */
|
||||
.code-language {
|
||||
text-transform: uppercase;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .code-language {
|
||||
color: #bd93f9;
|
||||
}
|
||||
|
||||
/* 文件名 */
|
||||
.code-filename {
|
||||
margin-left: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: #4a5568;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .code-filename {
|
||||
color: #f1fa8c;
|
||||
}
|
||||
|
||||
/* 复制按钮 */
|
||||
.copy-button {
|
||||
background: transparent;
|
||||
border: 1px solid #a0aec0;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.copy-button:hover {
|
||||
background: #edf2f7;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .copy-button {
|
||||
border-color: #44475a;
|
||||
color: #f8f8f2;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .copy-button:hover {
|
||||
background: #44475a;
|
||||
}
|
||||
|
||||
/* 代码内容区 */
|
||||
.code-block-content {
|
||||
padding: 1rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.code-block-content pre {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
overflow: visible;
|
||||
font-family: 'JetBrains Mono', Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.code-block-content code {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
font-family: inherit;
|
||||
color: inherit;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* ----------------------- */
|
||||
/* 终端样式 */
|
||||
/* ----------------------- */
|
||||
|
||||
.terminal-container {
|
||||
background-color: #f8fafc;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
border: 1px solid #adc2ff;
|
||||
box-shadow: 0 8px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
[data-theme="dark"] .terminal-container {
|
||||
background-color: #1a1e2a;
|
||||
border-color: rgba(255,255,255,0.1);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .terminal-container::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 终端标题栏 */
|
||||
.terminal-container .code-block-title {
|
||||
background-color: #f1f5f9;
|
||||
border-bottom: 1px solid #adc2ff;
|
||||
padding: 0.4rem 1rem;
|
||||
color: #334155;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .terminal-container .code-block-title {
|
||||
background-color: #111827;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
/* 终端控制按钮 */
|
||||
.terminal-controls {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.terminal-control {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.terminal-close {
|
||||
background-color: #f87171;
|
||||
}
|
||||
|
||||
.terminal-minimize {
|
||||
background-color: #fbbf24;
|
||||
}
|
||||
|
||||
.terminal-maximize {
|
||||
background-color: #34d399;
|
||||
}
|
||||
|
||||
/* 终端内容区 */
|
||||
.terminal-container .code-block-content {
|
||||
background-color: #f8fafc;
|
||||
color: #334155;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .terminal-container .code-block-content {
|
||||
background-color: #1a1e2a;
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
.terminal-pre {
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .terminal-pre {
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
/* 语言标签在终端样式中的颜色 */
|
||||
.terminal-container .code-language {
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .terminal-container .code-language {
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
/* 终端中的复制按钮 */
|
||||
.terminal-container .copy-button {
|
||||
border-color: #adc2ff;
|
||||
color: #4a5568;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.terminal-container .copy-button:hover {
|
||||
background: #ebf0ff;
|
||||
color: #4b6bff;
|
||||
border-color: #4b6bff;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .terminal-container .copy-button {
|
||||
border-color: rgba(255,255,255,0.3);
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .terminal-container .copy-button:hover {
|
||||
background: rgba(255,255,255,0.15);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* ----------------------- */
|
||||
/* Mermaid 图表样式 */
|
||||
/* ----------------------- */
|
||||
|
||||
.mermaid-figure {
|
||||
margin: 2rem auto;
|
||||
text-align: center;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mermaid-figure img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* 亮色主题图表 */
|
||||
.light-theme-diagram {
|
||||
display: block;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .light-theme-diagram {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 暗色主题图表 */
|
||||
.dark-theme-diagram {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .dark-theme-diagram {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Mermaid 错误样式 */
|
||||
.mermaid-error {
|
||||
border: 1px solid #e53e3e;
|
||||
background-color: #fff5f5;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .mermaid-error {
|
||||
background-color: #3b1a1a;
|
||||
border-color: #fc8181;
|
||||
}
|
||||
|
||||
.mermaid-error-message {
|
||||
color: #c53030;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .mermaid-error-message {
|
||||
color: #fc8181;
|
||||
}
|
||||
|
||||
/* ----------------------- */
|
||||
/* Highlight.js 主题样式 */
|
||||
/* ----------------------- */
|
||||
|
||||
/* 基本颜色方案 - 亮色 */
|
||||
.hljs {
|
||||
color: #1a202c;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .hljs {
|
||||
color: #f8f8f2;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* 注释 */
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #718096;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .hljs-comment,
|
||||
[data-theme="dark"] .hljs-quote {
|
||||
color: #6272a4;
|
||||
}
|
||||
|
||||
/* 关键字 */
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag,
|
||||
.hljs-addition {
|
||||
color: #805ad5;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .hljs-keyword,
|
||||
[data-theme="dark"] .hljs-selector-tag,
|
||||
[data-theme="dark"] .hljs-addition {
|
||||
color: #ff79c6;
|
||||
}
|
||||
|
||||
/* 变量名和属性 */
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-literal,
|
||||
.hljs-attr,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-pseudo,
|
||||
.hljs-meta {
|
||||
color: #dd6b20;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .hljs-variable,
|
||||
[data-theme="dark"] .hljs-template-variable,
|
||||
[data-theme="dark"] .hljs-literal,
|
||||
[data-theme="dark"] .hljs-attr,
|
||||
[data-theme="dark"] .hljs-selector-attr,
|
||||
[data-theme="dark"] .hljs-selector-pseudo,
|
||||
[data-theme="dark"] .hljs-meta {
|
||||
color: #f1fa8c;
|
||||
}
|
||||
|
||||
/* 函数名 */
|
||||
.hljs-title,
|
||||
.hljs-function .hljs-title,
|
||||
.hljs-section {
|
||||
color: #3182ce;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .hljs-title,
|
||||
[data-theme="dark"] .hljs-function .hljs-title,
|
||||
[data-theme="dark"] .hljs-section {
|
||||
color: #50fa7b;
|
||||
}
|
||||
|
||||
/* 类名和ID */
|
||||
.hljs-title.class_,
|
||||
.hljs-type,
|
||||
.hljs-class .hljs-title,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class {
|
||||
color: #d53f8c;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .hljs-title.class_,
|
||||
[data-theme="dark"] .hljs-type,
|
||||
[data-theme="dark"] .hljs-class .hljs-title,
|
||||
[data-theme="dark"] .hljs-selector-id,
|
||||
[data-theme="dark"] .hljs-selector-class {
|
||||
color: #8be9fd;
|
||||
}
|
||||
|
||||
/* 字符串 */
|
||||
.hljs-string,
|
||||
.hljs-regexp,
|
||||
.hljs-attribute,
|
||||
.hljs-doctag {
|
||||
color: #38a169;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .hljs-string,
|
||||
[data-theme="dark"] .hljs-regexp,
|
||||
[data-theme="dark"] .hljs-attribute,
|
||||
[data-theme="dark"] .hljs-doctag {
|
||||
color: #f1fa8c;
|
||||
}
|
||||
|
||||
/* 数字 */
|
||||
.hljs-number,
|
||||
.hljs-deletion,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet,
|
||||
.hljs-link,
|
||||
.hljs-formula {
|
||||
color: #e53e3e;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .hljs-number,
|
||||
[data-theme="dark"] .hljs-deletion,
|
||||
[data-theme="dark"] .hljs-symbol,
|
||||
[data-theme="dark"] .hljs-bullet,
|
||||
[data-theme="dark"] .hljs-link,
|
||||
[data-theme="dark"] .hljs-formula {
|
||||
color: #bd93f9;
|
||||
}
|
||||
|
||||
/* HTML标签 */
|
||||
.hljs-tag,
|
||||
.hljs-name,
|
||||
.hljs-selector-tag {
|
||||
color: #2b6cb0;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .hljs-tag,
|
||||
[data-theme="dark"] .hljs-name,
|
||||
[data-theme="dark"] .hljs-selector-tag {
|
||||
color: #ff79c6;
|
||||
}
|
||||
|
||||
/* 内置对象 */
|
||||
.hljs-built_in {
|
||||
color: #c05621;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .hljs-built_in {
|
||||
color: #8be9fd;
|
||||
}
|
||||
|
||||
/* 高亮代码行 */
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 终端命令特殊着色 */
|
||||
.terminal-container .hljs-built_in,
|
||||
.terminal-container .hljs-keyword {
|
||||
color: #4b6bff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .terminal-container .hljs-built_in,
|
||||
[data-theme="dark"] .terminal-container .hljs-keyword {
|
||||
color: #9ae6b4;
|
||||
}
|
||||
|
||||
/* 终端中的字符串 */
|
||||
.terminal-container .hljs-string {
|
||||
color: #ea580c;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .terminal-container .hljs-string {
|
||||
color: #fbd38d;
|
||||
}
|
||||
|
||||
/* 终端中的变量 */
|
||||
.terminal-container .hljs-variable {
|
||||
color: #8b5cf6;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .terminal-container .hljs-variable {
|
||||
color: #b794f4;
|
||||
}
|
||||
|
||||
/* 终端中的参数 */
|
||||
.terminal-container .hljs-params {
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .terminal-container .hljs-params {
|
||||
color: #cbd5e0;
|
||||
}
|
||||
|
||||
/* 终端中的注释 */
|
||||
.terminal-container .hljs-comment {
|
||||
color: #64748b;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .terminal-container .hljs-comment {
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.code-block-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.code-block-content::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.code-block-content::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.code-block-content::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .code-block-content::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .code-block-content::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .code-block-content::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
@ -1,231 +0,0 @@
|
||||
/* Mermaid图表主题样式
|
||||
* 支持亮色/暗色主题切换
|
||||
*/
|
||||
|
||||
/* 图表容器样式 */
|
||||
.mermaid-figure {
|
||||
margin: 2rem auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.mermaid-svg-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
max-height: 80vh; /* 限制容器最大高度为视窗高度的80% */
|
||||
}
|
||||
|
||||
/* 错误信息样式 */
|
||||
.mermaid-error-message {
|
||||
color: #e53e3e;
|
||||
padding: 1rem;
|
||||
border: 1px dashed #e53e3e;
|
||||
border-radius: 0.5rem;
|
||||
margin: 1rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 基础样式 - 亮色主题 */
|
||||
.mermaid-svg {
|
||||
/* 全局图表容器样式 */
|
||||
max-width: 100%;
|
||||
height: auto !important; /* 强制高度为自动,覆盖内联样式 */
|
||||
max-height: 80vh; /* 限制最大高度为视窗高度的80% */
|
||||
width: auto; /* 允许宽度自适应内容 */
|
||||
overflow: visible;
|
||||
/* 移除背景色,使用透明背景 */
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* 节点样式 */
|
||||
.mermaid-svg .node rect,
|
||||
.mermaid-svg .node circle,
|
||||
.mermaid-svg .node ellipse,
|
||||
.mermaid-svg .node polygon,
|
||||
.mermaid-svg .node path {
|
||||
fill: #f8fafc;
|
||||
stroke: #94a3b8;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
/* 基本填充和背景色 */
|
||||
.mermaid-svg .basic {
|
||||
fill: #f8fafc;
|
||||
stroke: #94a3b8;
|
||||
}
|
||||
|
||||
.mermaid-svg .labelBkg {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* 集群样式 */
|
||||
.mermaid-svg .cluster rect {
|
||||
fill: #f1f5f9;
|
||||
stroke: #cbd5e1;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
/* 边标签样式 */
|
||||
.mermaid-svg .edgeLabel rect {
|
||||
fill: transparent;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* 文本样式 */
|
||||
.mermaid-svg text {
|
||||
fill: #334155;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
.mermaid-svg .label {
|
||||
color: #334155;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.mermaid-svg .nodeLabel {
|
||||
color: #334155;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.mermaid-svg .cluster text {
|
||||
fill: #475569;
|
||||
}
|
||||
|
||||
/* 连线样式 */
|
||||
.mermaid-svg .edgePath .path {
|
||||
stroke: #94a3b8;
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
|
||||
.mermaid-svg .flowchart-link {
|
||||
stroke: #94a3b8;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.mermaid-svg marker {
|
||||
fill: #94a3b8;
|
||||
}
|
||||
|
||||
/* 箭头路径 */
|
||||
.mermaid-svg .arrowMarkerPath {
|
||||
fill: #94a3b8;
|
||||
stroke: #94a3b8;
|
||||
}
|
||||
|
||||
/* 特殊节点样式 */
|
||||
.mermaid-svg .node.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mermaid-svg .node.clickable:hover rect,
|
||||
.mermaid-svg .node.clickable:hover circle,
|
||||
.mermaid-svg .node.clickable:hover ellipse,
|
||||
.mermaid-svg .node.clickable:hover polygon {
|
||||
stroke-width: 2px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* ======= 暗色主题样式 ======= */
|
||||
[data-theme="dark"] .mermaid-svg {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .mermaid-svg .node rect,
|
||||
[data-theme="dark"] .mermaid-svg .node circle,
|
||||
[data-theme="dark"] .mermaid-svg .node ellipse,
|
||||
[data-theme="dark"] .mermaid-svg .node polygon,
|
||||
[data-theme="dark"] .mermaid-svg .node path {
|
||||
fill: #1e293b;
|
||||
stroke: #475569;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .mermaid-svg .basic {
|
||||
fill: #1e293b;
|
||||
stroke: #475569;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .mermaid-svg .labelBkg {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .mermaid-svg .cluster rect {
|
||||
fill: #0f172a;
|
||||
stroke: #334155;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .mermaid-svg .edgeLabel rect {
|
||||
fill: transparent;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .mermaid-svg text {
|
||||
fill: #e2e8f0;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .mermaid-svg .label {
|
||||
color: #e2e8f0;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .mermaid-svg .nodeLabel {
|
||||
color: #e2e8f0;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .mermaid-svg .cluster text {
|
||||
fill: #cbd5e1;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .mermaid-svg .edgePath .path {
|
||||
stroke: #64748b;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .mermaid-svg .flowchart-link {
|
||||
stroke: #64748b;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .mermaid-svg marker {
|
||||
fill: #64748b;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .mermaid-svg .arrowMarkerPath {
|
||||
fill: #64748b;
|
||||
stroke: #64748b;
|
||||
}
|
||||
|
||||
/* 序列图特殊样式 */
|
||||
[data-theme="dark"] .mermaid-svg .actor {
|
||||
fill: #334155 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .mermaid-svg .messageLine0 {
|
||||
stroke: #94a3b8 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .mermaid-svg .messageLine1 {
|
||||
stroke: #94a3b8 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .mermaid-svg #arrowhead path {
|
||||
fill: #94a3b8 !important;
|
||||
}
|
||||
|
||||
/* 甘特图特殊样式 */
|
||||
[data-theme="dark"] .mermaid-svg .section0 {
|
||||
fill: rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .mermaid-svg .section1,
|
||||
[data-theme="dark"] .mermaid-svg .section2 {
|
||||
fill: rgba(255, 255, 255, 0.07) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .mermaid-svg .task0,
|
||||
[data-theme="dark"] .mermaid-svg .task1,
|
||||
[data-theme="dark"] .mermaid-svg .task2,
|
||||
[data-theme="dark"] .mermaid-svg .task3 {
|
||||
fill: #1e3a8a !important;
|
||||
}
|
Loading…
Reference in New Issue
Block a user