From 8d6e7a3502385858a8b6afab4eb11ddb8ac8fb9d Mon Sep 17 00:00:00 2001 From: lsy Date: Fri, 28 Mar 2025 18:54:07 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20create=5Fpost.sh=20?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=E4=BB=A5=E5=85=81=E8=AE=B8=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E4=B8=BA=E7=A9=BA=EF=BC=8C=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=90=8D=E7=94=9F=E6=88=90=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=9B=E4=BF=AE=E6=94=B9=20GitProjectCollection=20=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E4=BB=A5=E7=AE=80=E5=8C=96=E9=85=8D=E7=BD=AE=EF=BC=8C?= =?UTF-8?q?=E7=A1=AE=E4=BF=9D=E7=94=A8=E6=88=B7=E5=90=8D=E4=B8=BA=E5=BF=85?= =?UTF-8?q?=E5=A1=AB=E9=A1=B9=EF=BC=9B=E5=9C=A8=20WorldHeatmap=20=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E4=B8=AD=E6=B7=BB=E5=8A=A0=E4=B8=BB=E9=A2=98=E5=8F=98?= =?UTF-8?q?=E5=8C=96=E7=9B=91=E5=90=AC=EF=BC=8C=E4=BC=98=E5=8C=96=E9=BC=A0?= =?UTF-8?q?=E6=A0=87=E4=BA=A4=E4=BA=92=E5=92=8C=E5=9B=BD=E5=AE=B6=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E9=80=BB=E8=BE=91=EF=BC=9B=E6=9B=B4=E6=96=B0=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E4=BB=A5=E5=8C=85=E5=90=AB=E6=97=85=E8=A1=8C=E8=B6=B3?= =?UTF-8?q?=E8=BF=B9=E5=8A=9F=E8=83=BD=E7=9A=84=E4=BD=BF=E7=94=A8=E7=A4=BA?= =?UTF-8?q?=E4=BE=8B=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- create_post.sh | 8 +- src/components/GitProjectCollection.tsx | 40 +- src/components/WorldHeatmap.tsx | 1128 ++++++++++------------- src/content/web/echoes博客使用说明.md | 71 +- src/pages/projects.astro | 9 +- 5 files changed, 555 insertions(+), 701 deletions(-) diff --git a/create_post.sh b/create_post.sh index 119c6aa..c4f3f5c 100644 --- a/create_post.sh +++ b/create_post.sh @@ -6,15 +6,15 @@ PROJECT_ROOT="$(cd "$(dirname "$0")" && pwd)" # 如果没有提供参数,使用交互式输入 if [ "$#" -lt 2 ]; then read -rp "请输入文章标题: " TITLE - read -rp "请输入文章路径 (例如: web/my-post): " PATH_ARG + read -rp "请输入文章路径 (例如: web/my-post),也可以为空: " PATH_ARG else TITLE=$1 PATH_ARG=$2 fi # 检查输入是否为空 -if [ -z "$TITLE" ] || [ -z "$PATH_ARG" ]; then - echo "错误: 标题和路径不能为空" +if [ -z "$TITLE" ] ; then + echo "错误: 标题不能为空" echo "使用方法: $0 <标题> <路径>" echo "示例: $0 \"我的新文章\" \"web/my-post\"" exit 1 @@ -31,7 +31,7 @@ FULL_PATH="$CONTENT_DIR/$PATH_ARG" mkdir -p "$FULL_PATH" # 构建最终的文件路径 -FILENAME="$FULL_PATH/$(basename "$PATH_ARG").md" +FILENAME="$FULL_PATH/$(basename "$TITLE").md" ABSOLUTE_PATH="$(cd "$(dirname "$FILENAME")" 2>/dev/null && pwd)/$(basename "$FILENAME")" # 检查文件是否已存在 diff --git a/src/components/GitProjectCollection.tsx b/src/components/GitProjectCollection.tsx index b0831ad..c2e7526 100644 --- a/src/components/GitProjectCollection.tsx +++ b/src/components/GitProjectCollection.tsx @@ -8,18 +8,9 @@ export enum GitPlatform { GITEE = 'gitee' } -// Git 配置接口 -export type GitConfig = { - username: string; - token?: string; - perPage?: number; - url?: string; -}; - // 平台默认配置 export const DEFAULT_GIT_CONFIG = { - perPage: 10, - giteaUrl: '' + perPage: 10 }; // 内部使用的平台配置 @@ -62,10 +53,12 @@ interface Pagination { interface GitProjectCollectionProps { platform: GitPlatform; - username?: string; + username: string; organization?: string; title?: string; - config: GitConfig; + token?: string; + perPage?: number; + url?: string; } const GitProjectCollection: React.FC = ({ @@ -73,7 +66,9 @@ const GitProjectCollection: React.FC = ({ username, organization, title, - config + token, + perPage = DEFAULT_GIT_CONFIG.perPage, + url }) => { const [projects, setProjects] = useState([]); const [pagination, setPagination] = useState({ current: 1, total: 1, hasNext: false, hasPrev: false }); @@ -81,8 +76,6 @@ const GitProjectCollection: React.FC = ({ const [error, setError] = useState(null); const [isPageChanging, setIsPageChanging] = useState(false); - const effectiveUsername = username || config.username; - const fetchData = async (page = 1) => { setLoading(true); @@ -97,10 +90,19 @@ const GitProjectCollection: React.FC = ({ baseUrl.searchParams.append('platform', platform); baseUrl.searchParams.append('page', page.toString()); + + // 构建配置对象 + const config = { + username, + token, + perPage, + url + }; + baseUrl.searchParams.append('config', JSON.stringify(config)); - if (effectiveUsername) { - baseUrl.searchParams.append('username', effectiveUsername); + if (username) { + baseUrl.searchParams.append('username', username); } if (organization) { @@ -132,7 +134,7 @@ const GitProjectCollection: React.FC = ({ useEffect(() => { fetchData(1); - }, [platform, effectiveUsername, organization]); + }, [platform, username, organization, token, perPage, url]); const handlePageChange = (page: number) => { if (isPageChanging) return; @@ -219,7 +221,7 @@ const GitProjectCollection: React.FC = ({

{displayTitle} - {effectiveUsername && (@{effectiveUsername})} + {username && (@{username})} {organization && (组织: {organization})}

