虎码字根表增强

虎码字根表五种显示方案切换,支持搜索高亮和键盘导航

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         虎码字根表增强
// @namespace    http://tampermonkey.net/
// @version      0.0.1
// @description  虎码字根表五种显示方案切换,支持搜索高亮和键盘导航
// @author 小明
// @license MIT
// @match        https://www.tiger-code.com/docs/comparisonTable
// @icon         https://www.tiger-code.com/favicon.ico
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    // 添加样式
    GM_addStyle(`
        .scheme-selector {
            position: fixed;
            top: 20px;
            right: 20px;
            background: white;
            padding: 15px;
            border-radius: 10px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.15);
            z-index: 10000;
            min-width: 300px;
            border: 1px solid #e0e0e0;
        }

        .slider-container {
            display: flex;
            background: #f5f5f5;
            border-radius: 25px;
            padding: 4px;
            margin-bottom: 15px;
            position: relative;
            transition: all 0.3s ease;
        }

        .slider-container:hover {
            background: #ebebeb;
        }

        .slider-track {
            position: absolute;
            top: 4px;
            bottom: 4px;
            background: #409eff;
            border-radius: 20px;
            transition: all 0.3s ease;
            z-index: 1;
        }

        .scheme-btn {
            flex: 1;
            padding: 8px 12px;
            border: none;
            background: none;
            border-radius: 20px;
            cursor: pointer;
            font-size: 12px;
            font-weight: 500;
            z-index: 2;
            position: relative;
            transition: color 0.3s ease;
        }

        .scheme-btn.active {
            color: white;
        }

        .switch-row {
            display: flex;
            align-items: center;
            margin-bottom: 10px;
            gap: 10px;
        }

        .search-input {
            flex: 1;
            padding: 8px 12px;
            border: 1px solid #dcdfe6;
            border-radius: 4px;
            outline: none;
            transition: border-color 0.3s;
        }

        .search-input:focus {
            border-color: #409eff;
        }

        .search-results {
            max-height: 200px;
            overflow-y: auto;
            border: 1px solid #e0e0e0;
            border-radius: 4px;
            padding: 10px;
            background: #fafafa;
            display: none;
        }

        .search-result-item {
            padding: 8px;
            border-bottom: 1px solid #eee;
            cursor: pointer;
            transition: background 0.2s;
        }

        .search-result-item:hover, .search-result-item.highlighted {
            background: #e6f7ff;
        }

        .search-result-item.active {
            background: #409eff;
            color: white;
        }

        .custom-switch {
            position: relative;
            display: inline-block;
            width: 60px;
            height: 24px;
        }

        .custom-switch input {
            opacity: 0;
            width: 0;
            height: 0;
        }

        .switch-slider {
            position: absolute;
            cursor: pointer;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background-color: #dcdfe6;
            transition: .4s;
            border-radius: 24px;
        }

        .switch-slider:before {
            position: absolute;
            content: "";
            height: 16px;
            width: 16px;
            left: 4px;
            bottom: 4px;
            background-color: white;
            transition: .4s;
            border-radius: 50%;
        }

        input:checked + .switch-slider {
            background-color: #409eff;
        }

        input:checked + .switch-slider:before {
            transform: translateX(36px);
        }

        .switch-text {
            position: absolute;
            left: 8px;
            right: 8px;
            top: 50%;
            transform: translateY(-50%);
            font-size: 12px;
            color: white;
            text-align: center;
            pointer-events: none;
        }

        .back-to-top {
            position: fixed;
            bottom: 30px;
            right: 30px;
            width: 50px;
            height: 50px;
            background: #409eff;
            color: white;
            border: none;
            border-radius: 50%;
            cursor: pointer;
            z-index: 9999;
            display: none;
            align-items: center;
            justify-content: center;
            font-size: 20px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.2);
            transition: all 0.3s ease;
        }

        .back-to-top:hover {
            background: #337ecc;
            transform: translateY(-2px);
        }

        .serial-number {
            font-weight: bold;
            color: #666;
            min-width: 40px;
            text-align: right;
            padding-right: 10px;
        }

        .empty-row {
            height: 10px;
            background-color: #f8f9fa;
        }

        .search-highlight {
            background-color: #fff3cd !important;
            transition: background-color 0.5s ease;
            box-shadow: 0 0 0 2px #ffc107;
        }

        .search-highlight-active {
            background-color: #409eff !important;
            color: white !important;
            box-shadow: 0 0 0 3px #ffc107;
        }

        .search-info {
            font-size: 12px;
            color: #666;
            margin-top: 5px;
            text-align: center;
        }
    `);

    // 键盘顺序映射
    const keyboardOrder = 'qwertyuiopasdfghjklzxcvbnm';
    const alphabetOrder = 'abcdefghijklmnopqrstuvwxyz';

    let currentScheme = GM_getValue('currentScheme', 1);
    let isSerialEnabled = GM_getValue('isSerialEnabled', currentScheme !== 1);
    let originalTableHTML = null;
    let searchTimeout = null;
    let resultsTimeout = null;
    let currentSearchResults = [];
    let currentSelectedIndex = -1;
    let isSearchActive = false;

    function init() {
        // 等待页面加载完成
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', () => {
                setTimeout(() => {
                    createUI();
                    setTimeout(restoreScheme, 2000);
                }, 5000);
            });
        } else {
            setTimeout(() => {
                createUI();
                setTimeout(restoreScheme, 2000);
            }, 5000);
        }
    }

    function createUI() {
        // 创建方案选择器
        const selector = document.createElement('div');
        selector.className = 'scheme-selector';
        selector.innerHTML = `
            <div class="slider-container">
                <div class="slider-track" style="width: 20%; left: 0%;"></div>
                <button class="scheme-btn active" data-scheme="1">one</button>
                <button class="scheme-btn" data-scheme="2">two</button>
                <button class="scheme-btn" data-scheme="3">three</button>
                <button class="scheme-btn" data-scheme="4">four</button>
                <button class="scheme-btn" data-scheme="5">five</button>
            </div>
            <div class="switch-row">
                <label class="custom-switch">
                    <input type="checkbox" ${isSerialEnabled ? 'checked' : ''}>
                    <span class="switch-slider">
                        <span class="switch-text">序号</span>
                    </span>
                </label>
                <input type="text" class="search-input" placeholder="搜索...">
            </div>
            <div class="search-results"></div>
        `;

        document.body.appendChild(selector);

        // 创建返回顶部按钮
        const backToTop = document.createElement('button');
        backToTop.className = 'back-to-top';
        backToTop.innerHTML = '↑';
        backToTop.title = '返回顶部';
        document.body.appendChild(backToTop);

        // 事件监听
        setupEventListeners(selector, backToTop);
    }

    function setupEventListeners(selector, backToTop) {
        // 方案按钮点击事件
        const buttons = selector.querySelectorAll('.scheme-btn');
        buttons.forEach(btn => {
            btn.addEventListener('click', (e) => {
                const scheme = parseInt(e.target.dataset.scheme);
                switchScheme(scheme);
            });
        });

        // 序号开关事件
        const switchInput = selector.querySelector('input[type="checkbox"]');
        switchInput.addEventListener('change', (e) => {
            if (currentScheme === 1 && e.target.checked) {
                switchScheme(2);
            } else if (currentScheme !== 1 && !e.target.checked) {
                switchScheme(1);
            }
        });

        // 搜索输入事件
        const searchInput = selector.querySelector('.search-input');
        const resultsContainer = selector.querySelector('.search-results');

        searchInput.addEventListener('input', (e) => {
            clearTimeout(searchTimeout);
            clearTimeout(resultsTimeout);

            const query = e.target.value.trim();
            if (query === '') {
                resultsContainer.style.display = 'none';
                clearSearchHighlights();
                isSearchActive = false;
                return;
            }

            searchTimeout = setTimeout(() => {
                performSearch(query, resultsContainer);
                resultsContainer.style.display = 'block';

                resultsTimeout = setTimeout(() => {
                    resultsContainer.style.display = 'none';
                    searchInput.value = '';
                    clearSearchHighlights();
                    isSearchActive = false;
                }, 10000);
            }, 300);
        });

        // 搜索框焦点事件
        searchInput.addEventListener('focus', () => {
            isSearchActive = true;
        });

        searchInput.addEventListener('blur', () => {
            // 延迟设置为非活动状态,以便点击搜索结果时可以处理
            setTimeout(() => {
                isSearchActive = false;
            }, 200);
        });

        // 键盘事件监听
        document.addEventListener('keydown', (e) => {
            if (!isSearchActive || currentSearchResults.length === 0) return;

            if (e.key === 'ArrowDown') {
                e.preventDefault();
                navigateSearchResults(1); // 向下
            } else if (e.key === 'ArrowUp') {
                e.preventDefault();
                navigateSearchResults(-1); // 向上
            } else if (e.key === 'Enter') {
                e.preventDefault();
                if (currentSelectedIndex >= 0 && currentSelectedIndex < currentSearchResults.length) {
                    scrollToResult(currentSelectedIndex);
                }
            } else if (e.key === 'Escape') {
                clearSearchHighlights();
                resultsContainer.style.display = 'none';
                searchInput.value = '';
                isSearchActive = false;
            }
        });

        // 返回顶部按钮事件
        backToTop.addEventListener('click', () => {
            window.scrollTo({ top: 0, behavior: 'smooth' });
        });

        // 滚动显示返回顶部按钮
        window.addEventListener('scroll', () => {
            if (window.pageYOffset > 300) {
                backToTop.style.display = 'flex';
            } else {
                backToTop.style.display = 'none';
            }
        });
    }

    function navigateSearchResults(direction) {
        if (currentSearchResults.length === 0) return;

        // 移除之前的高亮
        const previousIndex = currentSelectedIndex;
        if (previousIndex >= 0 && previousIndex < currentSearchResults.length) {
            const prevRow = currentSearchResults[previousIndex].row;
            if (prevRow) {
                prevRow.classList.remove('search-highlight-active');
                prevRow.classList.add('search-highlight');
            }
        }

        // 计算新的索引
        let newIndex = currentSelectedIndex + direction;
        if (newIndex < 0) newIndex = currentSearchResults.length - 1;
        if (newIndex >= currentSearchResults.length) newIndex = 0;

        currentSelectedIndex = newIndex;

        // 更新搜索结果列表高亮
        updateSearchResultsHighlight();

        // 滚动到选中的结果
        scrollToResult(currentSelectedIndex);
    }

    function updateSearchResultsHighlight() {
        const resultsContainer = document.querySelector('.search-results');
        const resultItems = resultsContainer.querySelectorAll('.search-result-item');

        resultItems.forEach((item, index) => {
            item.classList.remove('active');
            if (index === currentSelectedIndex) {
                item.classList.add('active');
            }
        });
    }

    function scrollToResult(index) {
        if (index < 0 || index >= currentSearchResults.length) return;

        const result = currentSearchResults[index];
        if (result && result.row) {
            // 添加活动高亮
            result.row.classList.remove('search-highlight');
            result.row.classList.add('search-highlight-active');

            // 平滑滚动到该行
            result.row.scrollIntoView({
                behavior: 'smooth',
                block: 'center'
            });
        }
    }

    function clearSearchHighlights() {
        const highlightedRows = document.querySelectorAll('.search-highlight, .search-highlight-active');
        highlightedRows.forEach(row => {
            row.classList.remove('search-highlight', 'search-highlight-active');
        });

        currentSearchResults = [];
        currentSelectedIndex = -1;
    }

    function switchScheme(scheme) {
        currentScheme = scheme;
        isSerialEnabled = scheme !== 1;

        // 更新UI状态
        updateUIState();

        // 保存状态
        GM_setValue('currentScheme', currentScheme);
        GM_setValue('isSerialEnabled', isSerialEnabled);

        // 应用方案
        applyScheme();
    }

    function updateUIState() {
        const selector = document.querySelector('.scheme-selector');
        const buttons = selector.querySelectorAll('.scheme-btn');
        const track = selector.querySelector('.slider-track');
        const switchInput = selector.querySelector('input[type="checkbox"]');

        // 更新按钮状态
        buttons.forEach(btn => {
            btn.classList.remove('active');
            if (parseInt(btn.dataset.scheme) === currentScheme) {
                btn.classList.add('active');
            }
        });

        // 更新滑块位置
        const trackWidth = 20; // 每个按钮占20%
        const trackLeft = (currentScheme - 1) * trackWidth;
        track.style.width = `${trackWidth}%`;
        track.style.left = `${trackLeft}%`;

        // 更新开关状态
        switchInput.checked = isSerialEnabled;
    }

    function applyScheme() {
        const table = document.querySelector('.text-2xl table');
        if (!table) return;

        // 保存原始表格HTML
        if (!originalTableHTML) {
            originalTableHTML = table.outerHTML;
        } else {
            table.outerHTML = originalTableHTML;
        }

        const newTable = document.querySelector('.text-2xl table');
        const tbody = newTable.querySelector('tbody');
        const rows = Array.from(tbody.querySelectorAll('tr'));

        switch(currentScheme) {
            case 1:
                // 方案1:原始表格
                removeSerialNumbers(newTable);
                break;
            case 2:
                // 方案2:添加序号
                addSerialNumbers(newTable, rows, false);
                break;
            case 3:
                // 方案3:添加序号和空行
                addSerialNumbers(newTable, rows, true);
                break;
            case 4:
                // 方案4:键盘顺序,使用旧序号
                reorderByKeyboard(newTable, rows, false);
                break;
            case 5:
                // 方案5:键盘顺序,新序号
                reorderByKeyboard(newTable, rows, true);
                break;
        }
    }

    function addSerialNumbers(table, rows, addEmptyRows) {
        const thead = table.querySelector('thead tr');
        const tbody = table.querySelector('tbody');

        // 添加序号列标题
        if (!thead.querySelector('.serial-number')) {
            const serialTh = document.createElement('th');
            serialTh.className = 'serial-number';
            serialTh.style.textAlign = 'left';
            serialTh.textContent = '序号';
            thead.insertBefore(serialTh, thead.firstChild);
        }

        // 清空tbody
        tbody.innerHTML = '';

        let currentLetter = '';
        let serialNumber = 1;

        rows.forEach((row, index) => {
            const codeCell = row.querySelector('td:nth-child(2)');
            const firstLetter = codeCell.textContent.trim().charAt(0).toLowerCase();

            // 添加空行(方案3)
            if (addEmptyRows && currentLetter && currentLetter !== firstLetter) {
                const emptyRow = document.createElement('tr');
                emptyRow.className = 'empty-row';
                const emptyCell = document.createElement('td');
                emptyCell.colSpan = 5;
                emptyRow.appendChild(emptyCell);
                tbody.appendChild(emptyRow);
            }

            currentLetter = firstLetter;

            // 添加序号
            const serialTd = document.createElement('td');
            serialTd.className = 'serial-number';
            serialTd.textContent = serialNumber.toString().padStart(3, '0');

            const newRow = row.cloneNode(true);
            newRow.insertBefore(serialTd, newRow.firstChild);
            tbody.appendChild(newRow);

            serialNumber++;
        });
    }

    function reorderByKeyboard(table, rows, generateNewSerial) {
        const thead = table.querySelector('thead tr');
        const tbody = table.querySelector('tbody');

        // 添加序号列标题
        if (!thead.querySelector('.serial-number')) {
            const serialTh = document.createElement('th');
            serialTh.className = 'serial-number';
            serialTh.style.textAlign = 'left';
            serialTh.textContent = '序号';
            thead.insertBefore(serialTh, thead.firstChild);
        }

        // 清空tbody
        tbody.innerHTML = '';

        // 按首字母分组
        const groups = {};
        rows.forEach(row => {
            const codeCell = row.querySelector('td:nth-child(2)');
            const firstLetter = codeCell.textContent.trim().charAt(0).toLowerCase();
            if (!groups[firstLetter]) {
                groups[firstLetter] = [];
            }
            groups[firstLetter].push(row);
        });

        let serialNumber = 1;

        // 按键盘顺序处理
        for (let key of keyboardOrder) {
            if (groups[key]) {
                // 添加该字母组的所有行
                groups[key].forEach(row => {
                    const serialTd = document.createElement('td');
                    serialTd.className = 'serial-number';

                    if (generateNewSerial) {
                        serialTd.textContent = serialNumber.toString().padStart(3, '0');
                    } else {
                        // 使用原始序号(需要从原始行中获取)
                        const originalIndex = rows.indexOf(row) + 1;
                        serialTd.textContent = originalIndex.toString().padStart(3, '0');
                    }

                    const newRow = row.cloneNode(true);
                    newRow.insertBefore(serialTd, newRow.firstChild);
                    tbody.appendChild(newRow);

                    serialNumber++;
                });

                // 添加空行(在字母组之间)
                const nextKey = keyboardOrder[keyboardOrder.indexOf(key) + 1];
                if (nextKey && groups[nextKey]) {
                    const emptyRow = document.createElement('tr');
                    emptyRow.className = 'empty-row';
                    const emptyCell = document.createElement('td');
                    emptyCell.colSpan = 5;
                    emptyRow.appendChild(emptyCell);
                    tbody.appendChild(emptyRow);
                }
            }
        }
    }

    function removeSerialNumbers(table) {
        const thead = table.querySelector('thead tr');
        const tbody = table.querySelector('tbody');

        // 移除序号列标题
        const serialTh = thead.querySelector('.serial-number');
        if (serialTh) {
            serialTh.remove();
        }

        // 移除所有行的序号列
        const rows = tbody.querySelectorAll('tr');
        rows.forEach(row => {
            const serialTd = row.querySelector('.serial-number');
            if (serialTd) {
                serialTd.remove();
            }
        });
    }

    function performSearch(query, resultsContainer) {
        const table = document.querySelector('.text-2xl table');
        if (!table) return;

        const rows = table.querySelectorAll('tbody tr');
        const results = [];

        // 先清除之前的高亮
        clearSearchHighlights();

        rows.forEach((row, index) => {
            const cells = row.querySelectorAll('td');
            let rowText = '';
            let hasMatch = false;

            cells.forEach((cell, cellIndex) => {
                // 跳过主根发音列(根据内容特征判断)
                const cellContent = cell.textContent;
                if (!cellContent.includes('ììng') && !cellContent.includes('ēēng')) {
                    rowText += cellContent + ' ';

                    // 检查是否匹配
                    if (cellContent.toLowerCase().includes(query.toLowerCase())) {
                        hasMatch = true;
                    }
                }
            });

            if (hasMatch || rowText.toLowerCase().includes(query.toLowerCase())) {
                // 添加高亮样式
                row.classList.add('search-highlight');

                results.push({
                    index: index + 1,
                    row: row,
                    text: rowText.trim()
                });
            }
        });

        currentSearchResults = results;
        currentSelectedIndex = results.length > 0 ? 0 : -1;

        displaySearchResults(results, resultsContainer, query);

        // 如果有结果,滚动到第一个结果
        if (results.length > 0) {
            scrollToResult(0);
            updateSearchResultsHighlight();
        }
    }

    function displaySearchResults(results, container, query) {
        container.innerHTML = '';

        if (results.length === 0) {
            container.innerHTML = '<div class="search-result-item">未找到匹配结果</div>';
            return;
        }

        // 添加结果数量信息
        const info = document.createElement('div');
        info.className = 'search-info';
        info.textContent = `找到 ${results.length} 个结果,使用 ↑↓ 键导航,Enter 确认,ESC 退出`;
        container.appendChild(info);

        results.forEach((result, index) => {
            const resultItem = document.createElement('div');
            resultItem.className = 'search-result-item';
            if (index === currentSelectedIndex) {
                resultItem.classList.add('active');
            }

            // 高亮匹配文本
            let displayText = result.text;
            const regex = new RegExp(query, 'gi');
            displayText = displayText.replace(regex, match =>
                `<mark style="background: #ffeb3b;">${match}</mark>`
            );

            resultItem.innerHTML = `
                <div style="font-weight: bold; margin-bottom: 4px;">
                    ${currentScheme !== 1 ? `序号: ${result.index.toString().padStart(3, '0')} | ` : ''}
                    ${displayText}
                </div>
            `;

            resultItem.addEventListener('click', () => {
                currentSelectedIndex = index;
                scrollToResult(index);
                updateSearchResultsHighlight();
            });

            container.appendChild(resultItem);
        });
    }

    function restoreScheme() {
        updateUIState();
        applyScheme();
    }

    // 初始化
    init();
})();