打开/关闭菜单
打开/关闭外观设置菜单
打开/关闭个人菜单
未登录
未登录用户的IP地址会在进行任意编辑后公开展示。

MediaWiki:WikiAI.js

MediaWiki界面页面

注意:在发布之后,您可能需要清除浏览器缓存才能看到所作出的更改的影响。

  • Firefox或Safari:按住Shift的同时单击刷新,或按Ctrl-F5Ctrl-R(Mac为⌘-R
  • Google Chrome:Ctrl-Shift-R(Mac为⌘-Shift-R
  • Edge:按住Ctrl的同时单击刷新,或按Ctrl-F5

(function() {
    'use strict';
    
    // 配置
    const CONFIG = {
        iframeSrc: window.ECWikiAIConfig?.src || 'https://service.wiki.easecation.net/ai',
        fabPosition: window.ECWikiAIConfig?.position || { bottom: '64px', right: '64px' },
        mobileBreakpoint: 640,
        debounceDelay: 150
    };
    
    // 防止重复初始化
    if (document.getElementById('ec-ai-fab')) {
        console.log('[EC Wiki AI] 已初始化,跳过');
        return;
    }
    
    // AI 图标 SVG
    const AI_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" style="width:20px;height:20px;vertical-align:middle;"><path d="M15.9991 2.99995C19.3131 2.99995 22 5.69516 22 8.9941V21H8.00099C4.68693 21 2.00001 18.3048 2.00001 15.0058V11H4.00001V15.0058C4.00001 17.2042 5.79547 19 8.00099 19H20V8.9941C20 6.79576 18.2046 4.99995 15.9991 4.99995H10V2.99995H15.9991ZM10 13H8.00002V11H10V13ZM16 13H14V11H16V13ZM3.52931 1.31928C3.70584 0.89349 4.29418 0.893492 4.47071 1.31928L4.72364 1.93061C5.15555 2.9734 5.96155 3.80612 6.97462 4.25679L7.6924 4.57612C8.10268 4.75894 8.10263 5.35615 7.6924 5.53902L6.93263 5.87691C5.94498 6.31619 5.15339 7.11941 4.71388 8.12789L4.46681 8.69332C4.28636 9.10745 3.71366 9.10745 3.53321 8.69332L3.28614 8.12789C2.84661 7.11942 2.05506 6.31619 1.06739 5.87691L0.307623 5.53902C-0.102517 5.35615 -0.102565 4.75894 0.307623 4.57612L1.0254 4.25679C2.03845 3.80613 2.84446 2.97343 3.27638 1.93061L3.52931 1.31928Z"></path></svg>`;
    
    // 判断是否为移动端
    const isMobile = () => window.innerWidth <= CONFIG.mobileBreakpoint;
    
    // 防抖函数
    const debounce = (fn, delay) => {
        let timer = null;
        return function(...args) {
            clearTimeout(timer);
            timer = setTimeout(() => fn.apply(this, args), delay);
        };
    };
    
    // 存储事件处理器引用,用于清理
    const eventHandlers = {};
    
    // 创建样式
    const style = document.createElement('style');
    style.textContent = `
        #ec-ai-fab {
            position: fixed;
            bottom: ${CONFIG.fabPosition.bottom};
            right: ${CONFIG.fabPosition.right};
            width: 48px;
            height: 48px;
            padding: 0;
            background: #206588;
            color: #fff;
            border: none;
            border-radius: 14px;
            cursor: pointer;
            font-size: 14px;
            font-weight: 600;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            box-shadow: 0 2px 8px rgba(0,0,0,0.15);
            z-index: 10000;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: opacity 0.4s ease, transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
            user-select: none;
            opacity: 0;
        }
        
        #ec-ai-fab.visible {
            opacity: 1;
        }
        
        #ec-ai-fab:hover {
            background: #0090e0;
            transform: translateY(-2px);
            box-shadow: 0 4px 12px rgba(0,0,0,0.2);
        }
        
        #ec-ai-fab:active {
            transform: translateY(0);
        }
        
        /* 移动端悬浮按钮适配 */
        @media (max-width: ${CONFIG.mobileBreakpoint}px) {
            #ec-ai-fab {
                bottom: 80px;
                right: 20px;
                width: 40px;
                height: 40px;
                padding: 0;
            }
        }
        
        /* 遮罩层 */
        #ec-ai-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.4);
            z-index: 10001;
            opacity: 0;
            visibility: hidden;
            transition: all 0.3s ease;
        }
        
        #ec-ai-overlay.active {
            opacity: 1;
            visibility: visible;
        }
        
        /* iframe 容器 - 桌面端右侧滑出 */
        #ec-ai-sidebar {
            position: fixed;
            top: 0;
            right: 0;
            width: 500px;
            height: 100vh;
            height: 100dvh;
            background: #fff;
            border-left: 1px solid #e1e4e8;
            box-shadow: -2px 0 8px rgba(0,0,0,0.08);
            z-index: 10002;
            display: flex;
            flex-direction: column;
            transform: translateX(100%);
            opacity: 0;
            transition: transform 0.3s ease, opacity 0.3s ease;
        }
        
        #ec-ai-sidebar.mobile {
            width: 100%;
            height: 100vh;
            height: 100dvh;
            border-left: none;
            /* 适配底部安全区域 */
            padding-bottom: env(safe-area-inset-bottom);
        }
        
        #ec-ai-sidebar.active {
            transform: translateX(0);
            opacity: 1;
        }
        
        /* 始终预留滚动条空间,防止隐藏时出现布局抖动 */
        html {
            scrollbar-gutter: stable;
        }
        
        /* 头部栏 */
        #ec-ai-header {
            background: #fff;
            border-bottom: 1px solid #e1e4e8;
            padding: 12px 16px;
            display: flex;
            justify-content: space-between;
            align-items: center;
            flex-shrink: 0;
            height: 56px;
        }
        
        #ec-ai-title {
            font-size: 16px;
            font-weight: 600;
            color: #24292e;
            display: flex;
            align-items: center;
            gap: 8px;
        }
        
        /* 关闭按钮 - 始终显示在右上角 */
        #ec-ai-close {
            width: 32px;
            height: 32px;
            border: none;
            background: transparent;
            color: #666;
            cursor: pointer;
            border-radius: 6px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 18px;
            font-weight: 300;
            transition: all 0.2s;
        }
        
        #ec-ai-close:hover {
            background: #f5f7fa;
            color: #24292e;
        }
        
        /* 关闭按钮文字(X) */
        #ec-ai-close::before {
            content: "×";
            line-height: 1;
        }
        
        /* iframe 容器 */
        #ec-ai-iframe-container {
            flex: 1;
            position: relative;
            background: #f5f7fa;
            /* 移动端适配底部安全区域 */
            padding-bottom: env(safe-area-inset-bottom);
        }
        
        #ec-ai-iframe {
            width: 100%;
            height: 100%;
            border: none;
            display: block;
        }
        
        /* 加载指示器 */
        #ec-ai-loader {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            display: flex;
            gap: 4px;
        }
        
        #ec-ai-loader span {
            width: 8px;
            height: 8px;
            background: #206588;
            border-radius: 50%;
            animation: ec-ai-pulse 1.4s infinite;
        }
        
        #ec-ai-loader span:nth-child(2) { animation-delay: 0.2s; }
        #ec-ai-loader span:nth-child(3) { animation-delay: 0.4s; }
        
        @keyframes ec-ai-pulse {
            0%, 100% { opacity: 0.4; transform: scale(1); }
            50% { opacity: 1; transform: scale(1.2); }
        }
        
        /* 加载错误提示 */
        #ec-ai-loader .ec-ai-error {
            text-align: center;
            padding: 20px;
            color: #666;
            font-size: 14px;
        }
        
        /* 滚动条样式 */
        #ec-ai-sidebar ::-webkit-scrollbar {
            width: 6px;
            height: 6px;
        }
        
        #ec-ai-sidebar ::-webkit-scrollbar-track {
            background: transparent;
        }
        
        #ec-ai-sidebar ::-webkit-scrollbar-thumb {
            background: #64acc4;
            border-radius: 3px;
        }
        
        #ec-ai-sidebar ::-webkit-scrollbar-thumb:hover {
            background: #4a8fa6;
        }
        
        #ec-ai-sidebar {
            scrollbar-width: thin;
            scrollbar-color: #64acc4 transparent;
        }
        
        /* 深色模式 - 合并所有深色样式 */
        @media (prefers-color-scheme: dark) {
            /* 滚动条 */
            #ec-ai-sidebar ::-webkit-scrollbar-thumb {
                background: #28494f;
            }
            
            #ec-ai-sidebar ::-webkit-scrollbar-thumb:hover {
                background: #3a6b73;
            }
            
            #ec-ai-sidebar {
                scrollbar-color: #28494f transparent;
            }
            
            /* 侧边栏 */
            #ec-ai-sidebar {
                background: #1a1a1a;
                border-left-color: #444;
            }
            
            #ec-ai-header {
                background: #2a2a2a;
                border-bottom-color: #444;
            }
            
            #ec-ai-title {
                color: #e0e0e0;
            }
            
            #ec-ai-close {
                color: #aaa;
            }
            
            #ec-ai-close:hover {
                background: #333;
                color: #e0e0e0;
            }
            
            #ec-ai-iframe-container {
                background: #1a1a1a;
            }
            
            #ec-ai-loader span {
                background: #64b5f6;
            }
            
            #ec-ai-loader .ec-ai-error {
                color: #aaa;
            }
        }
    `;
    document.head.appendChild(style);
    
    // 创建悬浮按钮
    const fab = document.createElement('button');
    fab.id = 'ec-ai-fab';
    fab.innerHTML = AI_ICON_SVG;
    fab.title = '打开 AI 助手';
    fab.setAttribute('aria-label', '打开 AI 助手');
    fab.addEventListener('click', openSidebar);
    document.body.appendChild(fab);
    
    // 触发淡入动画
    requestAnimationFrame(() => {
        fab.classList.add('visible');
    });
    
    // 创建遮罩层
    const overlay = document.createElement('div');
    overlay.id = 'ec-ai-overlay';
    overlay.addEventListener('click', closeSidebar);
    document.body.appendChild(overlay);
    
    // 创建侧边栏容器
    const sidebar = document.createElement('div');
    sidebar.id = 'ec-ai-sidebar';
    
    // 根据当前设备类型设置初始类名
    if (isMobile()) {
        sidebar.classList.add('mobile');
    }
    
    // 创建头部
    const header = document.createElement('div');
    header.id = 'ec-ai-header';
    
    const titleDiv = document.createElement('div');
    titleDiv.id = 'ec-ai-title';
    titleDiv.innerHTML = 'EaseCation Wiki AI';
    
    const closeBtn = document.createElement('button');
    closeBtn.id = 'ec-ai-close';
    closeBtn.title = '关闭';
    closeBtn.setAttribute('aria-label', '关闭');
    closeBtn.addEventListener('click', closeSidebar);
    
    header.appendChild(titleDiv);
    header.appendChild(closeBtn);
    sidebar.appendChild(header);
    
    // 创建 iframe 容器
    const iframeContainer = document.createElement('div');
    iframeContainer.id = 'ec-ai-iframe-container';
    
    // 创建加载动画
    const loader = document.createElement('div');
    loader.id = 'ec-ai-loader';
    loader.innerHTML = '<span></span><span></span><span></span>';
    iframeContainer.appendChild(loader);
    
    // 创建 iframe(懒加载)
    let iframe = null;
    
    sidebar.appendChild(iframeContainer);
    document.body.appendChild(sidebar);
    
    // 监听窗口大小变化(带防抖)
    eventHandlers.resize = debounce(function() {
        if (isMobile()) {
            sidebar.classList.add('mobile');
        } else {
            sidebar.classList.remove('mobile');
        }
    }, CONFIG.debounceDelay);
    window.addEventListener('resize', eventHandlers.resize);
    
    // 键盘快捷键支持
    eventHandlers.keydown = function(e) {
        // ESC 关闭
        if (e.key === 'Escape' && sidebar.classList.contains('active')) {
            closeSidebar();
        }
        // 开启快捷键: Ctrl/Cmd + Shift + A
        if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'A') {
            e.preventDefault();
            openSidebar();
        }
    };
    document.addEventListener('keydown', eventHandlers.keydown);
    
    // 监听系统主题变化并通知 iframe
    eventHandlers.themeChange = function(e) {
        if (iframe && iframe.contentWindow) {
            try {
                iframe.contentWindow.postMessage({
                    type: 'theme-change',
                    prefersDark: e.matches
                }, '*');
            } catch (err) {
                // 忽略跨域错误
            }
        }
    };
    const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');
    if (darkModeQuery.addEventListener) {
        darkModeQuery.addEventListener('change', eventHandlers.themeChange);
    } else if (darkModeQuery.addListener) {
        // 兼容旧浏览器
        darkModeQuery.addListener(eventHandlers.themeChange);
    }
    
    // 打开侧边栏
    function openSidebar() {
        sidebar.classList.toggle('mobile', isMobile());
        document.body.style.overflow = 'hidden';
        sidebar.classList.add('active');
        overlay.classList.add('active');
        
        // 首次加载 iframe
        if (!iframe) {
            iframe = document.createElement('iframe');
            iframe.id = 'ec-ai-iframe';
            iframe.src = CONFIG.iframeSrc;
            iframe.title = 'EaseCation Wiki AI 助手';
            iframe.setAttribute('allow', 'fullscreen');
            iframe.setAttribute('sandbox', 'allow-same-origin allow-scripts allow-popups allow-forms');
            
            // 加载完成后隐藏 loader
            eventHandlers.iframeLoad = function() {
                loader.style.display = 'none';
                // 发送当前主题
                if (iframe.contentWindow) {
                    try {
                        iframe.contentWindow.postMessage({
                            type: 'theme-change',
                            prefersDark: darkModeQuery.matches
                        }, '*');
                    } catch (err) {
                        // 忽略跨域错误
                    }
                }
            };
            iframe.addEventListener('load', eventHandlers.iframeLoad);
            
            // 加载失败处理
            eventHandlers.iframeError = function() {
                loader.innerHTML = '<div class="ec-ai-error">加载失败,请刷新重试</div>';
            };
            iframe.addEventListener('error', eventHandlers.iframeError);
            
            iframeContainer.appendChild(iframe);
        }
    }
    
    // 关闭侧边栏
    function closeSidebar() {
        sidebar.classList.remove('active');
        overlay.classList.remove('active');
        document.body.style.overflow = '';
    }
    
    // 清理方法
    function destroy() {
        // 移除事件监听
        window.removeEventListener('resize', eventHandlers.resize);
        document.removeEventListener('keydown', eventHandlers.keydown);
        if (darkModeQuery.removeEventListener) {
            darkModeQuery.removeEventListener('change', eventHandlers.themeChange);
        } else if (darkModeQuery.removeListener) {
            darkModeQuery.removeListener(eventHandlers.themeChange);
        }
        
        if (iframe) {
            iframe.removeEventListener('load', eventHandlers.iframeLoad);
            iframe.removeEventListener('error', eventHandlers.iframeError);
        }
        
        // 移除 DOM 元素
        style.remove();
        fab.remove();
        overlay.remove();
        sidebar.remove();
        
        // 清理 body overflow
        document.body.style.overflow = '';
        
        // 清理全局接口
        delete window.ECWikiAI;
        
        console.log('[EC Wiki AI] 已销毁');
    }
    
    // 全局接口
    window.ECWikiAI = {
        open: openSidebar,
        close: closeSidebar,
        toggle: function() {
            if (sidebar.classList.contains('active')) {
                closeSidebar();
            } else {
                openSidebar();
            }
        },
        isOpen: function() {
            return sidebar.classList.contains('active');
        },
        destroy: destroy
    };
    
    console.log('[EC Wiki AI] 加载成功');
})();