diff --git a/src/components/WorldHeatmap.tsx b/src/components/WorldHeatmap.tsx index fd7c9cb..9c852fe 100644 --- a/src/components/WorldHeatmap.tsx +++ b/src/components/WorldHeatmap.tsx @@ -12,6 +12,12 @@ interface WorldHeatmapProps { const WorldHeatmap: React.FC = ({ visitedPlaces }) => { const containerRef = useRef(null); const [hoveredCountry, setHoveredCountry] = useState(null); + const [theme, setTheme] = useState<'light' | 'dark'>( + typeof document !== 'undefined' && + (document.documentElement.classList.contains('dark') || document.documentElement.getAttribute('data-theme') === 'dark') + ? 'dark' : 'light' + ); + const sceneRef = useRef<{ scene: THREE.Scene; camera: THREE.PerspectiveCamera; @@ -19,7 +25,6 @@ const WorldHeatmap: React.FC = ({ visitedPlaces }) => { labelRenderer: CSS2DRenderer; controls: OrbitControls; earth: THREE.Mesh; - bgSphere: THREE.Mesh; countries: Map; raycaster: THREE.Raycaster; mouse: THREE.Vector2; @@ -27,8 +32,49 @@ const WorldHeatmap: React.FC = ({ visitedPlaces }) => { lastCameraPosition: THREE.Vector3 | null; lastMouseEvent: MouseEvent | null; lastClickedCountry: string | null; + lastMouseX: number | null; + lastMouseY: number | null; + lastHoverTime: number | null; + regionImportance?: Map; + importanceThreshold?: number; } | null>(null); + // 监听主题变化 + useEffect(() => { + const handleThemeChange = () => { + const isDark = + document.documentElement.classList.contains('dark') || + document.documentElement.getAttribute('data-theme') === 'dark'; + setTheme(isDark ? 'dark' : 'light'); + }; + + // 创建 MutationObserver 来监听 class 和 data-theme 属性的变化 + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if ( + (mutation.attributeName === 'class' && mutation.target === document.documentElement) || + (mutation.attributeName === 'data-theme' && mutation.target === document.documentElement) + ) { + handleThemeChange(); + } + }); + }); + + // 开始观察 + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['class', 'data-theme'] + }); + + // 初始检查 + handleThemeChange(); + + // 清理 + return () => { + observer.disconnect(); + }; + }, []); + useEffect(() => { if (!containerRef.current) return; @@ -44,20 +90,19 @@ const WorldHeatmap: React.FC = ({ visitedPlaces }) => { } // 检查当前是否为暗色模式 - const isDarkMode = document.documentElement.classList.contains('dark'); + const isDarkMode = document.documentElement.classList.contains('dark') || + document.documentElement.getAttribute('data-theme') === 'dark'; // 根据当前模式设置颜色 const getColors = () => { return { - background: 'transparent', // 改为透明背景 - earthBase: isDarkMode ? '#1f2937' : '#e5e7eb', - visited: isDarkMode ? '#059669' : '#10b981', - highlight: isDarkMode ? '#f59e0b' : '#f59e0b', - border: isDarkMode ? '#4b5563' : '#9ca3af', - visitedBorder: isDarkMode ? '#059669' : '#10b981', - chinaBorder: isDarkMode ? '#ef4444' : '#ef4444', // 中国边界使用红色 - text: isDarkMode ? '#ffffff' : '#374151', - bgSphere: isDarkMode ? '#111827' : '#f3f4f6', // 背景球体颜色 + earthBase: isDarkMode ? '#111827' : '#f3f4f6', // 深色模式更暗,浅色模式更亮 + visited: isDarkMode ? '#065f46' : '#34d399', // 访问过的颜色更鲜明 + border: isDarkMode ? '#6b7280' : '#d1d5db', // 边界颜色更柔和 + visitedBorder: isDarkMode ? '#10b981' : '#059669', // 访问过的边界颜色更鲜明 + chinaBorder: isDarkMode ? '#f87171' : '#ef4444', // 中国边界使用红色 + text: isDarkMode ? '#f9fafb' : '#1f2937', // 文本颜色对比更强 + highlight: isDarkMode ? '#fbbf24' : '#d97706', // 高亮颜色更适合当前主题 }; }; @@ -65,43 +110,42 @@ const WorldHeatmap: React.FC = ({ visitedPlaces }) => { // 创建场景 const scene = new THREE.Scene(); - // 将背景设置为透明 scene.background = null; - // 创建不透明材质的辅助函数 - const createOpaqueMaterial = (color: string, side: THREE.Side = THREE.FrontSide, renderOrder: number = 0) => { - const material = new THREE.MeshBasicMaterial({ + // 添加一个动态计算小区域的机制 + const regionSizeMetrics = new Map(); + + // 创建材质的辅助函数 + const createMaterial = (color: string, side: THREE.Side = THREE.FrontSide, opacity: number = 1.0) => { + return new THREE.MeshBasicMaterial({ color: color, side: side, - transparent: false, - opacity: 1.0, - depthTest: true, - depthWrite: true + transparent: true, + opacity: opacity }); - return material; }; - // 创建地球前,先创建一个背景球体 - 设置为对应主题的背景色 - const bgSphereGeometry = new THREE.SphereGeometry(2.1, 64, 64); - const bgSphereMaterial = createOpaqueMaterial(colors.bgSphere, THREE.BackSide); - const bgSphere = new THREE.Mesh(bgSphereGeometry, bgSphereMaterial); - bgSphere.renderOrder = 0; - scene.add(bgSphere); - - // 创建一个中间层球体,防止看透 - 设置为对应主题的背景色 - const midSphereGeometry = new THREE.SphereGeometry(2.0, 64, 64); - const midSphereMaterial = createOpaqueMaterial(colors.bgSphere); - const midSphere = new THREE.Mesh(midSphereGeometry, midSphereMaterial); - midSphere.renderOrder = 1; - scene.add(midSphere); - - // 在地球内部添加一个实心球体 - 设置为对应主题的背景色 - const innerSphereGeometry = new THREE.SphereGeometry(1.97, 32, 32); - const innerSphereMaterial = createOpaqueMaterial(colors.bgSphere); - const innerSphere = new THREE.Mesh(innerSphereGeometry, innerSphereMaterial); - innerSphere.renderOrder = 1.5; - scene.add(innerSphere); - + // 创建地球几何体 + const earthGeometry = new THREE.SphereGeometry(2.0, 64, 64); + const earthMaterial = createMaterial(colors.earthBase, THREE.FrontSide, isDarkMode ? 0.9 : 0.8); + const earth = new THREE.Mesh(earthGeometry, earthMaterial); + earth.renderOrder = 1; + scene.add(earth); + + // 添加光源 + const ambientLight = new THREE.AmbientLight(0xffffff, isDarkMode ? 0.7 : 0.8); + scene.add(ambientLight); + + const directionalLight = new THREE.DirectionalLight(isDarkMode ? 0xeeeeff : 0xffffff, isDarkMode ? 0.6 : 0.5); + directionalLight.position.set(5, 3, 5); + scene.add(directionalLight); + // 创建相机 const camera = new THREE.PerspectiveCamera( 45, @@ -114,13 +158,12 @@ const WorldHeatmap: React.FC = ({ visitedPlaces }) => { // 创建渲染器 const renderer = new THREE.WebGLRenderer({ antialias: true, - alpha: true, // 启用alpha通道 + 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); @@ -137,18 +180,16 @@ const WorldHeatmap: React.FC = ({ visitedPlaces }) => { // 添加控制器 const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; - controls.dampingFactor = 0.05; - controls.rotateSpeed = 0.5; + controls.dampingFactor = 0.25; // 大幅增加阻尼因子,从0.1到0.25提高稳定性 + controls.rotateSpeed = 0.2; // 降低旋转速度,提高稳定性 controls.autoRotate = true; - controls.autoRotateSpeed = 0.5; + 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); @@ -156,31 +197,18 @@ const WorldHeatmap: React.FC = ({ visitedPlaces }) => { } }); - // 创建地球几何体,注意减小尺寸防止Z-fighting - const earthGeometry = new THREE.SphereGeometry(1.95, 64, 64); - - // 使用不透明的基础材质 - const earthMaterial = createOpaqueMaterial(colors.earthBase, THREE.FrontSide); - - const earth = new THREE.Mesh(earthGeometry, earthMaterial); - earth.matrixAutoUpdate = false; - earth.updateMatrix(); - earth.renderOrder = 2; - scene.add(earth); - - // 添加光源 - const ambientLight = new THREE.AmbientLight(0xffffff, 0.8); - scene.add(ambientLight); - - const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2); - directionalLight.position.set(5, 3, 5); - scene.add(directionalLight); - // 创建国家边界 const countries = new Map(); const countryGroup = new THREE.Group(); earth.add(countryGroup); + // 保存所有线条对象的引用,用于快速检测 + const allLineObjects: THREE.Line[] = []; + const lineToCountryMap = new Map(); + + // 保存所有国家和省份的边界盒,用于优化检测 + const countryBoundingBoxes = new Map(); + // 创建一个辅助函数,用于将经纬度转换为三维坐标 const latLongToVector3 = (lat: number, lon: number, radius: number): THREE.Vector3 => { // 调整经度范围,确保它在[-180, 180]之间 @@ -198,7 +226,6 @@ const WorldHeatmap: React.FC = ({ visitedPlaces }) => { }; // 省份边界和中心点数据结构 - const provinceBoundaries = new Map(); const provinceCenters = new Map(); // 创建一个通用函数,用于处理地理特性(国家或省份) @@ -229,8 +256,9 @@ const WorldHeatmap: React.FC = ({ visitedPlaces }) => { let centerLon = 0; let centerLat = 0; let pointCount = 0; - let largestPolygonArea = 0; - let largestPolygonCenter = { lon: 0, lat: 0 }; + + // 创建边界盒用于碰撞检测 + const boundingBox = new THREE.Box3(); // 首先检查GeoJSON特性中是否有预定义的中心点 let hasPreDefinedCenter = false; @@ -244,22 +272,14 @@ const WorldHeatmap: React.FC = ({ visitedPlaces }) => { centerLat = cpLat; // 保存预定义中心点 - provinceCenters.set(regionName, centerVector); + if (regionType === 'province') { + provinceCenters.set(regionName, centerVector); + } } // 存储区域边界 const boundaries: THREE.Vector3[][] = []; - // 计算多边形面积的辅助函数 - const calculatePolygonArea = (coords: number[][]) => { - let area = 0; - for (let i = 0, j = coords.length - 1; i < coords.length; j = i++) { - area += coords[i][0] * coords[j][1]; - area -= coords[i][1] * coords[j][0]; - } - return Math.abs(area / 2); - }; - // 处理多边形坐标 const processPolygon = (polygonCoords: any) => { const points: THREE.Vector3[] = []; @@ -273,35 +293,49 @@ const WorldHeatmap: React.FC = ({ visitedPlaces }) => { pointCount++; // 使用辅助函数将经纬度转换为3D坐标 - points.push(latLongToVector3(lat, lon, scale)); + const vertex = latLongToVector3(lat, lon, scale); + points.push(vertex); + + // 扩展边界盒以包含此点 + boundingBox.expandByPoint(vertex); }); - // 计算当前多边形的面积和中心 - if (regionType === 'country') { - const area = calculatePolygonArea(polygonCoords); - if (area > largestPolygonArea) { - largestPolygonArea = area; - - // 计算多边形中心 - let polyLon = 0; - let polyLat = 0; - polygonCoords.forEach((point: number[]) => { - polyLon += point[0]; - polyLat += point[1]; - }); - - largestPolygonCenter = { - lon: polyLon / polygonCoords.length, - lat: polyLat / polygonCoords.length - }; - } - } - // 保存边界多边形 if (points.length > 2) { boundaries.push(points); } + // 收集区域大小指标 + if (!regionSizeMetrics.has(regionName)) { + regionSizeMetrics.set(regionName, {}); + } + + const metrics = regionSizeMetrics.get(regionName)!; + if (points.length > 2) { + // 计算边界框大小 + let minX = Infinity, minY = Infinity, minZ = Infinity; + let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity; + + points.forEach(point => { + minX = Math.min(minX, point.x); + minY = Math.min(minY, point.y); + minZ = Math.min(minZ, point.z); + maxX = Math.max(maxX, point.x); + maxY = Math.max(maxY, point.y); + maxZ = Math.max(maxZ, point.z); + }); + + const sizeX = maxX - minX; + const sizeY = maxY - minY; + const sizeZ = maxZ - minZ; + const boxSize = Math.sqrt(sizeX * sizeX + sizeY * sizeY + sizeZ * sizeZ); + + // 更新或初始化指标 + metrics.boundingBoxSize = metrics.boundingBoxSize ? + Math.max(metrics.boundingBoxSize, boxSize) : boxSize; + metrics.pointCount = (metrics.pointCount || 0) + points.length; + } + // 创建边界线 if (points.length > 1) { const lineGeometry = new THREE.BufferGeometry().setFromPoints(points); @@ -311,11 +345,7 @@ const WorldHeatmap: React.FC = ({ visitedPlaces }) => { : (borderColor || colors.border), linewidth: isRegionVisited ? 1.5 : 1, transparent: true, - opacity: isRegionVisited ? 0.9 : 0.7, - depthTest: false, // 禁用深度测试,解决Z-fighting问题 - polygonOffset: true, // 启用多边形偏移 - polygonOffsetFactor: isRegionVisited ? -2 : -1, // 已访问区域的边界线偏移更多,确保在上层 - polygonOffsetUnits: 1 + opacity: isRegionVisited ? 0.9 : 0.7 }); const line = new THREE.Line(lineGeometry, lineMaterial); @@ -324,111 +354,17 @@ const WorldHeatmap: React.FC = ({ visitedPlaces }) => { isVisited: isRegionVisited, originalColor: isRegionVisited ? (visitedBorderColor || colors.visitedBorder) - : (borderColor || colors.border) + : (borderColor || colors.border), + highlightColor: colors.highlight // 使用主题颜色中定义的高亮颜色 }; - // 设置已访问区域的边界线渲染顺序更高,确保它们始终绘制在未访问区域边界线的上方 + + // 设置渲染顺序 line.renderOrder = isRegionVisited ? 3 : 2; regionObject.add(line); - // 如果是已访问的地区,为这个边界创建一个填充面 - if (isRegionVisited && points.length >= 3) { - try { - // 使用ShapeGeometry创建一个平面填充 - // 首先创建一个2D平面上的形状 - const center = new THREE.Vector3(0, 0, 0); - // 将3D点投影到以该点为中心的平面上 - const projectedPoints = points.map(p => { - // 计算从地球中心到点的方向向量 - const dir = p.clone().normalize(); - // 计算投影平面的法向量(就是该点的方向) - const normal = dir.clone(); - // 创建一个与地球表面近似切线的平面 - const plane = new THREE.Plane().setFromNormalAndCoplanarPoint(normal, p); - - // 计算从中心到各点的投影坐标 - const v1 = new THREE.Vector3(); - const v2 = new THREE.Vector3(); - - // 获取平面上的两个相互垂直的方向作为UV坐标系 - let u = new THREE.Vector3(1, 0, 0); - if (Math.abs(normal.dot(u)) > 0.9) { - u = new THREE.Vector3(0, 1, 0); - } - const v = new THREE.Vector3().crossVectors(normal, u).normalize(); - u = new THREE.Vector3().crossVectors(v, normal).normalize(); - - // 计算点在该平面上的UV坐标 - const projected = p.clone().sub(center); - v1.set(projected.dot(u), projected.dot(v), 0); - return v1; - }); - - // 创建一个Shape对象 - const shape = new THREE.Shape(); - - // 移动到第一个点 - shape.moveTo(projectedPoints[0].x, projectedPoints[0].y); - - // 连接所有其他点 - for (let i = 1; i < projectedPoints.length; i++) { - shape.lineTo(projectedPoints[i].x, projectedPoints[i].y); - } - - // 关闭形状 - shape.closePath(); - - // 创建一个面材质 - const faceMaterial = new THREE.MeshBasicMaterial({ - color: colors.visited, - transparent: true, - opacity: 0.3, - side: THREE.DoubleSide, - depthWrite: false - }); - - // 根据中心点确定方向 - const centerPoint = points.reduce((acc, point) => acc.add(point), new THREE.Vector3()).multiplyScalar(1 / points.length); - const direction = centerPoint.clone().normalize(); - - // 根据曲面方向创建网格 - const geometry = new THREE.ShapeGeometry(shape); - const mesh = new THREE.Mesh(geometry, faceMaterial); - - // 放置并旋转网格以匹配多边形位置 - mesh.position.copy(centerPoint); - mesh.lookAt(center); - - // 设置网格属性 - mesh.userData = { - name: regionName, - isVisited: isRegionVisited, - originalColor: colors.visited - }; - mesh.renderOrder = 1; - - // 添加到区域对象 - regionObject.add(mesh); - } catch (error) { - console.error("填充区域时出错:", error); - // 如果填充区域出错,回退到简单的圆盘标记 - const centerPoint = points.reduce((acc, point) => acc.add(point), new THREE.Vector3()).multiplyScalar(1 / points.length); - const diskGeometry = new THREE.CircleGeometry(0.1, 32); - const diskMaterial = new THREE.MeshBasicMaterial({ - color: colors.visited, - transparent: true, - opacity: 0.3, - side: THREE.DoubleSide - }); - - const disk = new THREE.Mesh(diskGeometry, diskMaterial); - disk.position.copy(centerPoint); - disk.lookAt(0, 0, 0); - disk.rotateX(Math.PI / 2); - disk.userData = { name: regionName, isVisited: true }; - disk.renderOrder = 1; - regionObject.add(disk); - } - } + // 保存线条对象引用和对应的国家/地区名称 + allLineObjects.push(line); + lineToCountryMap.set(line, regionName); } }; @@ -446,21 +382,10 @@ const WorldHeatmap: React.FC = ({ visitedPlaces }) => { }); } - // 保存省份边界 - if (regionType === 'province' && boundaries.length > 0) { - provinceBoundaries.set(regionName, boundaries); - } - if (pointCount > 0 && !hasPreDefinedCenter) { - // 如果是国家且有最大多边形 - if (regionType === 'country' && largestPolygonArea > 0) { - centerLon = largestPolygonCenter.lon; - centerLat = largestPolygonCenter.lat; - } else { - // 回退到平均中心点 + // 计算平均中心点 centerLon /= pointCount; centerLat /= pointCount; - } // 将中心点经纬度转换为3D坐标 centerVector = latLongToVector3(centerLat, centerLon, scale + 0.005); @@ -472,6 +397,9 @@ const WorldHeatmap: React.FC = ({ visitedPlaces }) => { } if (pointCount > 0) { + // 保存地区的边界盒 + countryBoundingBoxes.set(regionName, boundingBox); + // 添加地区对象到父组 parent.add(regionObject); countries.set(regionName, regionObject); @@ -512,306 +440,8 @@ const WorldHeatmap: React.FC = ({ visitedPlaces }) => { countryGroup.add(chinaObject); countries.set('中国', chinaObject); - // 创建射线投射器用于鼠标交互 - const raycaster = new THREE.Raycaster(); - const mouse = new THREE.Vector2(); - - // 添加节流函数,限制鼠标移动事件的触发频率 - const throttle = (func: Function, limit: number) => { - let inThrottle: boolean = false; - return function(this: any, ...args: any[]) { - if (!inThrottle) { - func.apply(this, args); - inThrottle = true; - setTimeout(() => inThrottle = false, limit); - } - }; - }; - - // 计算鼠标射线和检测交互的通用函数 - const calculateMouseRay = (event: MouseEvent) => { - if (!containerRef.current || !sceneRef.current) return null; - - // 计算鼠标在画布中的归一化坐标 - const rect = containerRef.current.getBoundingClientRect(); - const x = ((event.clientX - rect.left) / rect.width) * 2 - 1; - const y = -((event.clientY - rect.top) / rect.height) * 2 + 1; - - // 应用坐标,确保值在合理范围内 - mouse.x = Math.max(-1, Math.min(1, x)); - mouse.y = Math.max(-1, Math.min(1, y)); - - // 设置射线投射器的精度 - raycaster.params.Line = { threshold: 0.2 }; - raycaster.setFromCamera(mouse, sceneRef.current.camera); - - return raycaster; - }; - - // 通用函数,查找与射线相交的国家 - const findIntersectedCountry = () => { - if (!sceneRef.current) return null; - - // 首先检测是否与地球相交 - const sphereCenter = new THREE.Vector3(0, 0, 0); - const sphereRadius = 2; // 地球半径 - const sphere = new THREE.Sphere(sphereCenter, sphereRadius); - - // 检测射线与球体是否相交,并获取交点 - const earthIntersectionPoint = new THREE.Vector3(); - const hasEarthIntersection = raycaster.ray.intersectSphere(sphere, earthIntersectionPoint); - - // 如果射线没有与地球相交,返回null - if (!hasEarthIntersection) return null; - - // 计算射线与地球交点的距离 - const distanceToEarthIntersection = earthIntersectionPoint.distanceTo(sceneRef.current.camera.position); - - // 检测与国家组的交叉 - let intersects = raycaster.intersectObject(countryGroup, true); - - // 过滤出只在地球前表面的交点 - const tolerance = 0.1; - intersects = intersects.filter(intersect => { - return intersect.distance <= (distanceToEarthIntersection + tolerance); - }); - - if (intersects.length === 0) return null; - - // 寻找相交的国家 - // 先尝试找到面对象(Mesh类型) - for (const intersect of intersects) { - if (intersect.object instanceof THREE.Mesh && - intersect.object.userData && - intersect.object.userData.name) { - return intersect.object.userData.name; - } - } - - // 如果没有找到面对象,尝试查找线对象 - for (const intersect of intersects) { - let countryObject = intersect.object; - - // 如果对象本身有userData,优先使用 - if (countryObject.userData && countryObject.userData.name) { - return countryObject.userData.name; - } - - // 向上遍历对象层次结构,找到有userData的父对象 - while (countryObject.parent) { - countryObject = countryObject.parent as THREE.Object3D; - if (countryObject.userData && countryObject.userData.name) { - return countryObject.userData.name; - } - // 如果已经到达地球对象,则停止遍历 - if (countryObject === earth) break; - } - } - - return null; - }; - - // 鼠标移动事件 - 使用节流函数包装 - const onMouseMove = throttle((event: MouseEvent) => { - if (!containerRef.current || !sceneRef.current) return; - - // 如果是通过点击选中的国家,不要通过移动鼠标改变它 - if (sceneRef.current.lastClickedCountry !== null) return; - - // 保存最后的鼠标事件,用于相机变化时重新检测 - sceneRef.current.lastMouseEvent = event; - - // 计算射线 - calculateMouseRay(event); - - // 查找相交的国家 - const countryName = findIntersectedCountry(); - - if (countryName) { - setHoveredCountry(countryName); - controls.autoRotate = false; - } else { - setHoveredCountry(null); - controls.autoRotate = true; - } - }, 60); // 60毫秒的节流时间 - - // 添加清除选择的函数 - const clearSelection = () => { - setHoveredCountry(null); - if (sceneRef.current) { - sceneRef.current.lastClickedCountry = null; - } - controls.autoRotate = true; - }; - - // 添加鼠标点击事件处理 - const onClick = (event: MouseEvent) => { - if (!containerRef.current || !sceneRef.current) return; - - // 计算射线 - calculateMouseRay(event); - - // 查找相交的国家 - const countryName = findIntersectedCountry(); - - if (countryName) { - setHoveredCountry(countryName); - controls.autoRotate = false; - sceneRef.current.lastClickedCountry = countryName; - } else { - clearSelection(); - } - }; - - // 添加鼠标双击事件处理 - const onDoubleClick = () => { - clearSelection(); - }; - - containerRef.current.addEventListener('mousemove', onMouseMove); - containerRef.current.addEventListener('click', onClick); - containerRef.current.addEventListener('dblclick', onDoubleClick); - - // 创建一个辅助函数,强制所有球体使用完全不透明的材质 - const enforceOpaqueMaterials = () => { - // 修复地球材质 - if (earth && earth.material) { - const mat = earth.material as THREE.MeshBasicMaterial; - mat.transparent = false; - mat.opacity = 1.0; - mat.depthTest = true; - mat.depthWrite = true; - mat.side = THREE.FrontSide; - mat.needsUpdate = true; - } - - // 修复所有背景球体材质 - [bgSphere, midSphere, innerSphere].forEach(sphere => { - if (sphere && sphere.material instanceof THREE.MeshBasicMaterial) { - sphere.material.transparent = false; - sphere.material.opacity = 1.0; - sphere.material.depthTest = true; - sphere.material.depthWrite = true; - sphere.material.needsUpdate = true; - } - }); - }; - - // 应用一次以确保初始状态正确 - enforceOpaqueMaterials(); - - // 动画循环 - const animate = () => { - if (!sceneRef.current) return; - - // 获取当前帧计数 - const frameCount = sceneRef.current.animationId || 0; - - // 每5帧检查一次材质状态 - if (frameCount % 5 === 0) { - // 使用统一函数更新材质状态 - 仅处理球体 - [bgSphere, midSphere, innerSphere, earth].forEach(sphere => { - if (sphere && sphere.material instanceof THREE.MeshBasicMaterial) { - sphere.material.transparent = false; - sphere.material.opacity = 1.0; - sphere.material.depthTest = true; - sphere.material.depthWrite = true; - sphere.material.needsUpdate = true; - } - }); - } - - // 更新控制器 - sceneRef.current.controls.update(); - - // 相机变化检测 - const cameraPosition = sceneRef.current.camera.position.clone(); - let cameraChanged = false; - - if (sceneRef.current.lastCameraPosition) { - const distance = cameraPosition.distanceTo(sceneRef.current.lastCameraPosition); - cameraChanged = distance > 0.35; - } - - // 只有当相机移动且有最后鼠标事件时,才重新触发鼠标事件 - if (cameraChanged && sceneRef.current.lastMouseEvent && !sceneRef.current.lastClickedCountry) { - onMouseMove(sceneRef.current.lastMouseEvent); - - // 确保已访问区域的边界线始终在未访问区域之上 - sceneRef.current.countries.forEach((object) => { - object.traverse((child) => { - if (child instanceof THREE.Line) { - const childIsVisited = child.userData?.isVisited === true; - if (childIsVisited) { - child.renderOrder = 3; - - if (child.material instanceof THREE.LineBasicMaterial) { - child.material.depthTest = false; - child.material.polygonOffset = true; - child.material.polygonOffsetFactor = -2; - child.material.polygonOffsetUnits = 1; - child.material.needsUpdate = true; - } - } - } - }); - }); - } - - // 降低相机位置保存频率 - if (frameCount % 20 === 0) { - sceneRef.current.lastCameraPosition = cameraPosition.clone(); - } - - // 渲染 - sceneRef.current.renderer.render(scene, camera); - sceneRef.current.labelRenderer.render(scene, camera); - - // 请求下一帧 - sceneRef.current.animationId = requestAnimationFrame(animate); - }; - - // 覆盖渲染器的render方法,确保每次渲染前重设材质状态 - const originalRender = renderer.render; - renderer.render = function(scene, camera) { - // 在渲染前仅对球体对象强制更新材质状态 - [bgSphere, midSphere, innerSphere, earth].forEach(sphere => { - if (sphere && sphere.material instanceof THREE.MeshBasicMaterial) { - sphere.material.transparent = false; - sphere.material.opacity = 1.0; - sphere.material.needsUpdate = true; - } - }); - - // 调用原始渲染方法 - originalRender.call(this, scene, camera); - }; - - // 保存场景引用,添加中间层球体引用 - sceneRef.current = { - scene, - camera, - renderer, - labelRenderer, - controls, - earth, - bgSphere, - countries, - raycaster, - mouse, - animationId: null, - lastCameraPosition: null, - lastMouseEvent: null, - lastClickedCountry: null - }; - // 将视图旋转到中国位置 const positionCameraToFaceChina = () => { - // 中国的中心点经纬度 - const centerLon = 104.195397; - const centerLat = 35.86166; - // 检查是否为小屏幕 const isSmallScreen = containerRef.current && containerRef.current.clientWidth < 640; @@ -819,10 +449,10 @@ const WorldHeatmap: React.FC = ({ visitedPlaces }) => { let fixedPosition; if (isSmallScreen) { // 小屏幕显示距离更远,以便看到更多地球 - fixedPosition = new THREE.Vector3(-2.10, 3.41, -7.5); + fixedPosition = new THREE.Vector3(-2.10, 3.41, -8.0); } else { // 大屏幕使用原来的位置 - fixedPosition = new THREE.Vector3(-2.10, 3.41, -6.08); + fixedPosition = new THREE.Vector3(-2.10, 3.41, -6.5); } // 应用位置 @@ -833,36 +463,318 @@ const WorldHeatmap: React.FC = ({ visitedPlaces }) => { // 禁用自动旋转一段时间 controls.autoRotate = false; - // 保存相机位置 - const chinaCameraInitialPosition = camera.position.clone(); - - // 3秒后恢复旋转 + // 6秒后恢复旋转 setTimeout(() => { if (sceneRef.current) { sceneRef.current.controls.autoRotate = true; } - }, 3000); + }, 6000); // 渲染 renderer.render(scene, camera); labelRenderer.render(scene, camera); - - // 将初始相机位置保存在sceneRef中 - if (sceneRef.current) { - sceneRef.current.lastCameraPosition = chinaCameraInitialPosition; - } }; // 应用初始相机位置 positionCameraToFaceChina(); - // 确保已访问区域的边界线优先显示 - setTimeout(() => { - ensureVisitedBordersOnTop(); - }, 100); // 稍微延迟确保所有元素都已创建完成 + // 创建射线投射器用于鼠标交互 + const raycaster = new THREE.Raycaster(); + const mouse = new THREE.Vector2(); - // 开始动画 - sceneRef.current.animationId = requestAnimationFrame(animate); + // 添加节流函数,限制鼠标移动事件的触发频率 + 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 findNearestCountry = (point: THREE.Vector3): string | null => { + let closestCountry = null; + let minDistance = Infinity; + let smallRegionDistance = Infinity; + let smallRegionCountry = null; + + // 遍历所有国家/地区的边界盒 + for (const [countryName, box] of countryBoundingBoxes.entries()) { + // 计算点到边界盒的距离 + const distance = box.distanceToPoint(point); + + // 估算边界盒大小 + const boxSize = box.getSize(new THREE.Vector3()).length(); + + // 如果点在边界盒内或距离非常近,直接选择该区域 + if (distance < 0.001) { + return countryName; + } + + // 同时跟踪绝对最近的区域 + if (distance < minDistance) { + minDistance = distance; + closestCountry = countryName; + } + + // 对于小区域,使用加权距离 + // 小区域的阈值(较小的边界盒尺寸) + const SMALL_REGION_THRESHOLD = 0.5; + if (boxSize < SMALL_REGION_THRESHOLD) { + // 针对小区域的加权距离(降低小区域的选中难度) + const weightedDistance = distance * (0.5 + boxSize / 2); + if (weightedDistance < smallRegionDistance) { + smallRegionDistance = weightedDistance; + smallRegionCountry = countryName; + } + } + } + + // 小区域优化逻辑 + if (smallRegionCountry && smallRegionDistance < minDistance * 2) { + return smallRegionCountry; + } + + // 处理中国的特殊情况 - 如果点击非常接近省份边界 + if (closestCountry === "中国") { + // 查找最近的中国省份 + let closestProvince = null; + let minProvinceDistance = Infinity; + + // 查找最近的中国省份 + for (const [countryName, box] of countryBoundingBoxes.entries()) { + if (countryName.startsWith("中国-")) { + const distance = box.distanceToPoint(point); + if (distance < minProvinceDistance) { + minProvinceDistance = distance; + closestProvince = countryName; + } + } + } + + if (closestProvince && minProvinceDistance < minDistance * 1.5) { + return closestProvince; + } + } + + return closestCountry; + }; + + // 解决射线检测和球面相交的问题 + const getPointOnSphere = (mouseX: number, mouseY: number, camera: THREE.Camera, radius: number): THREE.Vector3 | 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) { + return earthIntersects[0].point; + } + + // 如果没有直接相交,使用球体辅助检测 + const sphereGeom = new THREE.SphereGeometry(radius, 32, 32); + const sphereMesh = new THREE.Mesh(sphereGeom); + + const intersects = ray.intersectObject(sphereMesh); + if (intersects.length > 0) { + return intersects[0].point; + } + + return null; + }; + + // 简化的鼠标移动事件处理函数 + const onMouseMove = throttle((event: MouseEvent) => { + if (!containerRef.current || !sceneRef.current) return; + + // 获取鼠标在球面上的点 + const spherePoint = 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 (spherePoint) { + const countryName = findNearestCountry(spherePoint); + + if (countryName) { + // 高亮显示该国家/地区的线条 + allLineObjects.forEach(line => { + if (lineToCountryMap.get(line) === countryName && line.material instanceof THREE.LineBasicMaterial) { + line.material.color.set(line.userData.highlightColor); + } + }); + + // 更新悬停国家 + if (countryName !== hoveredCountry) { + setHoveredCountry(countryName); + } + + // 禁用自动旋转 + controls.autoRotate = false; + } else { + // 如果没有找到国家/地区,清除悬停状态 + if (hoveredCountry) { + setHoveredCountry(null); + controls.autoRotate = true; + } + } + } else { + // 如果没有找到球面点,清除悬停状态 + if (hoveredCountry) { + setHoveredCountry(null); + controls.autoRotate = true; + } + } + + // 保存鼠标事件和位置 + 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) return; + + // 获取鼠标在球面上的点 + const spherePoint = getPointOnSphere(event.clientX, event.clientY, camera, 2.01); + + // 如果找到点,寻找最近的国家/地区 + if (spherePoint) { + const countryName = findNearestCountry(spherePoint); + + if (countryName) { + // 重置所有线条颜色 + allLineObjects.forEach(line => { + if (line.material instanceof THREE.LineBasicMaterial) { + line.material.color.set(line.userData.originalColor); + } + }); + + // 高亮显示该国家/地区的线条 + allLineObjects.forEach(line => { + if (lineToCountryMap.get(line) === countryName && line.material instanceof THREE.LineBasicMaterial) { + line.material.color.set(line.userData.highlightColor); + } + }); + + // 更新选中国家 + setHoveredCountry(countryName); + sceneRef.current.lastClickedCountry = countryName; + controls.autoRotate = false; + } else { + // 如果没有找到国家/地区,清除选择 + clearSelection(); + } + } 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); + containerRef.current.addEventListener('click', onClick); + containerRef.current.addEventListener('dblclick', onDoubleClick); + + // 简化的动画循环函数 + 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, + regionImportance: undefined, + importanceThreshold: undefined + }; // 处理窗口大小变化 const handleResize = () => { @@ -877,124 +789,17 @@ const WorldHeatmap: React.FC = ({ visitedPlaces }) => { renderer.setSize(width, height); labelRenderer.setSize(width, height); - // 在调整大小后立即渲染一次,以防止闪烁或偏移 + // 立即渲染一次 renderer.render(sceneRef.current.scene, camera); labelRenderer.render(sceneRef.current.scene, camera); - - // 确保已访问区域的边界线总是显示在顶层 - ensureVisitedBordersOnTop(); }; window.addEventListener('resize', handleResize); - // 监听暗色模式变化 - const darkModeObserver = new MutationObserver(() => { - if (!sceneRef.current) return; - - const isDark = document.documentElement.classList.contains('dark'); - const newColors = getColors(); - - // 保持场景背景透明 - sceneRef.current.scene.background = null; - - // 设置透明背景 - sceneRef.current.renderer.setClearColor(0x000000, 0); - - // 更新材质颜色函数 - const updateMeshColor = (obj: THREE.Object3D, color: string) => { - if (obj instanceof THREE.Mesh && obj.material instanceof THREE.MeshBasicMaterial) { - obj.material.color.set(color); - obj.material.transparent = false; - obj.material.opacity = 1.0; - obj.material.needsUpdate = true; - } - }; - - // 更新背景相关球体颜色 - const bgColor = newColors.bgSphere; - - // 直接更新三个球体对象,而不是遍历整个场景 - [bgSphere, midSphere, innerSphere].forEach(sphere => { - if (sphere && sphere.material instanceof THREE.MeshBasicMaterial) { - updateMeshColor(sphere, bgColor); - } - }); - - // 更新地球颜色 - if (sceneRef.current.earth && sceneRef.current.earth.material) { - const earthMat = sceneRef.current.earth.material as THREE.MeshBasicMaterial; - earthMat.color.set(new THREE.Color(newColors.earthBase)); - earthMat.side = THREE.DoubleSide; - earthMat.needsUpdate = true; - } - - // 更新国家颜色 - sceneRef.current.countries.forEach((object, name) => { - const isVisited = visitedPlaces.includes(name); - - object.traverse((child) => { - if (child instanceof THREE.Line) { - const childIsVisited = child.userData?.isVisited === true; - - let color; - if (name.startsWith('中国-')) { - color = childIsVisited ? newColors.visitedBorder : newColors.chinaBorder; - } else if (name === '中国') { - color = newColors.chinaBorder; - } else { - color = isVisited ? newColors.visitedBorder : newColors.border; - } - - if (child.material instanceof THREE.LineBasicMaterial) { - child.material.color.set(new THREE.Color(color)); - child.userData.originalColor = color; - } - } - }); - }); - - // 暗色模式变化后重新应用渲染优先级 - setTimeout(() => { - ensureVisitedBordersOnTop(); - }, 50); - }); - - darkModeObserver.observe(document.documentElement, { attributes: true }); - - // 添加一个函数来确保已访问区域的边界线总是显示在顶层 - const ensureVisitedBordersOnTop = () => { - if (!sceneRef.current) return; - - // 优化边界线的渲染优先级和材质属性 - const updateLineMaterial = (child: THREE.Object3D) => { - if (!(child instanceof THREE.Line)) return; - - const childIsVisited = child.userData?.isVisited === true; - child.renderOrder = childIsVisited ? 3 : 2; - - if (child.material instanceof THREE.LineBasicMaterial) { - child.material.depthTest = false; - - if (childIsVisited) { - child.material.polygonOffset = true; - child.material.polygonOffsetFactor = -2; - child.material.polygonOffsetUnits = 1; - } - - child.material.needsUpdate = true; - } - }; - - // 遍历所有国家对象 - sceneRef.current.countries.forEach(object => { - object.traverse(updateLineMaterial); - }); - - // 立即渲染一次以应用更改 - sceneRef.current.renderer.render(sceneRef.current.scene, sceneRef.current.camera); - sceneRef.current.labelRenderer.render(sceneRef.current.scene, sceneRef.current.camera); - }; + // 开始动画 + sceneRef.current.animationId = requestAnimationFrame(animate); + // 清理函数 return () => { // 清理资源和事件监听器 if (sceneRef.current) { @@ -1028,11 +833,8 @@ const WorldHeatmap: React.FC = ({ visitedPlaces }) => { // 移除窗口事件监听器 window.removeEventListener('resize', handleResize); - - // 移除观察器 - darkModeObserver.disconnect(); }; - }, [visitedPlaces]); + }, [visitedPlaces, theme]); // 依赖于visitedPlaces和theme变化 return (
@@ -1041,12 +843,22 @@ const WorldHeatmap: React.FC = ({ visitedPlaces }) => { className="w-full h-[400px] sm:h-[450px] md:h-[500px] lg:h-[600px] xl:h-[700px]" /> {hoveredCountry && ( -
-
-

+

+
+

{hoveredCountry} - {hoveredCountry && visitedPlaces.includes(hoveredCountry) ? - ' ✓ 已去过' : ' 尚未去过'} + {hoveredCountry && visitedPlaces.includes(hoveredCountry) ? ( + + + + + 已去过 + + ) : ( + + 尚未去过 + + )}

diff --git a/src/content/web/echoes博客使用说明.md b/src/content/web/echoes博客使用说明.md index a194a20..ed3f690 100644 --- a/src/content/web/echoes博客使用说明.md +++ b/src/content/web/echoes博客使用说明.md @@ -14,6 +14,7 @@ tags: [] 4. **项目展示**:支持展示 GitHub、Gitea 和 Gitee 的项目 5. **观影记录**:集成豆瓣观影数据 6. **读书记录**:集成豆瓣读书数据 +7. **旅行足迹**:支持展示全球旅行足迹热力图 ## 基础配置 @@ -42,6 +43,9 @@ export const PSB_ICP_URL = '备案链接'; // 豆瓣配置 export const DOUBAN_ID = '你的豆瓣ID'; + +// 旅行足迹 +export const VISITED_PLACES = ['中国-北京', '中国-上海', '美国-纽约']; ``` ## 文章写作 @@ -110,23 +114,17 @@ tags: ["标签1", "标签2"] ```astro --- import GitProjectCollection from '@/components/GitProjectCollection'; -import { GitPlatform, type GitConfig } from '@/components/GitProjectCollection'; - -// Gitea 配置示例 -const giteaConfig: GitConfig = { - username: 'your-username', // 必填:用户名 - token: 'your-token', // 可选:访问令牌,用于访问私有仓库 - perPage: 10, // 可选:每页显示数量,默认 10 - url: 'your-git-url' // Gitea 必填,GitHub/Gitee 无需填写 -}; +import { GitPlatform } from '@/components/GitProjectCollection'; --- ``` @@ -158,6 +156,53 @@ import MediaGrid from '@/components/MediaGrid.astro'; /> ``` +## 旅行足迹 + +### WorldHeatmap 组件 + +`WorldHeatmap` 组件用于展示你去过的地方,以热力图的形式在世界地图上显示。 + +#### 基本用法 + +在 `src/consts.ts` 中配置你去过的地方: + +```typescript +// 配置你去过的地方 +export const VISITED_PLACES = [ + // 国内地区格式:'中国-省份/城市' + '中国-黑龙江', + '中国-北京', + '中国-上海', + // 国外地区直接使用国家名 + '马来西亚', + '泰国', + '美国' +]; +``` + +然后在页面中使用: + +```astro +--- +import Layout from "@/components/Layout.astro"; +import WorldHeatmap from '@/components/WorldHeatmap'; +import { VISITED_PLACES } from '@/consts'; +--- + + +
+

我的旅行足迹

+
+ +
+
+
+``` + + ## 主题切换 系统支持三种主题模式: diff --git a/src/pages/projects.astro b/src/pages/projects.astro index 0886427..31791e0 100644 --- a/src/pages/projects.astro +++ b/src/pages/projects.astro @@ -1,12 +1,7 @@ --- import Layout from '@/components/Layout.astro'; import GitProjectCollection from '@/components/GitProjectCollection'; -import { GitPlatform, type GitConfig } from '@/components/GitProjectCollection'; - -const giteaConfig: GitConfig = { - username: 'lsy', - url: 'https://g.lsy22.com', -}; +import { GitPlatform } from '@/components/GitProjectCollection'; --- @@ -16,7 +11,7 @@ const giteaConfig: GitConfig = { platform={GitPlatform.GITEA} username="lsy" title="Gitea 个人项目" - config={giteaConfig} + url="https://g.lsy22.com" client:load />