 const chart = echarts.init(document.getElementById('map'));

        // API 基础地址 - 使用相对路径,自动适配当前域名和端口
        const API_BASE_URL = '/api';

        // ====================================================
        // 设备判定 + 交互模式状态机(CLICK / HOVER 严格分离)
        // ====================================================
        const isMobileDevice = window.innerWidth <= 768;
        const INTERACTION_MODE = Object.freeze({
            CLICK: 'click',   // 点击国家才显示 tooltip(默认)
            HOVER: 'hover',   // 鼠标悬停 ~500ms 后显示 tooltip
        });
        const STORAGE_KEY_HOVER = 'worldmap.hoverMode';

        // 一次性迁移:老代码用的是 'worldmap.hoverEnabled',为避免老用户残留的 HOVER 偏好导致默认不是 CLICK,
        // 主动删除老 key(老用户重启后默认 CLICK,需要 toggle HOVER 可重设)
        try {
            localStorage.removeItem('worldmap.hoverEnabled');
        } catch (e) { /* localStorage 不可用时忽略 */ }

        const HOVER_DELAY_MS = 500;            // hover 模式防误触延迟
        const TOOLTIP_HIDE_DELAY_MS = 600;     // 鼠标移出后多久关闭 tooltip

        // 全局唯一的 tooltip 状态机(两种模式共用)
        const interaction = {
            // 当前模式
            mode: isMobileDevice
                ? INTERACTION_MODE.CLICK
                : (localStorage.getItem(STORAGE_KEY_HOVER) === 'true'
                    ? INTERACTION_MODE.HOVER
                    : INTERACTION_MODE.CLICK),
            // CLICK 模式专用:锁定的国家(空白点击解除);HOVER 模式始终 null
            lockedIsoCode: null,
            // HOVER 模式专用:防误触延时定时器
            hoverTimer: null,
            // 通用:tooltip 延迟关闭定时器
            hideTimer: null,
            // formatter 闭包依赖的当前 tooltip 数据
            tooltipState: null,
        };

        // ====================================================
        // 搜索功能(中英模糊搜索 + 跳转 + 键盘导航)
        // ====================================================
        const searchState = {
            // 搜索数据集:[{ iso, name, name_en, diameter, centroid }]
            countries: [],
            // 当前建议列表(用于键盘导航)
            suggestions: [],
            activeIndex: -1,
            panelOpen: false,
        };

        // 1) 从地图 features 构建搜索数据(带 ISO、中英文名、直径、质心)
        //    质心实时计算:几何中心(所有坐标点算术平均)
        function buildSearchIndex(worldJson) {
            // 内部计算质心(不能直接复用 then 闭包里的 calculateCentroid)
            function calcCentroid(feature) {
                const coords = feature.geometry.coordinates;
                let sumX = 0, sumY = 0, count = 0;
                function walk(c) {
                    if (Array.isArray(c[0])) c.forEach(walk);
                    else { sumX += c[0]; sumY += c[1]; count++; }
                }
                walk(coords);
                return count > 0 ? [sumX / count, sumY / count] : [0, 0];
            }

            return worldJson.features.map(f => {
                const iso = f.id;
                return {
                    iso,
                    name: f.properties.name,           // 中文
                    name_en: f.properties.name_en,     // 英文
                    diameter: f.properties.diameter || 10,
                    centroid: calcCentroid(f),
                };
            });
        }

        // 2) 转义正则特殊字符
        function escapeRegExp(s) {
            return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
        }

        // 3) 高亮匹配文本(子序列逐字高亮,配合模糊搜索)
        function highlight(text, query) {
            if (!query) return escapeHtml(text);
            const q = query.toLowerCase();
            const t = text;
            const tl = t.toLowerCase();
            let qi = 0;
            let out = '';
            for (let ti = 0; ti < t.length; ti++) {
                if (qi < q.length && tl[ti] === q[qi]) {
                    out += `<mark>${escapeHtml(t[ti])}</mark>`;
                    qi++;
                } else {
                    out += escapeHtml(t[ti]);
                }
            }
            return out;
        }

        // 防止 XSS 转义
        function escapeHtml(s) {
            return String(s)
                .replace(/&/g, '&amp;')
                .replace(/</g, '&lt;')
                .replace(/>/g, '&gt;')
                .replace(/"/g, '&quot;');
        }

        // 4) 模糊搜索:查询的字符按顺序在被搜索文本里依次匹配(不要求连续)
        //    例:查 "中国" 能匹配 "中华人民共和国" (先中后国,间隔 5 个字)
        //    例:查 "franc" 能匹配 "France"
        function isSubsequenceMatch(query, text) {
            if (!query) return true;
            let qi = 0;
            for (let ti = 0; ti < text.length && qi < query.length; ti++) {
                if (text[ti] === query[qi]) qi++;
            }
            return qi === query.length;
        }

        // 计算单字段匹配得分(越高越好,0 表示不匹配)
        function scoreField(query, field) {
            const q = query;
            const f = field;

            // 1. 精确匹配
            if (f === q) return 100;
            // 2. 开头匹配
            if (f.startsWith(q)) return 80;
            // 3. 连续包含
            if (f.includes(q)) return 60;
            // 4. 子序列模糊匹配(查查询字按顺序在字段中出现)
            if (isSubsequenceMatch(q, f)) {
                // 字符越紧凑分越高(紧凑度 = q.length / f.length)
                const compactness = q.length / f.length;
                // 基础分 30 + 紧凑度加成(最多 25 分)
                return 30 + Math.floor(compactness * 25);
            }
            return 0;
        }

        // 模糊搜索主函数(精确 > 开头 > 包含 > 子序列,同分时大国靠前)
        function searchCountries(query) {
            const q = query.trim().toLowerCase();
            if (!q) return [];

            const results = [];
            for (const country of searchState.countries) {
                // 中英文两个字段都计算得分,取高的
                const scoreCn = scoreField(q, country.name.toLowerCase());
                const scoreEn = scoreField(q, (country.name_en || '').toLowerCase());
                const score = Math.max(scoreCn, scoreEn);
                if (score > 0) {
                    results.push({ country, score });
                }
            }

            // 排序:分数高在前;同分时大国(diameter 大)靠前,更显眼
            results.sort((a, b) => b.score - a.score || b.country.diameter - a.country.diameter);

            // 返回前 12 个
            return results.slice(0, 12).map(r => r.country);
        }

        // 根据国家直径计算合适的跳转缩放(直径越大,缩得越小)
        function calcTargetZoom(diameter) {
            // 经验公式：zoom = 80 / (diameter * 2)
            const target = 80 / (Math.max(diameter, 1) * 2);
            // 限制在合理范围内(与 scaleLimit 对齐,移动端跳转到更小国家时也能放大)
            const maxZoom = isMobileDevice ? 16 : 20;
            return Math.max(1.2, Math.min(maxZoom, target));
        }

        // 打开/关闭搜索面板
        function openSearchPanel() {
            const panel = document.getElementById('searchPanel');
            panel.classList.add('open');
            searchState.panelOpen = true;
            // 自动聚焦输入框
            setTimeout(() => document.getElementById('searchInput').focus(), 50);
        }
        function closeSearchPanel() {
            const panel = document.getElementById('searchPanel');
            panel.classList.remove('open');
            searchState.panelOpen = false;
            searchState.activeIndex = -1;
            document.getElementById('searchInput').value = '';
            document.getElementById('searchSuggestions').innerHTML = '';
        }

        // 渲染建议列表
        function renderSuggestions(results, query) {
            const container = document.getElementById('searchSuggestions');
            if (!results || results.length === 0) {
                container.innerHTML = query
                    ? `<div class="search-empty">未找到匹配 "${escapeHtml(query)}" 的国家</div>`
                    : '';
                return;
            }

            container.innerHTML = results.map((c, i) => `
                <div class="search-suggestion${i === searchState.activeIndex ? ' active' : ''}"
                     data-index="${i}" data-iso="${c.iso}">
                    <span class="iso">${c.iso}</span>
                    <span class="name">${highlight(c.name, query)}</span>
                    <span class="name-en">${highlight(c.name_en || '', query)}</span>
                    <span class="diameter">⌀${c.diameter}°</span>
                </div>
            `).join('');

            // 绑定点击事件
            container.querySelectorAll('.search-suggestion').forEach(el => {
                el.addEventListener('click', () => {
                    const idx = parseInt(el.dataset.index, 10);
                    const c = results[idx];
                    if (c) jumpToCountry(c.iso, c.name);
                });
            });
        }

        // 绑定所有搜索相关事件(输入框、键盘、按钮)
        function bindSearchEvents() {
            const searchToggle = document.getElementById('searchToggle');
            const searchInput = document.getElementById('searchInput');

            // 点击搜索图标切换面板
            searchToggle.addEventListener('click', () => {
                if (searchState.panelOpen) {
                    closeSearchPanel();
                } else {
                    openSearchPanel();
                }
            });

            // 输入时实时筛选
            searchInput.addEventListener('input', () => {
                const q = searchInput.value;
                searchState.suggestions = searchCountries(q);
                searchState.activeIndex = searchState.suggestions.length > 0 ? 0 : -1;
                renderSuggestions(searchState.suggestions, q.trim());
            });

            // 键盘导航
            searchInput.addEventListener('keydown', (e) => {
                if (!searchState.panelOpen) return;

                if (e.key === 'ArrowDown') {
                    e.preventDefault();
                    const len = searchState.suggestions.length;
                    if (len === 0) return;
                    searchState.activeIndex = (searchState.activeIndex + 1) % len;
                    updateActiveSuggestion();
                } else if (e.key === 'ArrowUp') {
                    e.preventDefault();
                    const len = searchState.suggestions.length;
                    if (len === 0) return;
                    searchState.activeIndex = (searchState.activeIndex - 1 + len) % len;
                    updateActiveSuggestion();
                } else if (e.key === 'Enter') {
                    e.preventDefault();
                    if (searchState.activeIndex >= 0 && searchState.activeIndex < searchState.suggestions.length) {
                        const c = searchState.suggestions[searchState.activeIndex];
                        jumpToCountry(c.iso, c.name);
                    }
                } else if (e.key === 'Escape') {
                    e.preventDefault();
                    closeSearchPanel();
                }
            });

            // 全局快捷键:/ 聚焦输入框;Ctrl+K 打开面板
            document.addEventListener('keydown', (e) => {
                if (e.key === '/' && !searchState.panelOpen && document.activeElement.tagName !== 'INPUT') {
                    e.preventDefault();
                    openSearchPanel();
                } else if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
                    e.preventDefault();
                    if (searchState.panelOpen) closeSearchPanel();
                    else openSearchPanel();
                } else if (e.key === 'Escape' && searchState.panelOpen) {
                    closeSearchPanel();
                }
            });
        }

        function updateActiveSuggestion() {
            const items = document.querySelectorAll('.search-suggestion');
            items.forEach((el, i) => {
                el.classList.toggle('active', i === searchState.activeIndex);
            });
            // 滚动到可见区域
            const active = items[searchState.activeIndex];
            if (active) active.scrollIntoView({ block: 'nearest' });
        }

        // ====================================================
        // 搜索功能代码块结束
        // ====================================================

        // 国家数据缓存
        const countryCache = {
            info: new Map(),      // 国家基本信息缓存
            videos: new Map(),    // 国家视频列表缓存

            // 获取缓存的国家信息
            getInfo(countryId) {
                return this.info.get(countryId);
            },

            // 设置缓存的国家信息
            setInfo(countryId, data) {
                this.info.set(countryId, data);
            },

            // 获取缓存的视频列表
            getVideos(countryId) {
                return this.videos.get(countryId);
            },

            // 设置缓存的视频列表
            setVideos(countryId, data) {
                this.videos.set(countryId, data);
            },

            // 清空缓存
            clear() {
                this.info.clear();
                this.videos.clear();
            }
        };

        // 预加载状态管理
        const preloadState = {
            loading: new Map(),   // 正在加载的国家
            loaded: new Map(),    // 已加载完成的国家数据
            currentIsoCode: null, // 当前悬停的国家

            // 开始加载国家数据
            async load(countryId, enName, displayName) {
                // 如果已经在缓存中,直接返回
                if (this.loaded.has(countryId)) {
                    return this.loaded.get(countryId);
                }

                // 如果正在加载中,返回现有的 Promise
                if (this.loading.has(countryId)) {
                    return this.loading.get(countryId);
                }

                // 开始新的加载
                const loadPromise = this._doLoad(countryId, enName, displayName);
                this.loading.set(countryId, loadPromise);

                try {
                    const data = await loadPromise;
                    this.loaded.set(countryId, data);
                    return data;
                } finally {
                    this.loading.delete(countryId);
                }
            },

            // 实际加载数据
            async _doLoad(countryId, enName, displayName) {
                const [info, videosData] = await Promise.all([
                    fetchCountryInfo(countryId),
                    fetchCountryVideos(countryId)
                ]);

                if (info) {
                    return {
                        name: info.name,
                        name_en: info.name_en,
                        flag: info.national_flag,
                        intro: info.description,
                        capital: info.capital,
                        // 支持新的分类格式或旧的平铺格式
                        videosByCategory: videosData.videos_by_category || null,
                        categories: videosData.categories || null,
                        // 兼容旧格式
                        videos: videosData.videos || (videosData.videos_by_category ?
                            Object.values(videosData.videos_by_category).flat() : [])
                    };
                }

                // 回退到默认数据
                return getDefaultCountryData(enName);
            },

            // 获取已加载的数据(如果存在)
            getLoaded(countryId) {
                return this.loaded.get(countryId);
            },

            // 检查是否已加载
            isLoaded(countryId) {
                return this.loaded.has(countryId);
            },

            // 清空
            clear() {
                this.loading.clear();
                this.loaded.clear();
                this.currentIsoCode = null;
            }
        };

        // 从后端获取国家基本信息
        async function fetchCountryInfo(countryId) {
            // 先查缓存
            const cached = countryCache.getInfo(countryId);
            if (cached) {
                console.log(`[缓存命中] 国家信息: ${countryId}`);
                return cached;
            }

            try {
                console.log(`[请求后端] 国家信息: ${countryId}`);
                const response = await fetch(`${API_BASE_URL}/country/${countryId}`);
                if (!response.ok) {
                    if (response.status === 404) {
                        return null; // 国家不存在
                    }
                    throw new Error(`HTTP ${response.status}`);
                }
                const result = await response.json();
                if (result.success) {
                    countryCache.setInfo(countryId, result.data);
                    return result.data;
                }
                return null;
            } catch (error) {
                console.error(`获取国家信息失败 ${countryId}:`, error);
                return null;
            }
        }

        // 从后端获取国家视频列表(返回包含分类的完整数据)
        async function fetchCountryVideos(countryId) {
            // 先查缓存
            const cached = countryCache.getVideos(countryId);
            if (cached) {
                console.log(`[缓存命中] 视频列表: ${countryId}`);
                return cached;
            }

            try {
                console.log(`[请求后端] 视频列表: ${countryId}`);
                const response = await fetch(`${API_BASE_URL}/country/${countryId}/videos`);
                if (!response.ok) {
                    if (response.status === 404) {
                        return { videos: [], videos_by_category: {}, categories: [] };
                    }
                    throw new Error(`HTTP ${response.status}`);
                }
                const result = await response.json();
                if (result.success) {
                    // 缓存完整数据(包含分类)
                    countryCache.setVideos(countryId, result.data);
                    return result.data;
                }
                return { videos: [], videos_by_category: {}, categories: [] };
            } catch (error) {
                console.error(`获取视频列表失败 ${countryId}:`, error);
                return { videos: [], videos_by_category: {}, categories: [] };
            }
        }

        // 根据国家英文名或ISO代码获取数据(优先使用后端API)
        // 注意:现在使用 preloadState 来管理加载,这个函数保留用于兼容性
        async function getCountryData(enName, isoCode) {
            if (isoCode && preloadState.isLoaded(isoCode)) {
                return preloadState.getLoaded(isoCode);
            }

            // 如果有ISO代码,优先从后端获取
            if (isoCode) {
                const [info, videosData] = await Promise.all([
                    fetchCountryInfo(isoCode),
                    fetchCountryVideos(isoCode)
                ]);

                if (info) {
                    return {
                        name: info.name,
                        name_en: info.name_en,
                        flag: info.national_flag,
                        intro: info.description,
                        capital: info.capital,
                        videosByCategory: videosData.videos_by_category || null,
                        categories: videosData.categories || null,
                        videos: videosData.videos || (videosData.videos_by_category ?
                            Object.values(videosData.videos_by_category).flat() : [])
                    };
                }
            }

            // 回退到本地默认数据
            return getDefaultCountryData(enName);
        }

        // 默认本地数据(作为后备 - 极简版本)
        function getDefaultCountryData(enName) {
            return {
                name: enName,
                name_en: enName,
                flag: `https://flagcdn.com/w80/${getCountryCode(enName)}.png`,
                intro: `${enName} - 暂无详细信息`,
                videos: []
            };
        }

        // 简单的国家代码映射(用于默认国旗)
        function getCountryCode(enName) {
            const codeMap = {
                'China': 'cn', 'United States': 'us', 'Russia': 'ru', 'Brazil': 'br',
                'Australia': 'au', 'Japan': 'jp', 'United Kingdom': 'gb', 'France': 'fr',
                'Germany': 'de', 'India': 'in', 'Canada': 'ca', 'Italy': 'it',
                'Spain': 'es', 'Mexico': 'mx', 'Korea': 'kr', 'Netherlands': 'nl'
            };
            return codeMap[enName] || 'un';
        }

        // 缩放步进限制参数(按设备差异化)
        const SCALE_LIMIT = isMobileDevice
            ? { min: 1.2, max: 16 }     // 移动端:放大范围加大(原本 2-8 太窄,小国点不到)
            : { min: 0.8, max: 20 };    // 桌面端:缩放范围更宽
        const MAX_ZOOM_DELTA = isMobileDevice ? 0.5 : 0.6;  // 单次 zoom 变化幅度上限(移动端 0.25 → 0.5 加速缩放)
        let lastZoom = 1.2;  // 跟踪当前 zoom(用于步进限制)

        // 加载阿里云 + Natural Earth 合并世界地图数据 + 南海九段线
        fetch('./static/js/countries_enhanced.geo.js')
            .then(res => res.json())
            .then(worldJson => {
                echarts.registerMap('world', worldJson);
                const mapData = worldJson.features.map(f => ({
                    id: f.id,
                    name: f.properties.name,
                    name_en: f.properties.name_en,
                    value: 1,  // 占位值
                    diameter: f.properties.diameter || 10,  // 关键:传入直径
                    // CHN_JD (南海九段线) 特殊渲染:透明区域 + 虚线边框
                    // 不响应点击 / hover
                    ...(f.id === 'CHN_JD' ? {
                        itemStyle: {
                            areaColor: 'rgba(0,0,0,0)',
                            borderColor: '#3e2723',
                            borderWidth: 1.2,
                            borderType: 'dashed',
                            shadowBlur: 0,
                            shadowColor: 'transparent'
                        },
                        emphasis: {
                            itemStyle: {
                                areaColor: 'rgba(0,0,0,0)',
                                borderColor: '#3e2723',
                                borderWidth: 1.2,
                                borderType: 'dashed',
                                shadowBlur: 0,
                                shadowColor: 'transparent'
                            },
                            label: { show: false }
                        },
                        label: { show: false },
                        silent: true  // 不响应交互
                    } : {})
                }));
                console.log(`[地图] features=${worldJson.features.length}, 含南海九段线`);

                const option = {
                    backgroundColor: 'transparent',
                    series: [{
                        type: 'map',
                        map: 'world',
                        roam: true,          // 开启鼠标缩放和平移
                        zoom: 1.2,
                        center: [0, 0],    // 初始中心点
                        silent: false,
                        data: mapData,  // 关键:传入带 diameter 的数据
                        scaleLimit: SCALE_LIMIT,  // 按设备差异化(桌面端 {0.8,20}, 移动端 {2,8})
                        label: {
                            show: false ,
                            color: '#3e2723',
                            fontFamily: 'serif',
                            fontSize: 12,
                            fontWeight: 'bold'
                        },
                        itemStyle: {
                            areaColor: '#c9b896', //区域颜色 - 深羊皮黄
                            borderColor: '#6b4423',  // 线条颜色 - 深褐色墨水
                            borderWidth: 1.5,
                            shadowColor: 'rgba(60, 40, 20, 0.2)',
                            // shadowBlur: 10
                            shadowBlur: 3,
                            shadowOffsetX: 1,
                            shadowOffsetY: 1
                        },
                        emphasis: {
                            itemStyle: {
                                areaColor: '#a08060',  // 悬停变深(茶渍色)
                                borderColor: '#3e2723',    // 近黑色边界
                                borderWidth: 2.5,
                                shadowBlur: 15,
                                // shadowColor: 'rgba(14, 165, 233, 0.6)'
                            },
                            label: {
                                show: true,
                                color: '#3e2723',
                                fontFamily: 'serif',
                                fontSize: 14,
                                fontWeight: 'bold'
                            }
                        }
                    }],
                    tooltip: {
                        show: false  // 禁用 ECharts 内置 tooltip,完全由我们自定义 DOM tooltip 控制
                                      // (原因:ECharts 内置 tooltip 会在鼠标移出 series 时自动 hideTip,
                                      //  老板要求:弹窗只能通过点击控制,鼠标移动不更新状态)
                    }
                };

                // 计算每个国家的质心坐标
                function calculateCentroid(feature) {
                    const coords = feature.geometry.coordinates;
                    let sumX = 0, sumY = 0, count = 0;

                    function processCoords(c) {
                        if (Array.isArray(c[0])) {
                            c.forEach(processCoords);
                        } else {
                            sumX += c[0];
                            sumY += c[1];
                            count++;
                        }
                    }

                    processCoords(coords);
                    return count > 0 ? [sumX / count, sumY / count] : [0, 0];
                }

                // 为每个国家计算质心
                const countryCentroids = {};
                worldJson.features.forEach(f => {
                    countryCentroids[f.id] = calculateCentroid(f);
                });

                // 初始化搜索索引 + 绑定事件
                searchState.countries = buildSearchIndex(worldJson);
                bindSearchEvents();
                console.log(`[搜索] 索引构建完成,共 ${searchState.countries.length} 个国家`);

                // ============================================
                // 搜索跳转(自动进入 CLICK 模式 + 状态机锁定)
                // ============================================
                window.jumpToCountry = function(isoCode, displayName) {
                    const target = searchState.countries.find(c => c.iso === isoCode);
                    if (!target) return;

                    const zoom = calcTargetZoom(target.diameter);
                    closeSearchPanel();

                    chart.setOption({
                        series: [{ zoom: zoom, center: target.centroid }]
                    });
                    // 同步 lastZoom,避免跳转后被步进限幅
                    lastZoom = zoom;
                    // 搜索跳转永远走 CLICK 模式
                    if (interaction.mode === INTERACTION_MODE.HOVER) {
                        switchToMode(INTERACTION_MODE.CLICK);
                    }
                    interaction.lockedIsoCode = isoCode;
                    if (interaction.hideTimer) { clearTimeout(interaction.hideTimer); interaction.hideTimer = null; }

                    const fakeParams = {
                        name: displayName,
                        data: { id: isoCode, name_en: target.name_en },
                        dataIndex: 0
                    };
                    // 等待 ECharts 动画/重绘完成后再触发 tooltip(避免位置错位)
                    setTimeout(() => showCountryTooltip(fakeParams, true), 450);
                };

                chart.setOption(option);
                lastZoom = chart.getOption().series[0].zoom || 1.2;

                // ============================================
                // 窗口 resize:重画 + 重定位 tooltip
                // ============================================
                window.addEventListener('resize', () => {
                    chart.resize();
                    repositionActiveTooltip();
                });

                // ============================================
                // 缩放:标签刷新 + 步进限制(同一个 georoam 监听)
                // ============================================
                chart.on('georoam', function(params) {
                    // 1) 标签按缩放级别显示
                    updateLabelsByZoom();
                    // 2) 地图缩放/平移后重新定位当前 tooltip(位置跟着质心走)
                    repositionActiveTooltip();
                    // 3) 步进限制(仅 zoom,drag 不限)
                    if (params.zoom === undefined) return;
                    const delta = params.zoom - lastZoom;
                    if (Math.abs(delta) > MAX_ZOOM_DELTA) {
                        const limitedZoom = lastZoom + Math.sign(delta) * MAX_ZOOM_DELTA;
                        chart.setOption({ series: [{ zoom: limitedZoom }] });
                        lastZoom = limitedZoom;
                    } else {
                        lastZoom = params.zoom;
                    }
                });

                // ============================================
                // Tooltip 显示/隐藏(两种模式共用)
                // 完全使用自定义 DOM tooltip,不受 ECharts 默认行为影响
                // ============================================

                // 自定义 tooltip DOM 元素(页面加载时已在 body 添加)
                function getCustomTooltipEl() {
                    return document.getElementById('customCountryTooltip');
                }

                // 计算 tooltip 应该出现的位置(质心为锚点)
                function calculateTooltipPosition() {
                    if (!interaction.tooltipState) return null;
                    const { isoCode } = interaction.tooltipState;
                    const centroid = countryCentroids[isoCode];
                    const tipEl = getCustomTooltipEl();
                    if (!tipEl) return null;

                    const viewportWidth = window.innerWidth;
                    const viewportHeight = window.innerHeight;
                    const isMobile = viewportWidth <= 480;
                    const tooltipWidth = isMobile ? 280 : 320;
                    // offsetHeight 需 tooltip 已渲染,初始取估计值
                    const tooltipHeight = tipEl.offsetHeight || 300;

                    if (!centroid) {
                        return {
                            left: (viewportWidth - tooltipWidth) / 2,
                            top: viewportHeight * 0.15
                        };
                    }
                    const pixelPos = chart.convertToPixel({seriesIndex: 0}, centroid);
                    if (!pixelPos) {
                        return {
                            left: (viewportWidth - tooltipWidth) / 2,
                            top: viewportHeight * 0.15
                        };
                    }
                    if (isMobile) {
                        return {
                            left: (viewportWidth - tooltipWidth) / 2,
                            top: viewportHeight * 0.1
                        };
                    }
                    // 桌面端:质心上方优先,空间不够时在质心下方
                    let left = pixelPos[0] - tooltipWidth / 2;
                    let top = pixelPos[1] - tooltipHeight - 15;
                    if (top < 10) top = pixelPos[1] + 15;
                    if (left < 10) left = 10;
                    if (left + tooltipWidth > viewportWidth - 10) {
                        left = viewportWidth - tooltipWidth - 10;
                    }
                    return { left, top };
                }

                // 渲染并显示 tooltip
                function showCustomTooltip() {
                    if (!interaction.tooltipState) return;
                    const { countryData, isoCode, enName, displayName, isLoading } = interaction.tooltipState;
                    const tipEl = getCustomTooltipEl();
                    if (!tipEl) return;

                    // 设置内容
                    if (isLoading || !countryData) {
                        tipEl.innerHTML = buildLoadingHtml(displayName, enName, isoCode);
                    } else {
                        tipEl.innerHTML = buildTooltipHtml(countryData, isoCode, enName, displayName);
                    }

                    // 显示 + 定位
                    tipEl.style.display = 'block';
                    const pos = calculateTooltipPosition();
                    if (pos) {
                        tipEl.style.left = pos.left + 'px';
                        tipEl.style.top = pos.top + 'px';
                    }
                }

                // 隐藏 tooltip
                function hideCustomTooltip() {
                    const tipEl = getCustomTooltipEl();
                    if (tipEl) tipEl.style.display = 'none';
                }

                // 窗口缩放 / 地图缩放时重新定位当前 tooltip
                function repositionActiveTooltip() {
                    if (!interaction.tooltipState) return;
                    const tipEl = getCustomTooltipEl();
                    if (!tipEl || tipEl.style.display === 'none') return;
                    const pos = calculateTooltipPosition();
                    if (pos) {
                        tipEl.style.left = pos.left + 'px';
                        tipEl.style.top = pos.top + 'px';
                    }
                }

                function showCountryTooltip(params, forceLoading = false) {
                    const country = params.name;
                    const isoCode = params.data.id;
                    const enName = params.data.name_en || country;

                    if (!forceLoading && preloadState.isLoaded(isoCode)) {
                        // 已缓存:立即显示完整内容
                        const countryData = preloadState.getLoaded(isoCode);
                        interaction.tooltipState = { countryData, isoCode, enName, displayName: country, isLoading: false };
                        showCustomTooltip();
                    } else {
                        // 未缓存:先显示加载中,异步刷新
                        interaction.tooltipState = { countryData: null, isoCode, enName, displayName: country, isLoading: true };
                        showCustomTooltip();

                        preloadState.load(isoCode, enName, country).then((countryData) => {
                            // 校验仍相关性:
                            //   - CLICK 模式:仅锁定同一国家时刷新
                            //   - HOVER 模式:无锁定概念,直接刷新
                            const stillRelevant = interaction.mode === INTERACTION_MODE.CLICK
                                ? interaction.lockedIsoCode === isoCode
                                : true;
                            if (stillRelevant) {
                                interaction.tooltipState = { countryData, isoCode, enName, displayName: country, isLoading: false };
                                showCustomTooltip();  // 重新渲染为完整内容
                            }
                        });
                    }
                }

                function cancelTooltipHide() {
                    if (interaction.hideTimer) {
                        clearTimeout(interaction.hideTimer);
                        interaction.hideTimer = null;
                    }
                }
                function scheduleTooltipHide() {
                    cancelTooltipHide();
                    interaction.hideTimer = setTimeout(() => {
                        // CLICK 模式下锁定时不隐藏
                        if (interaction.mode === INTERACTION_MODE.CLICK && interaction.lockedIsoCode) {
                            return;
                        }
                        hideCustomTooltip();
                    }, TOOLTIP_HIDE_DELAY_MS);
                }
                function closeTooltip() {
                    cancelTooltipHide();
                    if (interaction.hoverTimer) {
                        clearTimeout(interaction.hoverTimer);
                        interaction.hoverTimer = null;
                    }
                    interaction.lockedIsoCode = null;
                    hideCustomTooltip();
                }

                // ============================================
                // 事件绑定(CLICK / HOVER 严格分支,互不干扰)
                // ============================================

                // ---- HOVER 模式专用 ----
                chart.on('mouseover', function(params) {
                    if (params.componentType !== 'series') return;
                    if (interaction.mode !== INTERACTION_MODE.HOVER) return; // CLICK 模式直接短路
                    const country = params.name;
                    const isoCode = params.data.id;
                    const enName = params.data.name_en || country;
                    cancelTooltipHide();
                    if (interaction.hoverTimer) clearTimeout(interaction.hoverTimer);

                    // 500ms 防误触:到点后才决定是否显示
                    interaction.hoverTimer = setTimeout(() => {
                        // 统一调用 showCountryTooltip:
                        //   - 已加载:立即显示完整内容
                        //   - 未加载:先显示 loading,加载完成后自动刷新为完整内容
                        showCountryTooltip({ name: country, data: { id: isoCode, name_en: enName } }, false);
                    }, HOVER_DELAY_MS);
                });

                // ---- HOVER 模式专用:鼠标移出 ----
                chart.on('mouseout', function(params) {
                    if (params.componentType !== 'series') return;
                    if (interaction.mode !== INTERACTION_MODE.HOVER) return;
                    if (interaction.hoverTimer) {
                        clearTimeout(interaction.hoverTimer);
                        interaction.hoverTimer = null;
                    }
                    scheduleTooltipHide();
                });

                // ---- CLICK 模式专用 ----
                let __lastClickHandled = 0;  // 共享给 zr click handler,避免同一次点击重复处理
                chart.on('click', function(params) {
                    __lastClickHandled = Date.now();  // 标记为已处理(防止 zr click 重复关闭)
                    if (interaction.mode !== INTERACTION_MODE.CLICK) return; // HOVER 模式直接短路

                    // 点击的不是国家(如海洋、空白) -> 关闭弹窗
                    // (原因:chart.containPixel 不准会误判海洋;zrender 内部派发的 click 事件也可能是空 hit)
                    if (params.componentType !== 'series' || !params.data || !params.data.id) {
                        closeTooltip();
                        return;
                    }

                    const country = params.name;
                    const isoCode = params.data.id;
                    const enName = params.data.name_en || country;

                    // 1) 再次点击同一国家 -> 关闭弹窗(toggle 行为)
                    if (interaction.lockedIsoCode === isoCode) {
                        closeTooltip();
                        return;
                    }

                    // 2) 点击其他国家 -> 立即显示新国家的弹窗(锁定 + 质心位置)
                    //    tooltip 位置不随鼠标移动,完全锁定在质心(由 tooltip.position 函数保证)
                    interaction.lockedIsoCode = isoCode;
                    cancelTooltipHide();
                    showCountryTooltip({ name: country, data: { id: isoCode, name_en: enName } }, true);
                });

                // 空白处点击:仅 CLICK 模式解除锁定
                // 用 zr.on('mouseup') 替代 native click 监听(避免移动端 touch 抖动造成"闪过就关")
                // (原因:移动端 touchend 手指抖动导致 touchend 位置 与 touchstart 不同,
                //  如果 touchend 位置在海洋上,findHover 返空,但用户意图是点击刚才 touchstart 命中的国家,
                //  就会“刚显示就关闭”。修复:用 mouseup 判断 + 300ms grace period
                //  保护刚刚点击的国家,避免同一点击被判定为空白)
                chart.getZr().on('mouseup', function(e) {
                    if (interaction.mode !== INTERACTION_MODE.CLICK) return;
                    // 300ms grace:刚被 chart.on('click') 命中的点击,即使 mouseup 位置在海洋也不关闭
                    if (Date.now() - __lastClickHandled < 300) return;
                    // mouseup target = null 表示点在海洋/空白处
                    if (!e.target) {
                        closeTooltip();
                    }
                });

                // ---- 跨模式:tooltip 框内鼠标穿透(防止自动关闭)----
                document.addEventListener('mouseover', function(e) {
                    const tooltipEl = e.target.closest('.country-info');
                    if (!tooltipEl) return;
                    cancelTooltipHide();
                    if (interaction.hoverTimer) {
                        clearTimeout(interaction.hoverTimer);
                        interaction.hoverTimer = null;
                    }
                });

                document.addEventListener('mouseout', function(e) {
                    const tooltipEl = e.target.closest('.country-info');
                    if (!tooltipEl) return;
                    const related = e.relatedTarget;
                    // 移到 tooltip 内 / 画布上 → 不关闭
                    if (related && (related.closest('.country-info') || related.closest('canvas'))) {
                        return;
                    }
                    // CLICK 模式:tooltip 内鼠标离开不主动关闭(锁定由空白点击解除)
                    if (interaction.mode === INTERACTION_MODE.CLICK) return;
                    // HOVER 模式:按计划延迟关闭
                    scheduleTooltipHide();
                });

                function updateLabelsByZoom() {
                    const zoom = chart.getOption().series[0].zoom;

                    const newData = worldJson.features.map(f => {
                        const d = f.properties.diameter || 10;
                        const pixelSize = d * zoom * 2;

                        return {
                            name: f.properties.name,
                            value: 1,
                            id: f.id,
                            name_en: f.properties.name_en,
                            label: {
                                show: pixelSize > 60,
                                fontSize: pixelSize > 120 ? 13 : 10
                            }
                        };
                    });

                    chart.setOption({ series: [{ data: newData }] });
                }

                // ============================================
                // 悬停模式开关:点击切换、桌面端记忆、移动端隐藏
                // ============================================
                const hoverToggleBtn = document.getElementById('hoverToggle');

                function syncHoverToggleButton() {
                    if (!hoverToggleBtn) return;
                    const isHover = interaction.mode === INTERACTION_MODE.HOVER;
                    hoverToggleBtn.classList.toggle('off', !isHover);
                    hoverToggleBtn.setAttribute('aria-checked', String(isHover));
                    hoverToggleBtn.title = isHover
                        ? '悬停提示(已开启,鼠标移到国家 ~500ms 出 tooltip)'
                        : '悬停提示(已关闭,点击国家才出 tooltip)';
                }

                function switchToMode(newMode) {
                    if (interaction.mode === newMode) return;
                    closeTooltip();
                    interaction.mode = newMode;
                    // 仅桌面端写 localStorage(移动端永远 CLICK)
                    if (!isMobileDevice) {
                        localStorage.setItem(STORAGE_KEY_HOVER, newMode === INTERACTION_MODE.HOVER ? 'true' : 'false');
                    }
                    syncHoverToggleButton();
                }

                if (hoverToggleBtn) {
                    hoverToggleBtn.addEventListener('click', () => {
                        const newMode = interaction.mode === INTERACTION_MODE.HOVER
                            ? INTERACTION_MODE.CLICK
                            : INTERACTION_MODE.HOVER;
                        switchToMode(newMode);
                    });
                    syncHoverToggleButton();
                    // 移动端强制隐藏开关
                    if (isMobileDevice) {
                        hoverToggleBtn.classList.add('mobile-hidden');
                    }
                }

                console.log(`[交互] 初始化完成 - 设备: ${isMobileDevice ? '移动' : '桌面'}, 模式: ${interaction.mode}`);

                console.log(`[交互] 初始化完成 - 设备: ${isMobileDevice ? '移动' : '桌面'}, 模式: ${interaction.mode}`);
            })
            .catch(err => {
                console.error('地图数据加载失败:', err);
                document.getElementById('map').innerHTML =
                    '<div style="color:white;text-align:center;padding-top:20%">地图数据加载失败,请检查网络连接</div>';
            });

        // 响应式
        window.addEventListener('resize', () => chart.resize());

        // 构建加载中的 tooltip HTML
        function buildLoadingHtml(country, enName, isoCode) {
            const tooltipId = `tooltip-${isoCode}`;
            return `
                <div id="${tooltipId}" class="tooltip-container loading">
                    <div class="country-header">
                        <div class="country-flag" style="background:linear-gradient(90deg, #d4c4a8 25%, #e5d5b8 50%, #d4c4a8 75%);background-size:200% 100%;animation:shimmer 1.5s infinite;width:36px;height:24px;border-radius:3px;"></div>
                        <div class="country-name">
                            <h4>${country}</h4>
                            <div class="country-en">${enName}</div>
                        </div>
                    </div>
                    <div class="country-content">
                        <div class="loading-content" style="text-align:center;padding:30px 20px;color:#8b6914;">
                            <div class="loading-spinner" style="width:30px;height:30px;border:3px solid rgba(139,105,20,0.2);border-top-color:#8b6914;border-radius:50%;animation:spin 1s linear infinite;margin:0 auto 15px;"></div>
                            <div style="font-size:13px;">正在加载...</div>
                        </div>
                    </div>
                </div>
            `;
        }

        // 将 publish_date 字符串（2026/4/4 或 2026-04-04）格式化为 YYYY-MM-DD
        function formatPublishDate(s) {
            if (!s) return '';
            const m = String(s).match(/^(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})/);
            if (!m) return s;
            return `${m[1]}-${m[2].padStart(2, '0')}-${m[3].padStart(2, '0')}`;
        }

        // 构建单个视频项 HTML
        function buildVideoItemHtml(v) {
            // 处理 B 站图片防盗链
            let thumbUrl = v.thumb || v.cover_url;
            if (thumbUrl && thumbUrl.includes('hdslb.com')) {
                thumbUrl = thumbUrl.replace('http://', 'https://');
            }
            const title = v.title || v.video_title;
            const url = v.url || v.video_url;
            const author = v.author || '';
            const dateText = formatPublishDate(v.publish_date);
            const authorHtml = `<span class="video-author" title="作者: ${escapeHtml(author || '未知')}">${escapeHtml(author || '未知')}</span>`;
            const dateHtml = dateText
                ? `<span class="video-date" title="发布于 ${dateText}">${dateText}</span>`
                : '';
            return `
                <div class="video-item" onclick="window.open('${url}', '_blank')">
                    <div class="video-thumb">
                        <img src="${thumbUrl}" alt="${title}"
                             referrerpolicy="no-referrer"
                             onerror="this.onerror=null; this.src=''; this.style.display='none'; this.parentElement.innerHTML='<span>视频</span>';">
                    </div>
                    <div class="video-info">
                        <div class="video-title" title="${title}">${title}</div>
                        <div class="video-meta">
                            ${authorHtml}
                            ${dateHtml}
                        </div>
                    </div>
                </div>
            `;
        }

        // 将 publish_date 字符串解析为可比较的时间戳，空日期视为 0（排到最后）
        function parsePublishDate(s) {
            if (!s) return 0;
            const m = String(s).match(/^(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})/);
            if (!m) return 0;
            return new Date(+m[1], +m[2] - 1, +m[3]).getTime();
        }

        // 按 publish_date 倒序排序：最新的在前，空日期排最后
        function sortVideosByDate(videos) {
            return [...videos].sort((a, b) =>
                parsePublishDate(b.publish_date) - parsePublishDate(a.publish_date)
            );
        }

        // 构建分类视频列表 HTML
        function buildCategorizedVideosHtml(countryData) {
            // 优先使用分类数据
            if (countryData.videosByCategory && Object.keys(countryData.videosByCategory).length > 0) {
                const categories = countryData.categories || Object.keys(countryData.videosByCategory);

                return categories.map(category => {
                    const videos = countryData.videosByCategory[category];
                    if (!videos || videos.length === 0) return '';

                    // 按创作日期倒序：最新 → 最早
                    const sortedVideos = sortVideosByDate(videos);
                    const videosHtml = sortedVideos.map(buildVideoItemHtml).join('');
                    return `
                        <div class="video-category">
                            <div class="video-category-title">${category} (${sortedVideos.length})</div>
                            <div class="video-list">
                                ${videosHtml}
                            </div>
                        </div>
                    `;
                }).join('');
            }

            // 回退到旧格式(平铺列表)
            const hasVideos = countryData.videos && countryData.videos.length > 0;
            if (!hasVideos) {
                return '<div class="no-videos">暂无视频</div>';
            }

            // 同样按日期倒序
            const sortedVideos = sortVideosByDate(countryData.videos);
            const videosHtml = sortedVideos.map(buildVideoItemHtml).join('');
            return `
                <div class="video-category">
                    <div class="video-category-title">相关视频 (${sortedVideos.length})</div>
                    <div class="video-list">
                        ${videosHtml}
                    </div>
                </div>
            `;
        }

        // 构建 tooltip HTML 内容
        function buildTooltipHtml(countryData, isoCode, enName, displayName) {
            const categorizedVideosHtml = buildCategorizedVideosHtml(countryData);
            const safeId = enName.replace(/\s+/g, '-');
            const tooltipId = `tooltip-${isoCode}`;

            return `
                <div id="${tooltipId}" class="tooltip-container">
                    <div class="country-header">
                        <img class="country-flag" src="${countryData.flag}" alt="${countryData.name}" onerror="this.style.display='none'">
                        <div class="country-name">
                            <h4>${countryData.name}</h4>
                            <div class="country-en">${countryData.name_en || enName}</div>
                        </div>
                    </div>
                    <div class="country-content">
                        <div class="country-intro">
                            <div class="country-intro-text" id="intro-${safeId}">${countryData.intro}</div>
                            <span class="intro-toggle" onclick="toggleIntro(this)">展开</span>
                        </div>
                        ${categorizedVideosHtml}
                    </div>
                </div>
            `;
        }

        // 展开/收起介绍文字
        window.toggleIntro = function(el) {
            const textEl = el.previousElementSibling;
            if (!textEl) return;

            const isExpanded = textEl.classList.contains('expanded');
            if (isExpanded) {
                textEl.classList.remove('expanded');
                el.textContent = '展开';
                // 滚动回顶部
                textEl.scrollTop = 0;
            } else {
                textEl.classList.add('expanded');
                el.textContent = '收起';
            }
        };
