Bangumi Staff 作品列表 v1.3

在 Bangumi 条目页面,鼠标悬停在 Staff 名字上时显示其参与作品信息。作品按评分人数排序,动态展示数量。适合看新番的时候快速认知各个staff。

// ==UserScript==
// @name         Bangumi Staff 作品列表 v1.3
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  在 Bangumi 条目页面,鼠标悬停在 Staff 名字上时显示其参与作品信息。作品按评分人数排序,动态展示数量。适合看新番的时候快速认知各个staff。
// @author       You
// @match        https://bgm.tv/subject/*
// @match        https://bangumi.tv/subject/*
// @match        https://chii.in/subject/*
// @grant        GM_xmlhttpRequest
// @connect      api.bgm.tv
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- 配置 ---
    const TOOLTIP_WIDTH = 480;
    const TOOLTIP_HEIGHT = 500; // 预估高度,用于定位
    const MAX_WORKS_TO_SHOW = 50;
    const MIN_WORKS_TO_SHOW = 5;
    const REQUEST_DELAY_MIN = 200;
    const REQUEST_DELAY_MAX = 500;
    const CACHE_EXPIRY = 30 * 60 * 1000; // 30分钟
    const IMAGE_SIZE = 'grid';

    // --- 工具函数 ---
    function formatDate(dateStr) {
        if (!dateStr) return 'N/A';
        const parts = dateStr.split('-');
        if (parts.length > 0 && parts[0]) {
            return parts[0];
        }
        return 'N/A';
    }

    function debounce(func, delay) {
        let timer;
        return function (...args) {
            clearTimeout(timer);
            timer = setTimeout(() => func.apply(this, args), delay);
        };
    }

    // --- 缓存 ---
    const cache = {
        data: {},
        set(key, value) {
            this.data[key] = {
                value: value,
                timestamp: Date.now()
            };
        },
        get(key) {
            const item = this.data[key];
            if (item && (Date.now() - item.timestamp) < CACHE_EXPIRY) {
                return item.value;
            }
            delete this.data[key];
            return null;
        }
    };

    // --- 核心逻辑 ---
    let tooltip = null;
    let currentFetchRequest = null;
    let hideTimeout = null;
    let showTimeout = null; // 用于延迟显示Tooltip
    let currentLink = null; // 记录当前触发 Tooltip 的链接元素
    let currentPersonId = null; // 记录当前请求的ID

    function createTooltip() {
        if (tooltip) return;

        tooltip = document.createElement('div');
        tooltip.id = 'bangumi-staff-tooltip-v1-8';
        // *** 修改1: 将 position 改为 absolute ***
        tooltip.style.cssText = `
            position: absolute;
            z-index: 9999;
            width: ${TOOLTIP_WIDTH}px;
            max-height: 600px;
            overflow: hidden; /* 外层容器隐藏溢出 */
            background: #f9f9f9;
            border: 1px solid #ddd;
            border-radius: 8px;
            box-shadow: 0 8px 24px rgba(0,0,0,0.15);
            padding: 16px;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            color: #333;
            line-height: 1.6;
            display: none;
            left: -1000px;
            top: -1000px;
        `;
        document.body.appendChild(tooltip);

        // 鼠标进入 Tooltip 时取消隐藏计划
        tooltip.addEventListener('mouseenter', () => {
            if (hideTimeout) {
                clearTimeout(hideTimeout);
                hideTimeout = null;
            }
        });

        // 鼠标离开 Tooltip 时计划隐藏
        tooltip.addEventListener('mouseleave', (e) => {
            // 检查鼠标是否移向了触发当前 Tooltip 的链接
            const toElement = e.relatedTarget;
            if (currentLink && currentLink.contains(toElement)) {
                // 移向了链接,不隐藏
                return;
            }
            scheduleHideTooltip();
        });

        const style = document.createElement('style');
        style.textContent = `
            #bangumi-staff-tooltip-v1-8 .tooltip-header {
                display: flex;
                flex-wrap: wrap;
                align-items: center;
                margin-bottom: 16px;
                padding-bottom: 16px;
                border-bottom: 1px solid #eee;
                gap: 10px;
            }
            #bangumi-staff-tooltip-v1-8 .tooltip-name {
                font-size: 20px;
                font-weight: 700;
                color: #0066cc;
                flex: 1;
                min-width: 0;
            }
            #bangumi-staff-tooltip-v1-8 .tooltip-name-text {
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
            }
            #bangumi-staff-tooltip-v1-8 .tooltip-name-link {
                color: inherit;
                text-decoration: none;
            }
            #bangumi-staff-tooltip-v1-8 .tooltip-name-link:hover {
                text-decoration: underline;
                color: #0056b3;
            }
            #bangumi-staff-tooltip-v1-8 .tooltip-stats {
                font-size: 13px;
                color: #666;
                white-space: nowrap;
            }
            #bangumi-staff-tooltip-v1-8 .tooltip-bio {
                font-size: 14px;
                color: #555;
                margin-bottom: 16px;
                line-height: 1.5;
            }
            /* *** 修改2: 为滚动区域添加类名和初始样式 *** */
            #bangumi-staff-tooltip-v1-8 .tooltip-works-scroll-container {
                max-height: 400px;
                overflow-y: auto; /* 初始允许滚动 */
                padding-right: 8px;
                margin: 0 0 10px 0;
            }
            #bangumi-staff-tooltip-v1-8 .tooltip-works-list {
                list-style: none;
                padding: 0;
                margin: 0;
            }
            #bangumi-staff-tooltip-v1-8 .tooltip-work-item {
                display: flex;
                margin-bottom: 16px;
                padding: 12px;
                border-radius: 8px;
                background: white;
                box-shadow: 0 2px 6px rgba(0,0,0,0.05);
            }
            #bangumi-staff-tooltip-v1-8 .tooltip-work-cover-container {
                width: 60px;
                height: 80px;
                flex-shrink: 0;
                border-radius: 4px;
                overflow: hidden;
                background-color: #f0f0f0;
                display: flex;
                align-items: center;
                justify-content: center;
            }
            #bangumi-staff-tooltip-v1-8 .tooltip-work-cover {
                width: 100%;
                height: 100%;
                object-fit: cover;
            }
            #bangumi-staff-tooltip-v1-8 .tooltip-work-content {
                flex: 1;
                margin-left: 16px;
                min-width: 0;
            }
            #bangumi-staff-tooltip-v1-8 .tooltip-work-title-row {
                display: flex;
                align-items: center;
                margin-bottom: 6px;
            }
            #bangumi-staff-tooltip-v1-8 .tooltip-work-title {
                font-weight: 700;
                color: #222;
                text-decoration: none;
                font-size: 15px;
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
                flex: 1;
                min-width: 0;
            }
            #bangumi-staff-tooltip-v1-8 .tooltip-work-title:hover {
                color: #0056b3;
                text-decoration: underline;
            }
            #bangumi-staff-tooltip-v1-8 .tooltip-work-meta {
                display: flex;
                flex-wrap: wrap;
                margin-bottom: 6px;
                font-size: 13px;
                color: #666;
                gap: 8px;
            }
            #bangumi-staff-tooltip-v1-8 .tooltip-work-year,
            #bangumi-staff-tooltip-v1-8 .tooltip-work-rating-count {
                background: #f0f0f0;
                padding: 2px 6px;
                border-radius: 4px;
                flex-shrink: 0;
            }
            #bangumi-staff-tooltip-v1-8 .tooltip-work-role {
                font-size: 13px;
                color: #555;
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
            }
            #bangumi-staff-tooltip-v1-8 .loading {
                text-align: center;
                padding: 30px 20px;
                color: #666;
            }
            #bangumi-staff-tooltip-v1-8 .loading::before {
                content: "";
                display: block;
                width: 40px;
                height: 40px;
                margin: 0 auto 15px;
                border: 3px solid rgba(0,119,204,0.2);
                border-top: 3px solid #0077cc;
                border-radius: 50%;
                animation: spin 1s linear infinite;
            }
            #bangumi-staff-tooltip-v1-8 .error {
                color: #e53935;
                padding: 15px;
                text-align: center;
                background: #fff2f2;
                border-radius: 8px;
            }

            @keyframes spin {
                0% { transform: rotate(0deg); }
                100% { transform: rotate(360deg); }
            }

            /* 自定义滚动条样式 */
            #bangumi-staff-tooltip-v1-8 .tooltip-works-scroll-container::-webkit-scrollbar {
                width: 6px;
            }
            #bangumi-staff-tooltip-v1-8 .tooltip-works-scroll-container::-webkit-scrollbar-track {
                background: #f1f1f1;
                border-radius: 10px;
            }
            #bangumi-staff-tooltip-v1-8 .tooltip-works-scroll-container::-webkit-scrollbar-thumb {
                background: #c1c1c1;
                border-radius: 10px;
            }
            #bangumi-staff-tooltip-v1-8 .tooltip-works-scroll-container::-webkit-scrollbar-thumb:hover {
                background: #a8a8a8;
            }
        `;
        document.head.appendChild(style);
    }

    // *** 修改3: 优化位置计算函数 (v1.0.8) ***
    function updateTooltipPosition(linkRect, mouseX, mouseY) {
        if (!tooltip) return;

        // 获取页面滚动偏移量
        const scrollX = window.pageXOffset || document.documentElement.scrollLeft;
        const scrollY = window.pageYOffset || document.documentElement.scrollTop;

        // 获取视口尺寸
        const viewportWidth = window.innerWidth;
        const viewportHeight = window.innerHeight;

        // 计算链接相对于文档的位置(加上滚动偏移)
        const linkDocTop = linkRect.top + scrollY;
        const linkDocLeft = linkRect.left + scrollX;
        const linkDocBottom = linkRect.bottom + scrollY;
        const linkDocRight = linkRect.right + scrollX;

        // --- 优化后的定位逻辑 ---
        let finalX, finalY;
        let positionFound = false;

        // 1. 优先尝试放在链接上方
        finalX = linkDocLeft;
        finalY = linkDocTop - TOOLTIP_HEIGHT - 10; // 上方 10px 间距

        // 检查上方空间是否足够且在视口内
        if (finalY >= scrollY) { // 上方不超出页面顶部
            positionFound = true;
        }

        // 2. 如果上方空间不足或超出视口顶部,尝试放在下方
        if (!positionFound) {
            finalY = linkDocBottom + 10; // 下方 10px 间距
            // 检查下方空间是否足够且在视口内
            if (finalY + TOOLTIP_HEIGHT <= scrollY + viewportHeight) { // 下方不超出页面底部
                positionFound = true;
            }
        }

        // 3. 如果上下方都不行,选择离视口边缘更近的一边并进行调整
        if (!positionFound) {
            const spaceAbove = linkDocTop - scrollY;
            const spaceBelow = (scrollY + viewportHeight) - linkDocBottom;

            if (spaceAbove > spaceBelow) {
                // 上方空间相对更大,强制放在上方可见区域
                finalY = Math.max(scrollY + 10, linkDocTop - TOOLTIP_HEIGHT - 10);
            } else {
                // 下方空间相对更大,强制放在下方可见区域
                finalY = Math.min(scrollY + viewportHeight - TOOLTIP_HEIGHT - 10, linkDocBottom + 10);
            }
            positionFound = true; // 此时无论如何都确定了Y位置
        }

        // 4. 处理水平方向,确保不超出左右边界
        if (finalX + TOOLTIP_WIDTH > scrollX + viewportWidth) {
            // 右侧空间不足,尝试左对齐链接右侧边缘
            finalX = linkDocRight - TOOLTIP_WIDTH;
            // 确保不会超出左边界
            finalX = Math.max(scrollX + 10, finalX);
        } else {
            // 确保左侧不会超出边界
            finalX = Math.max(scrollX + 10, finalX);
        }

        // 5. 最终微调 Y 轴,确保不会超出视口极端边界 (可选,增加鲁棒性)
        // 这一步在上面的逻辑中已经基本覆盖,但可以再做一次保险检查
        finalY = Math.max(scrollY + 10, Math.min(finalY, scrollY + viewportHeight - 200)); // 200是保守估计的最小可视高度

        tooltip.style.left = `${finalX}px`;
        tooltip.style.top = `${finalY}px`;
    }


    function showTooltip(htmlContent, linkElement, mouseX, mouseY) {
        if (!tooltip) createTooltip();

        // 清除之前的隐藏计划
        if (hideTimeout) {
            clearTimeout(hideTimeout);
            hideTimeout = null;
        }

        tooltip.innerHTML = htmlContent;
        tooltip.style.display = 'block';
        const linkRect = linkElement.getBoundingClientRect();
        updateTooltipPosition(linkRect, mouseX, mouseY);

        // *** 修改4: 在显示 Tooltip 后,为滚动区域添加事件监听器 ***
        const scrollContainer = tooltip.querySelector('.tooltip-works-scroll-container');
        if (scrollContainer) {
            // 移除旧的监听器(如果有的话),防止重复绑定
            scrollContainer.removeEventListener('wheel', handleScrollWheel);

            // 添加新的监听器
            scrollContainer.addEventListener('wheel', handleScrollWheel, { passive: false }); // passive: false 允许 preventDefault
        }
    }

    // *** 修改5: 处理滚轮事件的函数 (v1.0.7 修正版) ***
    function handleScrollWheel(event) {
        const container = event.currentTarget; // 这是 .tooltip-works-scroll-container

        // 如果容器内容不足以滚动,则不处理,让事件(默认)冒泡到页面
        if (container.scrollHeight <= container.clientHeight) {
            // 不调用 preventDefault,让事件冒泡
            return;
        }

        // 获取鼠标在容器内的位置
        const rect = container.getBoundingClientRect();
        const isMouseOverContainer = (
            event.clientX >= rect.left &&
            event.clientX <= rect.right &&
            event.clientY >= rect.top &&
            event.clientY <= rect.bottom
        );

        // 只有当鼠标悬停在可滚动容器上时,才处理滚动
        if (isMouseOverContainer) {
             const delta = event.deltaY;
             const scrollTop = container.scrollTop;
             const scrollHeight = container.scrollHeight;
             const clientHeight = container.clientHeight;

             // 检查是否滚动到了顶部或底部的边界
             const isAtTopBoundary = scrollTop === 0 && delta < 0;
             const isAtBottomBoundary = (scrollTop + clientHeight >= scrollHeight) && delta > 0;

             // 如果在边界上,不 preventDefault,让页面滚动
             if (isAtTopBoundary || isAtBottomBoundary) {
                 // 不调用 preventDefault,让事件冒泡
                 return;
             }

             // 否则,在容器内部滚动,并阻止事件冒泡
             event.preventDefault(); // 阻止页面滚动
             container.scrollTop += delta; // 手动滚动容器
        }
        // 如果鼠标不在容器上,事件会自然冒泡到页面,无需额外处理
    }

    function scheduleHideTooltip() {
        if (hideTimeout) {
            clearTimeout(hideTimeout);
        }
        hideTimeout = setTimeout(() => {
            hideTooltip();
        }, 300); // 保持 300ms 延迟
    }

    function hideTooltip() {
        // 清除所有计划
        if (showTimeout) {
            clearTimeout(showTimeout);
            showTimeout = null;
        }
        if (hideTimeout) {
            clearTimeout(hideTimeout);
            hideTimeout = null;
        }

        // 隐藏 Tooltip
        if (tooltip) {
            tooltip.style.display = 'none';
            // *** 修改6: 隐藏时移除滚动监听器 ***
            const scrollContainer = tooltip.querySelector('.tooltip-works-scroll-container');
            if (scrollContainer) {
                scrollContainer.removeEventListener('wheel', handleScrollWheel);
            }
        }

        // 取消进行中的请求
        if (currentFetchRequest) {
            currentFetchRequest.abort();
            currentFetchRequest = null;
        }

        // 重置状态
        currentLink = null;
        currentPersonId = null;
    }

    function sortWorksByRatingCount(works) {
        if (!Array.isArray(works)) return works;
        return [...works].sort((a, b) => {
            const countA = (a.rating && typeof a.rating.total === 'number') ? a.rating.total : 0;
            const countB = (b.rating && typeof b.rating.total === 'number') ? b.rating.total : 0;
            return countB - countA;
        });
    }

    function determineWorksToShowCount(totalWorks) {
        if (totalWorks <= MIN_WORKS_TO_SHOW) {
            return totalWorks;
        }
        return Math.min(Math.max(MIN_WORKS_TO_SHOW, Math.floor(totalWorks * 0.3)), MAX_WORKS_TO_SHOW, totalWorks);
    }

    function fetchPersonInfo(personId, personName, personUrl, linkElement, mouseX, mouseY) {
        // 如果有正在进行的请求(针对不同ID),先取消它
        if (currentFetchRequest && currentPersonId !== personId) {
            currentFetchRequest.abort();
            currentFetchRequest = null;
        }

        // 如果正在请求的是同一个ID,则不重复请求,但可以更新位置
        if (currentPersonId === personId) {
             if (tooltip && tooltip.style.display !== 'none') {
                 const linkRect = linkElement.getBoundingClientRect();
                 updateTooltipPosition(linkRect, mouseX, mouseY);
             }
            return;
        }

        // 更新当前状态为新ID
        currentPersonId = personId;
        currentLink = linkElement;

        const cachedData = cache.get(`person_${personId}`);
        if (cachedData) {
            displayPersonInfo(cachedData, personName, personUrl, linkElement, mouseX, mouseY);
            return;
        }

        showTooltip('<div class="loading">加载中...</div>', linkElement, mouseX, mouseY);

        const delay = Math.random() * (REQUEST_DELAY_MAX - REQUEST_DELAY_MIN) + REQUEST_DELAY_MIN;
        showTimeout = setTimeout(() => {
            // 再次检查状态,确保请求仍然是针对当前目标
            if (currentPersonId !== personId) return;

            currentFetchRequest = GM_xmlhttpRequest({
                method: "GET",
                url: `https://api.bgm.tv/v0/persons/${personId}`,
                headers: {
                    "Accept": "application/json",
                    "User-Agent": "BangumiStaffInfoTooltip/1.0.8"
                },
                onload: function(response) {
                    // 确保这是当前需要的数据
                    if (currentPersonId !== personId) return;

                    try {
                        const personData = JSON.parse(response.responseText);

                        GM_xmlhttpRequest({
                            method: "GET",
                            url: `https://api.bgm.tv/v0/persons/${personId}/subjects?limit=${Math.min(100, MAX_WORKS_TO_SHOW + 10)}`,
                            headers: {
                                "Accept": "application/json",
                                "User-Agent": "BangumiStaffInfoTooltip/1.0.8"
                            },
                            onload: function(response) {
                                // 确保这是当前需要的数据
                                if (currentPersonId !== personId) return;

                                try {
                                    const worksData = JSON.parse(response.responseText);
                                    const combinedData = {
                                        person: personData,
                                        works: worksData
                                    };
                                    cache.set(`person_${personId}`, combinedData);
                                    displayPersonInfo(combinedData, personName, personUrl, linkElement, mouseX, mouseY);
                                } catch (e) {
                                    console.error("Error parsing works data:", e, response.responseText);
                                    showTooltip('<div class="error">解析作品数据时出错</div>', linkElement, mouseX, mouseY);
                                }
                            },
                            onerror: function(error) {
                                if (currentPersonId !== personId) return;
                                console.error("Works request failed:", error);
                                showTooltip('<div class="error">获取作品信息失败</div>', linkElement, mouseX, mouseY);
                            }
                        });
                    } catch (e) {
                        if (currentPersonId !== personId) return;
                        console.error("Error parsing person data:", e, response.responseText);
                        showTooltip('<div class="error">解析人物数据时出错</div>', linkElement, mouseX, mouseY);
                    }
                },
                onerror: function(error) {
                    if (currentPersonId !== personId) return;
                    console.error("Person request failed:", error);
                    showTooltip('<div class="error">获取人物信息失败</div>', linkElement, mouseX, mouseY);
                }
            });
        }, delay);
    }

    function displayPersonInfo(data, personName, personUrl, linkElement, mouseX, mouseY) {
         // 再次确认是为当前链接显示
         if (currentLink !== linkElement) return;

        if (!data || !data.person || !data.works) {
            showTooltip('<div class="error">数据不完整</div>', linkElement, mouseX, mouseY);
            return;
        }

        const person = data.person;
        let works = data.works;

        if (!Array.isArray(works)) {
            works = [];
        }

        const sortedWorks = sortWorksByRatingCount(works);
        const totalWorks = works.length;
        const worksToShowCount = determineWorksToShowCount(totalWorks);
        const worksToShow = sortedWorks.slice(0, worksToShowCount);

        const nameToShow = person.name_cn || person.name || personName;
        const nameLinkHtml = `<a href="${personUrl}" target="_blank" class="tooltip-name-link">${nameToShow}</a>`;
        const bio = person.summary ? `<div class="tooltip-bio">${person.summary.substring(0, 200)}${person.summary.length > 200 ? '...' : ''}</div>` : '';

        let worksHtml = '';
        if (worksToShow.length > 0) {
            worksToShow.forEach(work => {
                const title = work.name_cn || work.name || '未知标题';
                const year = formatDate(work.date);
                const role = work.staff || '未知';
                let coverUrl = '';
                if (work.images && work.images[IMAGE_SIZE]) {
                     coverUrl = work.images[IMAGE_SIZE];
                } else if (work.image) {
                     coverUrl = work.image;
                }
                const subjectUrl = `https://bgm.tv/subject/${work.id}`;

                let ratingCountText = '';
                if (work.rating && typeof work.rating.total === 'number') {
                    ratingCountText = `<span class="tooltip-work-rating-count">${work.rating.total}人评分</span>`;
                }

                worksHtml += `
                    <li class="tooltip-work-item">
                        <div class="tooltip-work-cover-container">
                            ${coverUrl ?
                              `<img src="${coverUrl}" alt="${title} 封面" class="tooltip-work-cover" loading="lazy">` :
                              ''}
                        </div>
                        <div class="tooltip-work-content">
                            <div class="tooltip-work-title-row">
                                <a href="${subjectUrl}" target="_blank" class="tooltip-work-title">${title}</a>
                            </div>
                            <div class="tooltip-work-meta">
                                ${year !== 'N/A' ? `<span class="tooltip-work-year">${year}</span>` : ''}
                                ${ratingCountText}
                            </div>
                            <div class="tooltip-work-role">${role}</div>
                        </div>
                    </li>
                `;
            });
        } else {
            worksHtml = '<li>暂无作品信息</li>';
        }

        // *** 修改7: 用新的滚动容器包装作品列表 ***
        const htmlContent = `
            <div class="tooltip-header">
                <div class="tooltip-name">${nameLinkHtml}</div>
                <span class="tooltip-stats">(共 ${totalWorks} 部作品)</span>
            </div>
            ${bio}
            <div class="tooltip-works-scroll-container">
                <ul class="tooltip-works-list">
                    ${worksHtml}
                </ul>
            </div>
        `;

        showTooltip(htmlContent, linkElement, mouseX, mouseY);
    }

    function init() {
        createTooltip();

        // 使用防抖处理 mouseover 事件
        document.addEventListener('mouseover', debounce(function(e) {
            const link = e.target.closest('a[href*="/person/"]');
            if (link) {
                const href = link.getAttribute('href');
                const match = href.match(/\/person\/(\d+)/);
                if (match) {
                    const personId = match[1];
                    const personName = link.textContent.trim() || '未知';
                    const personUrl = link.href;

                    // 计算鼠标位置
                    const x = e.clientX;
                    const y = e.clientY;

                    // 如果移入了新的链接,立即取消之前的隐藏和显示计划
                    if (currentLink && currentLink !== link) {
                        if (hideTimeout) {
                            clearTimeout(hideTimeout);
                            hideTimeout = null;
                        }
                        if (showTimeout) {
                            clearTimeout(showTimeout);
                            showTimeout = null;
                        }
                        // 立即隐藏旧的 Tooltip
                        if (tooltip) {
                             tooltip.style.display = 'none';
                             // 移除旧的滚动监听器
                             const oldScrollContainer = tooltip.querySelector('.tooltip-works-scroll-container');
                             if (oldScrollContainer) {
                                 oldScrollContainer.removeEventListener('wheel', handleScrollWheel);
                             }
                        }
                        if (currentFetchRequest) {
                            currentFetchRequest.abort();
                            currentFetchRequest = null;
                        }
                        // 重置旧状态
                        currentLink = null;
                        currentPersonId = null;
                    }

                    // 更新当前链接(即使相同也需要,因为可能是从 Tooltip 移回)
                    currentLink = link;
                    fetchPersonInfo(personId, personName, personUrl, link, x, y);
                }
            }
        }, 100)); // 100ms 防抖

        // 精细化处理 mouseout 事件
        document.addEventListener('mouseout', function(e) {
            const from = e.target;
            const to = e.relatedTarget;

            const fromLink = from.closest('a[href*="/person/"]');
            const toLink = to ? to.closest('a[href*="/person/"]') : null;
            const isTooltipTarget = tooltip && tooltip.contains(to);

            // 情况1: 鼠标从当前 Staff 链接移出
            if (fromLink && fromLink === currentLink) {
                // 并且没有移到 Tooltip 上,也没有移到另一个 Staff 链接上
                if (!isTooltipTarget && toLink !== currentLink) {
                    scheduleHideTooltip();
                }
                // 如果移到了 Tooltip 或另一个链接,则不处理,让对应元素的 mouseenter 来处理
            }

            // 情况2: 鼠标从 Tooltip 移出
            // 这个逻辑已经在 Tooltip 的 mouseleave 事件监听器中处理了
        });

    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

})();