/** * ECharts 地图足迹模块 (支持钻取功能 - 点击省份查看下一级, 点击按钮返回) */ const MapFootprintModule = (() => { let myChart = null; // ECharts 实例 let currentMapLevel = "country"; // 当前地图层级 ('country', 'province') let currentProvinceCode = null; // 当前显示的省份编码 (如果有的话) // --- 配置 --- const COUNTRY_MAP_NAME = "china_custom"; const COUNTRY_GEOJSON_PATH = "./static/js/echarts/china.geojson"; const PROVINCE_GEOJSON_BASE_PATH = "./static/js/echarts/province/"; // 假设省份 GeoJSON 存放在这个目录 // --- 数据 --- // 注意:这里的坐标可能需要根据你实际访问的城市进行微调 const visitedPlaces = [ { name: "济南市", province: "山东省", value: [117.024961, 36.682788], desc:'正在这里努力打工!', imgList: [ "./static/img/location/jinan/1.jpg", "./static/img/location/jinan/2.jpg", "./static/img/location/jinan/3.jpg", "./static/img/location/jinan/4.jpg", ], }, { name: "青岛市", value: [120.384423, 36.065918], province: "山东省" , desc:'这是我人生第一次看见海,我永远不会忘记。同样的还有你们。', imgList: [ "./static/img/location/qingdao/1.jpg", "./static/img/location/qingdao/2.jpg", "./static/img/location/qingdao/3.jpg", ], }, { name: "淄博市", desc:'也算是吃上淄博烧烤了。', value: [118.055915, 36.813547], province: "山东省" }, { name: "潍坊市", desc:'风筝节!不错不错!', value: [119.162775, 36.705759], province: "山东省" }, { name: "保定市", desc:'大学四年,那是最自由的时候。', value: [115.482331, 38.867657], province: "河北省" }, { name: "衡水市", desc: '生我养我的地方,也是埋葬我的地方', value: [115.665996, 37.739574], province: "河北省" }, { name: "沧州市", desc: 'hei !你一定要幸福啊!带着我们几个的祝福,走下去啊!', value: [116.838581, 38.308094], province: "河北省" }, { name: "廊坊市", value: [116.704374, 39.523949], province: "河北省" , desc:'五一寻友,一起夜间烧烤,宿醉到天明', imgList: [ "./static/img/location/langfang/1.jpg", "./static/img/location/langfang/2.jpg", "./static/img/location/langfang/3.jpg", ], }, { name: "石家庄市", desc: '我曾经来过。', value: [114.514891, 38.042309], province: "河北省" }, { name: "张家口市", desc: '三个准大学生,傻傻的做七八个小时的绿皮火车来到这里,只为某人一份未知的爱情。', value: [114.886714, 40.811943], province: "河北省" }, { name: "洛阳市", province: "河南省", value: [112.454174, 34.618139], desc:'群山巍峨,银装素裹,不虚此行', imgList: [ "./static/img/location/luoyang/1.jpg", "./static/img/location/luoyang/2.jpg", "./static/img/location/luoyang/3.jpg", ], }, { name: "昌平区", value: [116.235904, 40.218086], province: "北京市" }, { name: "杭州市", desc: '细雨朦胧的西湖才是最美的。', value: [120.153576, 30.287459], province: "浙江省" }, { name: "苏州市", value: [120.585316, 31.298886], province: "江苏省" }, { name: "扬州市", value: [119.421003, 32.393159], province: "江苏省" }, { name: "镇江市", value: [119.452753, 32.204402], province: "江苏省" }, { name: "南京市", value: [118.796877, 32.060255], province: "江苏省" }, { name: "无锡市", value: [120.311987, 31.49092], province: "江苏省" }, ]; // 省份名称到其 GeoJSON 文件名的映射 (需要根据你的文件名调整) const provinceNameToGeoFile = { 北京市: "北京市.geojson", 山东省: "山东省.geojson", 河北省: "河北省.geojson", 河南省: "河南省.geojson", 浙江省: "浙江省.geojson", 江苏省: "江苏省.geojson", }; // --- 工具函数 --- /** * 根据省份中文名获取其对应的 GeoJSON 文件完整路径 * @param {string} provinceName - 省份中文名 * @returns {string|null} - 文件路径或 null */ const getProvinceGeoPath = (provinceName) => { const fileName = provinceNameToGeoFile[provinceName]; if (fileName) { return ` ${PROVINCE_GEOJSON_BASE_PATH}${fileName}`; } console.warn(`未找到省份 " ${provinceName}" 对应的 GeoJSON 文件。`); return null; }; /** * 根据地图层级和可选的省份编码生成 ECharts 配置 * @param {string} level - 地图层级 ('country' or 'province') * @param {string} [provinceCode] - 省份编码 (当 level='province' 时使用) * @returns {Object} - ECharts 配置对象 */ const getEChartsOption = (level, provinceCode = null) => { let geoMapName, roamSetting, zoomLevel, centerCoord, titleText; let filteredVisitedPlaces = []; // 用于过滤足迹点 let seriesConfig = []; // 存储所有 series let visualMapConfig = null; // 存储 visualMap 配置 (仅省份需要) if (level === "country") { geoMapName = COUNTRY_MAP_NAME; roamSetting = false; // 启用缩放和平移 zoomLevel = 1.1; centerCoord = [104.06, 32]; // 中国中心 titleText = ""; filteredVisitedPlaces = visitedPlaces; // 显示所有足迹 // 国家地图:保持原有的 geo 和 scatter 配置 seriesConfig = [ { name: "足迹", type: "scatter", coordinateSystem: "geo", data: filteredVisitedPlaces, symbol: "pin", symbolSize: 18, itemStyle: { color: "#FF6B6B", borderColor: "#fff", borderWidth: 1, }, emphasis: { itemStyle: { color: "#dd4444", }, }, tooltip: { // --- 核心修改:Formatter 函数 --- formatter: function (params) { // 确保 params.data 存在 if (!params.data) { return params.name || "未知地点"; // 如果没有数据,至少显示名称或默认文字 } // 获取地点名称和坐标 const name = params.data.name || "未知地点"; const longitude = params.data.value && params.data.value[0] ? params.data.value[0].toFixed(4) : "N/A"; const latitude = params.data.value && params.data.value[1] ? params.data.value[1].toFixed(4) : "N/A"; // 初始化 tooltip 内容字符串 let tooltipHtml = `${name}
坐标: [${longitude}, ${latitude}]
`; // 添加一些换行和分割 // 如果还需要显示其他描述信息(例如可以从 params.data 中获取),可以在此处添加 const description = params.data.desc || "博主也不知道说啥了。。。"; if (description) { tooltipHtml += `描述: ${description}
`; } // 获取图片列表 const imageList = params.data.imgList; // 直接从数据对象获取 imgList // 检查是否有图片列表且不为空 if (Array.isArray(imageList) && imageList.length > 0) { // 添加标题 tooltipHtml += "照片:
"; // 遍历图片列表,生成 HTML 图像标签 imageList.forEach((imgSrc) => { tooltipHtml += `${name} 图片`; }); } else { // 如果没有图片或 imgList 不存在/为空,则显示提示 tooltipHtml += "照片: 看来博主不是很爱拍照~"; } return tooltipHtml; // 返回构建好的 HTML 字符串 }, }, }, ]; } else if (level === "province" && provinceCode) { geoMapName = `province_ ${provinceCode}`; // 为省份地图创建唯一名称 roamSetting = false; // 省份地图也启用缩放平移 zoomLevel = 1.0; // 可以根据需要调整 // 可以根据省份动态设置中心点,这里暂时固定 // centerCoord = [118.5, 36.5]; // 示例:山东中心 // 从 visitedPlaces 中找一个城市作为中心点,如果没有,则使用默认 const firstPlaceInProvince = visitedPlaces.find( (place) => place.province === provinceCode, ); centerCoord = firstPlaceInProvince ? firstPlaceInProvince.value : [118.5, 36.5]; if (provinceCode == "山东省") { centerCoord = [118.5, 36.5]; } else if (provinceCode == "河北省") { centerCoord = [115.482331, 40]; } else if (provinceCode == "浙江省") { centerCoord = [120.153576, 29.4]; } else if (provinceCode == "江苏省") { centerCoord = [119, 32.9]; } else if (provinceCode == "河南省") { centerCoord = [114, 34]; } else if (provinceCode == "北京市") { centerCoord = [116.407395, 40.3]; } titleText = ` ${provinceCode} - “印迹”`; // 过滤出属于当前省份的足迹点 filteredVisitedPlaces = visitedPlaces.filter( (place) => place.province === provinceCode, ); // 构造 map 系列的数据,访问过的城市 value 设为 1,未访问的可以设为 0 // 为了实现高亮,我们只需要列出访问过的城市即可,未访问的城市会使用默认颜色 const provinceMapData = filteredVisitedPlaces.map((city) => ({ name: city.name, desc: city.desc || "", imgList: city.imgList || [], value: 1, // 值为 1 表示访问过,用于 visualMap 区分 })); // --- 配置 visualMap --- visualMapConfig = { show: false, // 通常不显示 visualMap 组件条 min: 0, // 最小值 max: 1, // 最大值 inRange: { color: ["#FF6B6B"], // 高亮颜色 (访问过的城市) }, calculable: true, }; // --- 创建省份地图系列 (用于高亮城市) --- const provinceMapSeries = { // name: "访问过的城市", type: "map", map: geoMapName, // 指向当前省份的 GeoJSON roam: roamSetting, zoom: zoomLevel, center: centerCoord, silent: false, // 设置为 false,允许响应鼠标事件以显示 tooltip label: { show: true, // 可以选择是否显示城市名称标签 color: "#000", // 标签颜色 fontSize: 10, }, emphasis: { label: { // 悬停时标签效果 show: true, fontWeight: "bold", }, itemStyle: { // 悬停时区域效果 opacity: 0.8, // 降低透明度 // areaColor: '#ffcccc', // 也可以设置悬停颜色,但会受 visualMap 影响 }, }, // 数据驱动颜色 data: provinceMapData, // 默认样式(未在 data 中定义的区域将使用此样式) itemStyle: { areaColor: "#eef2ff", // 未访问区域的默认颜色 borderColor: "#333", borderWidth: 0.5, }, // 添加 tooltip 配置到 series 级别 tooltip: { trigger: 'item', formatter: function(params) { // 检查是否有数据 if (!params.data) { return params.name || "未知城市"; } const cityName = params.data.name || params.name || "未知城市"; let tooltipHtml = `
${cityName}
`; // 如果这个城市在 visitedPlaces 中有记录 const placeData = visitedPlaces.find(p => p.name === cityName && p.province === provinceCode); if (placeData) { // 显示描述 const description = placeData.desc || "暂无描述"; tooltipHtml += `
${description}
`; // 显示图片 if (placeData.imgList && placeData.imgList.length > 0) { tooltipHtml += `
`; placeData.imgList.forEach((imgSrc) => { tooltipHtml += ` ${cityName} `; }); tooltipHtml += `
`; } } return tooltipHtml; } } }; // 将省份地图系列和 geo 组件添加到配置中 seriesConfig = [provinceMapSeries]; // geo 配置也需要包含省份 geo // 注意:在返回全国地图时,需要重新设置 geo 为国家地图 // 这个 geo 组件会覆盖全局的 geo 配置 // 我们需要将 geo 配置移动到外面处理 } else { console.error("无效的地图层级或省份代码"); return {}; // 返回空配置 } // 构建最终的 option const option = { title: { text: titleText, subtext: "读万卷书,不如行万里路。见多才会识广~", left: "center", textStyle: { fontSize: 18, }, subtextStyle: { fontSize: 12, }, }, tooltip: { trigger: 'item', backgroundColor: 'rgba(255, 255, 255, 0.95)', borderColor: '#ddd', borderWidth: 1, textStyle: { color: '#333' }, extraCssText: 'box-shadow: 0 2px 8px rgba(0,0,0,0.15); border-radius: 6px;' }, // geo 组件配置 geo: level === "country" ? { map: geoMapName, roam: roamSetting, zoom: zoomLevel, center: centerCoord, emphasis: { itemStyle: { areaColor: "#f0f0f0", // geo 强调样式,当没有 series 时生效 borderColor: "#409EFF", borderWidth: 1, }, }, itemStyle: { areaColor: "#eef2ff", // geo 默认样式,当没有 series 时生效 borderColor: "#333", }, } : { // 省份地图的 geo 配置 id: "province_geo_for_clicks", // 给 geo 组件一个 ID 便于识别 map: geoMapName, roam: roamSetting, zoom: zoomLevel, center: centerCoord, silent: false, // 设置为 false,允许 geo 组件响应点击 // 隐藏 geo 组件的视觉表现,因为高亮由 series 控制 itemStyle: { areaColor: "transparent", // 透明填充 borderColor: "transparent", // 透明边框 }, emphasis: { itemStyle: { areaColor: "rgba(64, 158, 255, 0.2)", // 可选:点击时的视觉反馈 borderColor: "transparent", }, }, }, series: seriesConfig, // 使用构建好的 series 配置 }; // 如果当前是省份地图,添加 visualMap 配置 if (visualMapConfig) { option.visualMap = visualMapConfig; } return option; }; /** * 加载并注册指定路径的 GeoJSON 地图数据 * @param {string} path - GeoJSON 文件路径 * @param {string} mapName - 注册到 ECharts 的地图名称 * @returns {Promise} - 成功或失败 */ const loadAndRegisterSingleMap = async (path, mapName) => { try { console.log(`正在加载 GeoJSON: ${path}`); const response = await fetch(path); if (!response.ok) { throw new Error( `加载 GeoJSON 失败: ${response.status} ${response.statusText}`, ); } const geoJsonData = await response.json(); // 注册地图 echarts.registerMap(mapName, geoJsonData); console.log(`GeoJSON 地图数据 ${mapName} 加载并注册成功。`); return true; } catch (error) { console.error("加载或注册 GeoJSON 地图时出错:", error); return false; } }; /** * 切换到指定的地图层级和区域 * @param {string} targetLevel - 目标层级 ('country' or 'province') * @param {string} [targetProvinceCode] - 目标省份编码 (当 targetLevel='province' 时使用) */ const switchMap = async (targetLevel, targetProvinceCode = null) => { if (!myChart) { console.error("ECharts 实例不存在,无法切换地图。"); return; } let mapNameToUse, geoPathToLoad; if (targetLevel === "country") { mapNameToUse = COUNTRY_MAP_NAME; geoPathToLoad = COUNTRY_GEOJSON_PATH; currentMapLevel = "country"; currentProvinceCode = null; } else if (targetLevel === "province" && targetProvinceCode) { mapNameToUse = `province_ ${targetProvinceCode}`; const path = getProvinceGeoPath(targetProvinceCode); if (!path) { console.error( `无法切换到省份 ${targetProvinceCode}: 找不到对应的 GeoJSON 文件。`, ); return; } geoPathToLoad = path; currentMapLevel = "province"; currentProvinceCode = targetProvinceCode; } else { console.error("无效的目标层级或省份代码。"); return; } // 尝试加载并注册目标地图数据 const loadSuccess = await loadAndRegisterSingleMap( geoPathToLoad, mapNameToUse, ); if (!loadSuccess) { console.error(`切换地图失败:无法加载 ${geoPathToLoad}`); return; } // 生成新配置并应用 const newOption = getEChartsOption(targetLevel, targetProvinceCode); myChart.setOption(newOption, true); // 使用 notMerge=true 确保完全替换 // 更新返回按钮的可见性 updateBackButtonVisibility(); }; // --- 新增函数:管理返回按钮 --- /** * 获取返回按钮的DOM元素 * @returns {HTMLElement|null} */ const getBackButtonElement = () => { // 假设按钮ID是固定的,可以根据需要调整 return document.getElementById("map-back-button"); }; /** * 创建返回按钮并将其添加到图表容器中 * @param {HTMLElement} chartContainer - ECharts 图表容器 */ const createBackButton = (chartContainer) => { const button = document.createElement("button"); button.id = "map-back-button"; button.textContent = "⬅返回全国地图"; button.style.position = "absolute"; button.style.top = "50px"; button.style.left = "10px"; button.style.zIndex = 1000; // 确保按钮在图表之上 button.style.padding = "2px 5px"; // 减少内边距以更接近纯文本 button.style.fontSize = "14px"; // 根据需要调整字体大小 button.style.cursor = "pointer"; button.style.border = "none"; // 去掉边框 button.style.backgroundColor = "transparent"; // 设置背景透明 button.style.display = "none"; // 初始隐藏 button.onclick = () => { console.log("点击了返回按钮。"); switchMap("country"); }; chartContainer.appendChild(button); }; /** * 根据当前地图层级更新返回按钮的可见性 */ const updateBackButtonVisibility = () => { const button = getBackButtonElement(); if (!button) { console.warn("返回按钮 DOM 元素未找到,无法更新其可见性。"); return; } // 当前在省份地图时显示按钮,否则隐藏 button.style.display = currentMapLevel === "province" ? "block" : "none"; }; // --- /新增函数 --- /** * 初始化 ECharts 地图 (初始加载国家地图) * @param {HTMLElement} container - 图表容器DOM元素 */ const initEChartsMap = async (container) => { if (!container) { console.error("地图容器未找到"); return; } // 初始化 ECharts 实例 myChart = echarts.init(container); // 创建并添加返回按钮 createBackButton(container); // 首先加载并注册国家地图 const countryLoadSuccess = await loadAndRegisterSingleMap( COUNTRY_GEOJSON_PATH, COUNTRY_MAP_NAME, ); if (!countryLoadSuccess) { console.error("初始化失败:无法加载国家 GeoJSON。"); return; // 或者显示错误信息给用户 } // 应用初始配置 (国家地图) const initialOption = getEChartsOption("country"); myChart.setOption(initialOption); // --- 添加事件监听 --- // 监听 geo 组件的点击事件 // 关键修改:现在 geo 组件在省份地图时也存在,并且不静默 myChart.on("click", "geo", function (params) { console.log("Geo component clicked:", params.componentId, params.name); // Debug log if (currentMapLevel === "country") { // 当前是国家地图,点击省份则进入 const clickedProvinceName = params.name; console.log(`点击了省份: ${clickedProvinceName}`); // 尝试获取省份对应的 GeoJSON 文件名 const fileName = Object.keys(provinceNameToGeoFile).find( (key) => key === clickedProvinceName, ); console.log(fileName); if (fileName) { const provinceCode = fileName; // 这里假设文件名就是省份简称 // console.log(`即将切换到省份: ${provinceCode}`); switchMap("province", provinceCode); } else { showModal(); console.log(`未配置省份 " ${clickedProvinceName}" 的详细地图。`); } } else if ( currentMapLevel === "province" && params.componentId === "province_geo_for_clicks" ) { // 当前是省份地图,且点击的是省份 geo 组件,则返回全国地图 console.log("点击省份 geo 组件,返回全国地图"); switchMap("country"); } // 如果在省份地图上点击,但不是省份 geo 组件(理论上不会发生,因为 series 是 silent 的),可以有其他逻辑,比如不处理 }); // 监听窗口大小变化,自动适配 window.addEventListener("resize", function () { if (myChart) { myChart.resize(); } }); }; /** * 获取地图容器ID * @returns {string} */ const getMapContainerId = () => { return "echarts-map-container"; }; /** * 渲染 ECharts 地图内容到指定容器 * @param {HTMLElement} contentDiv - 标签页的内容容器 */ const renderMap = (contentDiv) => { // 清空内容区域 // contentDiv.innerHTML = ''; // 创建地图容器 const mapDiv = document.createElement("div"); mapDiv.id = getMapContainerId(); mapDiv.style.width = "100%"; mapDiv.style.height = "90vh"; mapDiv.style.marginTop = "20px"; mapDiv.style.position = "relative"; contentDiv.appendChild(mapDiv); // 加载 GeoJSON 并初始化地图 setTimeout(async () => { const container = document.getElementById(getMapContainerId()); if (container) { await initEChartsMap(container); // 使用 await 确保初始化完成 } else { console.error("地图容器 DOM 元素未找到,无法初始化图表。"); } }, 100); }; /** * 销毁 ECharts 实例 (可选,用于性能优化或切换时清理) */ const destroyMap = () => { if (myChart) { myChart.dispose(); myChart = null; console.log("ECharts 地图已销毁"); // 可以考虑移除返回按钮 const button = getBackButtonElement(); if (button && button.parentNode) { button.parentNode.removeChild(button); } } }; // 返回公共方法 return { renderMap, destroyMap, }; })(); function showModal() { document.getElementById("modalOverlay").style.display = "block"; } const closeModal = () => { document.getElementById("modalOverlay").style.display = "none"; }; // --- 页面加载完成后执行 --- document.addEventListener("DOMContentLoaded", function () { // 获取遮罩层和内容区域元素 const modalOverlay = document.getElementById("modalOverlay"); const modalContent = document.getElementById("modalContent"); // 定义点击遮罩层的处理函数 function handleOverlayClick(event) { // 检查点击的目标是否是遮罩层本身(而不是它的子元素,比如里面的 p 或 button) if (event.target === modalOverlay) { closeModal(); // 如果是,就关闭弹窗 } } // 为遮罩层添加点击事件监听器 modalOverlay.addEventListener("click", handleOverlayClick); // --- 示例:如何显示弹窗 (供参考) --- // window.showModal = function() { // modalOverlay.style.display = 'block'; // // 注意:如果每次都显示,事件监听器只需要添加一次,或者在这里检查是否已存在 // } });