// ==UserScript==
// @name POE2 Trade ST工具箱
// @namespace http://tampermonkey.net/
// @version 2.2.1
// @description 自动转换简繁中文(页面转简体,输入转繁体)- stomtian
// @author stomtian
// @match https://www.pathofexile.com/trade*
// @match https://pathofexile.com/trade*
// @grant GM_getValue
// @grant GM_setValue
// @grant unsafeWindow
// @license MIT
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/full.min.js
// @run-at document-end
// @noframes true
// ==/UserScript==
(function() {
'use strict';
console.log('POE2 Trade ST工具箱已加载');
const STATE = {
pageSimplified: GM_getValue('pageSimplified', true),
inputTraditional: GM_getValue('inputTraditional', true),
originalTexts: new WeakMap(),
configs: GM_getValue('savedConfigs', {}), // 保存的配置
autoLoadEnabled: GM_getValue('autoLoadEnabled', false), // 自动加载开关
matchedCards: [], // 添加匹配的卡片列表
currentMatchIndex: -1, // 添加当前匹配索引
showOnlyMatched: GM_getValue('showOnlyMatched', false), // 添加新的状态
searchPresets: GM_getValue('searchPresets', {}) // 添加预设关键词存储
};
const CUSTOM_DICT = [
['回覆', '回復'],
['恢覆', '恢復'],
];
const CONFIG = {
maxAttempts: 50,
checkInterval: 100,
inputSelector: 'input[type="text"]:not(#config-name):not(#config-category), textarea',
textSelector: '.search-bar, .search-advanced-pane, .results-container, .resultset',
excludeSelector: 'script, style, input, textarea, select, .converter-controls'
};
function waitForElement(selector) {
return new Promise(resolve => {
if (document.querySelector(selector)) {
resolve();
return;
}
const observer = new MutationObserver(() => {
try {
if (document.querySelector(selector)) {
observer.disconnect();
resolve();
}
} catch (error) {}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
});
}
function waitForOpenCC() {
return new Promise((resolve, reject) => {
if (typeof window.OpenCC !== 'undefined') {
resolve(window.OpenCC);
return;
}
let attempts = 0;
const checkInterval = setInterval(() => {
if (typeof window.OpenCC !== 'undefined') {
clearInterval(checkInterval);
resolve(window.OpenCC);
return;
}
if (++attempts >= CONFIG.maxAttempts) {
clearInterval(checkInterval);
reject(new Error('OpenCC 加载超时'));
}
}, CONFIG.checkInterval);
});
}
function createConverters(OpenCC) {
const toTraditional = OpenCC.ConverterFactory(
OpenCC.Locale.from.cn,
OpenCC.Locale.to.tw.concat([CUSTOM_DICT])
);
const toSimplified = OpenCC.ConverterFactory(
OpenCC.Locale.from.tw,
OpenCC.Locale.to.cn
);
return { toTraditional, toSimplified };
}
function createInputHandler(converter) {
return function handleInput(e) {
// 如果输入框标记了不需要转换,则直接返回
if (e?.target?.dataset?.noConvert === 'true') return;
if (!STATE.inputTraditional) return;
if (!e?.target?.value) return;
const cursorPosition = e.target.selectionStart;
const text = e.target.value;
requestAnimationFrame(() => {
try {
const convertedText = converter.toTraditional(text);
if (text === convertedText) return;
e.target.value = convertedText;
if (typeof cursorPosition === 'number') {
e.target.setSelectionRange(cursorPosition, cursorPosition);
}
e.target.dispatchEvent(new Event('input', {
bubbles: true,
cancelable: true
}));
} catch (error) {}
});
};
}
function convertPageText(converter, forceRestore = false) {
if (!STATE.pageSimplified && !forceRestore) return;
try {
const elements = document.querySelectorAll(CONFIG.textSelector);
if (!elements.length) return;
elements.forEach(root => {
try {
const walker = document.createTreeWalker(
root,
NodeFilter.SHOW_TEXT,
{
acceptNode: function(node) {
try {
if (!node.textContent.trim()) return NodeFilter.FILTER_REJECT;
const parent = node.parentNode;
if (!parent) return NodeFilter.FILTER_REJECT;
if (parent.closest?.(CONFIG.excludeSelector)) {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
} catch (error) {
return NodeFilter.FILTER_REJECT;
}
}
}
);
let node;
while (node = walker.nextNode()) {
try {
const text = node.textContent.trim();
if (!text) continue;
if (!STATE.originalTexts.has(node)) {
STATE.originalTexts.set(node, text);
}
if (STATE.pageSimplified) {
const convertedText = converter.toSimplified(text);
if (text !== convertedText) {
node.textContent = convertedText;
}
} else {
const originalText = STATE.originalTexts.get(node);
if (originalText && node.textContent !== originalText) {
node.textContent = originalText;
}
}
} catch (error) {}
}
} catch (error) {}
});
} catch (error) {}
}
function attachInputListener(handleInput) {
try {
const inputElements = document.querySelectorAll(CONFIG.inputSelector);
inputElements.forEach(element => {
try {
// 排除搜索框
if (element?.dataset?.hasConverter || element?.dataset?.isSearchInput) return;
element.addEventListener('input', handleInput);
element.dataset.hasConverter = 'true';
} catch (error) {}
});
} catch (error) {}
}
function createObserver(handleInput, converter) {
return new MutationObserver(mutations => {
try {
let needsTextConversion = false;
for (const mutation of mutations) {
if (!mutation.addedNodes.length) continue;
try {
const hasNewInputs = Array.from(mutation.addedNodes).some(node => {
try {
return node.querySelectorAll?.(CONFIG.inputSelector)?.length > 0;
} catch (error) {
return false;
}
});
if (hasNewInputs) {
attachInputListener(handleInput);
}
needsTextConversion = true;
} catch (error) {}
}
if (needsTextConversion) {
setTimeout(() => convertPageText(converter), 100);
}
} catch (error) {}
});
}
function createConfigModal() {
const modalHtml = `
<div id="config-modal" style="display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
background: #1a1a1a; padding: 20px; border-radius: 8px; z-index: 10000; min-width: 600px; color: #fff;">
<div style="display: flex; justify-content: space-between; margin-bottom: 15px;">
<h3 style="margin: 0;">ST工具箱</h3>
<button id="close-config-modal" style="background: none; border: none; color: #fff; cursor: pointer;">✕</button>
</div>
<!-- 标签栏 -->
<div style="display: flex; gap: 10px; margin-bottom: 15px; border-bottom: 1px solid #444; padding-bottom: 10px;">
<button id="tab-configs" class="modal-tab active" style="padding: 8px 20px; background: #4a90e2; border: none; color: #fff; cursor: pointer; border-radius: 4px; transition: all 0.2s;">配置管理</button>
<button id="tab-presets" class="modal-tab" style="padding: 8px 20px; background: #3d3d3d; border: none; color: #fff; cursor: pointer; border-radius: 4px; transition: all 0.2s;">预设关键词</button>
<button id="tab-settings" class="modal-tab" style="padding: 8px 20px; background: #3d3d3d; border: none; color: #fff; cursor: pointer; border-radius: 4px; transition: all 0.2s;">设置</button>
</div>
<!-- 配置管理面板 -->
<div id="panel-configs" class="panel" style="display: block;">
<div style="padding: 15px; background: #2d2d2d; border-radius: 4px;">
<div style="margin-bottom: 15px;">
<input type="text" id="config-name" placeholder="配置名称"
style="padding: 5px; margin-right: 10px; background: #3d3d3d; border: 1px solid #444; color: #fff; width: 200px;">
<div class="custom-select" style="display: inline-block; position: relative; width: 150px; margin-right: 10px;">
<input type="text" id="config-category" placeholder="选择或输入分类"
style="padding: 5px; background: #3d3d3d; border: 1px solid #444; color: #fff; width: 100%; cursor: pointer;">
<div id="category-dropdown" style="display: none; position: absolute; top: 100%; left: 0; width: 100%;
background: #3d3d3d; border: 1px solid #444; border-top: none; max-height: 200px; overflow-y: auto; z-index: 1000;">
</div>
</div>
<button id="save-config" style="padding: 5px 10px; background: #4a90e2; border: none; color: #fff; cursor: pointer; border-radius: 3px;">
保存当前配置
</button>
</div>
<div id="category-tabs" style="margin-bottom: 15px; border-bottom: 1px solid #444; padding-bottom: 10px;"></div>
<div id="config-list" style="max-height: 300px; overflow-y: auto;">
</div>
</div>
</div>
<!-- 设置面板 -->
<div id="panel-settings" class="panel" style="display: none;">
<div style="padding: 15px; background: #2d2d2d; border-radius: 4px;">
<!-- 功能开关 -->
<div style="margin-bottom: 20px;">
<div style="font-weight: bold; margin-bottom: 10px; color: #4a90e2;">功能开关</div>
<div style="display: flex; gap: 10px;">
<button id="toggle-page-simplified" style="flex: 1; padding: 8px 15px; background: ${STATE.pageSimplified ? '#4a90e2' : '#3d3d3d'}; border: none; color: #fff; cursor: pointer; border-radius: 3px; transition: background-color 0.2s; text-align: center;">
${STATE.pageSimplified ? '✓ 页面简体' : '✗ 页面简体'}
</button>
<button id="toggle-input-traditional" style="flex: 1; padding: 8px 15px; background: ${STATE.inputTraditional ? '#4a90e2' : '#3d3d3d'}; border: none; color: #fff; cursor: pointer; border-radius: 3px; transition: background-color 0.2s; text-align: center;">
${STATE.inputTraditional ? '✓ 输入繁体' : '✗ 输入繁体'}
</button>
<button id="toggle-auto-load" style="flex: 1; padding: 8px 15px; background: ${STATE.autoLoadEnabled ? '#4a90e2' : '#3d3d3d'}; border: none; color: #fff; cursor: pointer; border-radius: 3px; transition: background-color 0.2s; text-align: center;">
${STATE.autoLoadEnabled ? '✓ 自动加载' : '✗ 自动加载'}
</button>
</div>
</div>
<!-- 配置导入导出 -->
<div style="margin-top: 20px;">
<div style="font-weight: bold; margin-bottom: 10px; color: #4a90e2;">配置导入导出</div>
<div style="display: flex; gap: 10px;">
<button id="export-configs" style="flex: 1; padding: 8px 15px; background: #27ae60; border: none; color: #fff; cursor: pointer; border-radius: 3px;">导出配置</button>
<button id="import-configs" style="flex: 1; padding: 8px 15px; background: #e67e22; border: none; color: #fff; cursor: pointer; border-radius: 3px;">导入配置</button>
<input type="file" id="import-file" accept=".json" style="display: none;">
</div>
</div>
</div>
</div>
<!-- 预设关键词面板 -->
<div id="panel-presets" class="panel" style="display: none;">
<div style="padding: 15px; background: #2d2d2d; border-radius: 4px;">
<div style="margin-bottom: 15px; display: flex; justify-content: flex-end;">
<button id="add-preset" style="padding: 8px 20px; background: #4a90e2; border: none; color: #fff; cursor: pointer; border-radius: 4px; transition: all 0.2s;">
添加预设
</button>
</div>
<div id="preset-list" style="max-height: 400px; overflow-y: auto;">
</div>
</div>
</div>
</div>
<!-- 预设编辑弹窗 -->
<div id="preset-edit-modal" style="display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
background: #1a1a1a; padding: 20px; border-radius: 8px; z-index: 10002; width: 800px; color: #fff; box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);">
<div style="display: flex; justify-content: space-between; margin-bottom: 15px;">
<h3 style="margin: 0;" id="preset-edit-title">添加预设</h3>
<button id="close-preset-edit" style="background: none; border: none; color: #fff; cursor: pointer; font-size: 20px;">✕</button>
</div>
<div style="margin-bottom: 15px;">
<div style="margin-bottom: 10px;">
<label style="display: block; margin-bottom: 5px;">预设名称</label>
<input type="text" id="preset-name" style="width: 100%; padding: 8px; background: #2d2d2d; border: 1px solid #444; color: #fff; border-radius: 4px;">
</div>
<div>
<label style="display: block; margin-bottom: 5px;">关键词(用;分隔)</label>
<textarea id="preset-keywords" style="width: 100%; height: 200px; padding: 8px; background: #2d2d2d; border: 1px solid #444; color: #fff; border-radius: 4px; resize: vertical; font-family: monospace;"></textarea>
</div>
</div>
<div style="display: flex; justify-content: flex-end; gap: 10px;">
<button id="cancel-preset-edit" style="padding: 8px 20px; background: #3d3d3d; border: none; color: #fff; cursor: pointer; border-radius: 4px;">
取消
</button>
<button id="save-preset" style="padding: 8px 20px; background: #4a90e2; border: none; color: #fff; cursor: pointer; border-radius: 4px;">
保存
</button>
</div>
</div>
<!-- 添加遮罩层 -->
<div id="preset-edit-overlay" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); z-index: 10001;"></div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHtml);
// 添加遮罩
const overlay = document.createElement('div');
overlay.id = 'config-modal-overlay';
overlay.style.cssText = `
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 9999;
`;
document.body.appendChild(overlay);
// 添加样式
const style = document.createElement('style');
style.textContent = `
.modal-tab.active {
background: #4a90e2 !important;
}
.modal-tab:hover {
background: #357abd !important;
}
.panel {
transition: opacity 0.3s ease;
}
#config-list::-webkit-scrollbar {
width: 8px;
}
#config-list::-webkit-scrollbar-track {
background: #1a1a1a;
}
#config-list::-webkit-scrollbar-thumb {
background: #444;
border-radius: 4px;
}
#config-list::-webkit-scrollbar-thumb:hover {
background: #555;
}
`;
document.head.appendChild(style);
setupConfigModalEvents();
updateConfigList();
setupCategoryDropdown();
}
function setupCategoryDropdown() {
const categoryInput = document.getElementById('config-category');
const dropdown = document.getElementById('category-dropdown');
let isDropdownVisible = false;
function updateDropdown() {
const categories = Object.keys(STATE.configs);
const inputValue = categoryInput.value.toLowerCase();
dropdown.innerHTML = '';
categories
.filter(category => category.toLowerCase().includes(inputValue))
.forEach(category => {
const item = document.createElement('div');
item.className = 'dropdown-item';
item.textContent = category;
item.onclick = () => {
categoryInput.value = category;
hideDropdown();
};
dropdown.appendChild(item);
});
if (categories.length === 0) {
const item = document.createElement('div');
item.className = 'dropdown-item';
item.textContent = '无已有分类';
item.style.color = '#666';
dropdown.appendChild(item);
}
}
function showDropdown() {
updateDropdown();
dropdown.style.display = 'block';
isDropdownVisible = true;
}
function hideDropdown() {
dropdown.style.display = 'none';
isDropdownVisible = false;
}
categoryInput.addEventListener('focus', showDropdown);
categoryInput.addEventListener('input', updateDropdown);
// 点击外部区域时隐藏下拉列表
document.addEventListener('click', (e) => {
const isClickInside = categoryInput.contains(e.target) || dropdown.contains(e.target);
if (!isClickInside && isDropdownVisible) {
hideDropdown();
}
});
// 阻止事件冒泡,避免点击下拉列表时触发外部点击事件
dropdown.addEventListener('click', (e) => {
e.stopPropagation();
});
}
function setupConfigModalEvents() {
const modal = document.getElementById('config-modal');
const overlay = document.getElementById('config-modal-overlay');
const closeBtn = document.getElementById('close-config-modal');
const saveBtn = document.getElementById('save-config');
const togglePageBtn = document.getElementById('toggle-page-simplified');
const toggleInputBtn = document.getElementById('toggle-input-traditional');
const toggleAutoLoadBtn = document.getElementById('toggle-auto-load');
const exportBtn = document.getElementById('export-configs');
const importBtn = document.getElementById('import-configs');
const importFile = document.getElementById('import-file');
const tabConfigs = document.getElementById('tab-configs');
const tabSettings = document.getElementById('tab-settings');
const tabPresets = document.getElementById('tab-presets');
const panelConfigs = document.getElementById('panel-configs');
const panelSettings = document.getElementById('panel-settings');
const panelPresets = document.getElementById('panel-presets');
const savePresetBtn = document.getElementById('save-preset');
const addPresetBtn = document.getElementById('add-preset');
const presetEditModal = document.getElementById('preset-edit-modal');
const presetEditOverlay = document.getElementById('preset-edit-overlay');
const closePresetEdit = document.getElementById('close-preset-edit');
const cancelPresetEdit = document.getElementById('cancel-preset-edit');
const presetEditTitle = document.getElementById('preset-edit-title');
// 标签切换函数
function switchTab(activeTab) {
// 重置所有标签和面板
[tabConfigs, tabSettings, tabPresets].forEach(tab => {
tab.classList.remove('active');
tab.style.background = '#3d3d3d';
});
[panelConfigs, panelSettings, panelPresets].forEach(panel => {
panel.style.display = 'none';
});
// 激活选中的标签和面板
activeTab.classList.add('active');
activeTab.style.background = '#4a90e2';
// 显示对应的面板
if (activeTab === tabConfigs) {
panelConfigs.style.display = 'block';
} else if (activeTab === tabSettings) {
panelSettings.style.display = 'block';
} else if (activeTab === tabPresets) {
panelPresets.style.display = 'block';
updatePresetList();
}
}
// 标签切换事件
tabConfigs.addEventListener('click', () => switchTab(tabConfigs));
tabSettings.addEventListener('click', () => switchTab(tabSettings));
tabPresets.addEventListener('click', () => switchTab(tabPresets));
// 初始化显示配置管理标签
switchTab(tabConfigs);
closeBtn.addEventListener('click', () => {
modal.style.display = 'none';
overlay.style.display = 'none';
});
overlay.addEventListener('click', () => {
modal.style.display = 'none';
overlay.style.display = 'none';
});
togglePageBtn.addEventListener('click', () => {
STATE.pageSimplified = !STATE.pageSimplified;
GM_setValue('pageSimplified', STATE.pageSimplified);
togglePageBtn.textContent = STATE.pageSimplified ? '✓ 页面简体' : '✗ 页面简体';
togglePageBtn.style.backgroundColor = STATE.pageSimplified ? '#4a90e2' : '#3d3d3d';
convertPageText(window.converter, true);
});
toggleInputBtn.addEventListener('click', () => {
STATE.inputTraditional = !STATE.inputTraditional;
GM_setValue('inputTraditional', STATE.inputTraditional);
toggleInputBtn.textContent = STATE.inputTraditional ? '✓ 输入繁体' : '✗ 输入繁体';
toggleInputBtn.style.backgroundColor = STATE.inputTraditional ? '#4a90e2' : '#3d3d3d';
});
toggleAutoLoadBtn.addEventListener('click', () => {
STATE.autoLoadEnabled = !STATE.autoLoadEnabled;
GM_setValue('autoLoadEnabled', STATE.autoLoadEnabled);
toggleAutoLoadBtn.textContent = STATE.autoLoadEnabled ? '✓ 自动加载' : '✗ 自动加载';
toggleAutoLoadBtn.style.backgroundColor = STATE.autoLoadEnabled ? '#4a90e2' : '#3d3d3d';
});
saveBtn.addEventListener('click', saveCurrentConfig);
// 修改导出配置
exportBtn.addEventListener('click', () => {
const configData = {
version: '2.0.0',
configs: {},
searchPresets: STATE.searchPresets
};
// 复制配置,但不包含 timestamp
Object.keys(STATE.configs).forEach(category => {
configData.configs[category] = {};
Object.keys(STATE.configs[category]).forEach(name => {
configData.configs[category][name] = {
url: STATE.configs[category][name].url
};
});
});
const blob = new Blob([JSON.stringify(configData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `poe2_trade_configs_${new Date().toISOString().slice(0,10)}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
// 导入配置按钮点击
importBtn.addEventListener('click', () => {
importFile.click();
});
// 处理文件导入
importFile.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
try {
const importedData = JSON.parse(event.target.result);
// 验证导入的数据
if (!importedData.version || (!importedData.configs && !importedData.searchPresets)) {
throw new Error('无效的配置文件格式');
}
// 确认导入
if (confirm(`确定要导入这些配置吗?\n这将会覆盖同名的现有配置和预设关键词。`)) {
// 合并配置
if (importedData.configs) {
Object.keys(importedData.configs).forEach(category => {
if (!STATE.configs[category]) {
STATE.configs[category] = {};
}
Object.assign(STATE.configs[category], importedData.configs[category]);
});
GM_setValue('savedConfigs', STATE.configs);
updateConfigList();
}
// 合并预设关键词
if (importedData.searchPresets) {
Object.assign(STATE.searchPresets, importedData.searchPresets);
GM_setValue('searchPresets', STATE.searchPresets);
updatePresetList();
}
alert('配置导入成功!');
}
} catch (error) {
alert('导入失败:' + error.message);
}
// 清除文件选择,允许重复导入同一个文件
importFile.value = '';
};
reader.readAsText(file);
});
// 添加预设标签切换事件
tabPresets.addEventListener('click', () => {
tabPresets.classList.add('active');
tabConfigs.classList.remove('active');
tabSettings.classList.remove('active');
tabConfigs.style.background = '#3d3d3d';
tabSettings.style.background = '#3d3d3d';
panelConfigs.style.display = 'none';
panelSettings.style.display = 'none';
panelPresets.style.display = 'block';
updatePresetList();
});
// 添加保存预设事件
savePresetBtn.addEventListener('click', saveSearchPreset);
function closePresetEditModal() {
presetEditModal.style.display = 'none';
presetEditOverlay.style.display = 'none';
}
// 打开预设编辑弹窗
addPresetBtn.addEventListener('click', () => {
presetEditModal.style.display = 'block';
presetEditOverlay.style.display = 'block';
presetEditTitle.textContent = '添加预设';
document.getElementById('preset-name').value = '';
document.getElementById('preset-keywords').value = '';
document.getElementById('preset-name').dataset.editMode = 'false';
});
// 关闭预设编辑弹窗
[closePresetEdit, cancelPresetEdit, presetEditOverlay].forEach(btn => {
btn.addEventListener('click', closePresetEditModal);
});
// 添加预设名称和关键词输入框的事件处理
const presetNameInput = document.getElementById('preset-name');
const presetKeywordsInput = document.getElementById('preset-keywords');
// 标记这些输入框不需要转换
presetNameInput.dataset.noConvert = 'true';
presetKeywordsInput.dataset.noConvert = 'true';
// 移除原有的输入转换处理
[presetNameInput, presetKeywordsInput].forEach(input => {
const oldInput = input.cloneNode(true);
input.parentNode.replaceChild(oldInput, input);
});
}
// 修改 saveCurrentConfig 函数
function saveCurrentConfig() {
const name = document.getElementById('config-name').value.trim();
const category = document.getElementById('config-category').value.trim();
if (!name) {
alert('请输入配置名称');
return;
}
if (!category) {
alert('请输入分类名称');
return;
}
if (!STATE.configs[category]) {
STATE.configs[category] = {};
}
STATE.configs[category][name] = {
url: window.location.href
};
GM_setValue('savedConfigs', STATE.configs);
updateConfigList();
document.getElementById('config-name').value = '';
document.getElementById('config-category').value = '';
}
function updateConfigList() {
const configList = document.getElementById('config-list');
const categoryTabs = document.getElementById('category-tabs');
configList.innerHTML = '';
categoryTabs.innerHTML = '';
// 获取所有分类
const categories = Object.keys(STATE.configs);
// 如果没有配置,显示提示信息
if (categories.length === 0) {
configList.innerHTML = '<div style="text-align: center; color: #666;">暂无保存的配置</div>';
return;
}
// 创建标签
categories.forEach((category, index) => {
const tabButton = document.createElement('button');
tabButton.textContent = category;
tabButton.style.cssText = `
background: ${index === 0 ? '#4a90e2' : '#3d3d3d'};
border: none;
color: #fff;
padding: 5px 15px;
cursor: pointer;
border-radius: 3px;
transition: background-color 0.2s;
margin-right: 10px;
`;
tabButton.dataset.category = category;
tabButton.title = '双击删除分类';
tabButton.addEventListener('click', (e) => {
document.querySelectorAll('#category-tabs button[data-category]').forEach(btn => {
btn.style.backgroundColor = '#3d3d3d';
});
tabButton.style.backgroundColor = '#4a90e2';
showCategoryConfigs(category);
});
tabButton.addEventListener('dblclick', (e) => {
e.stopPropagation();
deleteCategory(category);
});
categoryTabs.appendChild(tabButton);
});
// 默认显示第一个分类的配置
showCategoryConfigs(categories[0]);
}
function deleteCategory(category) {
const configCount = Object.keys(STATE.configs[category]).length;
if (confirm(`确定要删除分类 "${category}" 及其包含的 ${configCount} 个配置吗?`)) {
delete STATE.configs[category];
GM_setValue('savedConfigs', STATE.configs);
updateConfigList();
}
}
function showCategoryConfigs(category) {
const configList = document.getElementById('config-list');
configList.innerHTML = '';
const configs = STATE.configs[category];
Object.entries(configs).forEach(([name, data]) => {
const configItem = document.createElement('div');
configItem.style.cssText = `
display: grid;
grid-template-columns: 1fr auto auto auto;
align-items: center;
padding: 8px;
margin: 5px 0;
background: #3d3d3d;
border-radius: 4px;
gap: 10px;
`;
const nameSpan = document.createElement('span');
nameSpan.textContent = name;
nameSpan.style.cssText = `
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
const loadBtn = document.createElement('button');
loadBtn.textContent = '读取';
loadBtn.style.cssText = `
background: #4a90e2;
border: none;
color: #fff;
padding: 3px 12px;
cursor: pointer;
border-radius: 3px;
transition: background-color 0.2s;
`;
loadBtn.onclick = () => loadConfig(data.url);
const updateBtn = document.createElement('button');
updateBtn.textContent = '更新';
updateBtn.style.cssText = `
background: #27ae60;
border: none;
color: #fff;
padding: 3px 12px;
cursor: pointer;
border-radius: 3px;
transition: background-color 0.2s;
`;
updateBtn.onclick = (e) => {
e.stopPropagation();
updateConfig(category, name);
};
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '删除';
deleteBtn.style.cssText = `
background: #e74c3c;
border: none;
color: #fff;
padding: 3px 12px;
cursor: pointer;
border-radius: 3px;
transition: background-color 0.2s;
`;
deleteBtn.onclick = (e) => {
e.stopPropagation();
deleteConfig(category, name);
};
configItem.appendChild(nameSpan);
configItem.appendChild(loadBtn);
configItem.appendChild(updateBtn);
configItem.appendChild(deleteBtn);
configList.appendChild(configItem);
});
}
function loadConfig(url) {
window.location.href = url;
}
function deleteConfig(category, name) {
if (confirm(`确定要删除配置 "${name}" 吗?`)) {
delete STATE.configs[category][name];
if (Object.keys(STATE.configs[category]).length === 0) {
delete STATE.configs[category];
}
GM_setValue('savedConfigs', STATE.configs);
updateConfigList();
}
}
function createConfigButton() {
const floatingButton = document.createElement('div');
floatingButton.style.cssText = `
position: fixed;
right: 20px;
top: 50%;
transform: translateY(-50%);
width: 50px;
height: 50px;
background: linear-gradient(135deg, #3c3c28 0%, #2a2a1c 100%);
border-radius: 25px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: #ffd700;
font-weight: bold;
font-family: 'Fontin SmallCaps', Arial, sans-serif;
font-size: 18px;
box-shadow: 0 0 20px rgba(0,0,0,0.5),
inset 0 0 8px rgba(255, 215, 0, 0.3),
0 0 30px rgba(255, 215, 0, 0.2);
border: 1px solid rgba(255, 215, 0, 0.4);
z-index: 9998;
transition: all 0.3s ease;
user-select: none;
touch-action: none;
text-shadow: 0 0 10px rgba(255, 215, 0, 0.6);
animation: normalGlowing 2s ease-in-out infinite;
`;
floatingButton.textContent = 'ST';
floatingButton.title = 'ST工具箱 (按住可拖动)';
// 添加悬停效果
floatingButton.addEventListener('mouseenter', () => {
if (!isDragging) {
floatingButton.style.transform = 'scale(1.1)';
floatingButton.style.boxShadow = `
0 0 25px rgba(0,0,0,0.5),
inset 0 0 12px rgba(255, 215, 0, 0.5),
0 0 40px rgba(255, 215, 0, 0.3)
`;
floatingButton.style.color = '#ffe44d';
floatingButton.style.textShadow = '0 0 15px rgba(255, 215, 0, 0.8)';
floatingButton.style.border = '1px solid rgba(255, 215, 0, 0.6)';
floatingButton.style.animation = 'none';
if (isHidden) {
showButton();
}
}
});
floatingButton.addEventListener('mouseleave', () => {
if (!isDragging) {
floatingButton.style.transform = 'scale(1)';
floatingButton.style.boxShadow = `
0 0 20px rgba(0,0,0,0.5),
inset 0 0 8px rgba(255, 215, 0, 0.3),
0 0 30px rgba(255, 215, 0, 0.2)
`;
floatingButton.style.color = '#ffd700';
floatingButton.style.textShadow = '0 0 10px rgba(255, 215, 0, 0.6)';
floatingButton.style.border = '1px solid rgba(255, 215, 0, 0.4)';
floatingButton.style.animation = 'normalGlowing 2s ease-in-out infinite';
checkAndHideButton();
}
});
// 添加拖拽功能
let isDragging = false;
let startX, startY;
let lastX = GM_getValue('floatingButtonX', window.innerWidth - 70);
let lastY = GM_getValue('floatingButtonY', window.innerHeight / 2);
let dragDistance = 0;
let mouseDownTime = 0;
let isHidden = false;
function dragStart(e) {
isDragging = true;
dragDistance = 0;
mouseDownTime = Date.now();
const rect = floatingButton.getBoundingClientRect();
startX = e.clientX - rect.left;
startY = e.clientY - rect.top;
floatingButton.style.transition = 'none';
floatingButton.style.transform = 'scale(1)';
}
function drag(e) {
if (!isDragging) return;
e.preventDefault();
const x = e.clientX - startX;
const y = e.clientY - startY;
// 计算拖动距离
const dx = x - lastX;
const dy = y - lastY;
dragDistance += Math.sqrt(dx * dx + dy * dy);
// 限制拖动范围
const maxX = window.innerWidth - floatingButton.offsetWidth;
const maxY = window.innerHeight - floatingButton.offsetHeight;
lastX = Math.max(0, Math.min(x, maxX));
lastY = Math.max(0, Math.min(y, maxY));
floatingButton.style.left = lastX + 'px';
floatingButton.style.top = lastY + 'px';
floatingButton.style.right = 'auto';
}
function dragEnd(e) {
if (!isDragging) return;
const dragDuration = Date.now() - mouseDownTime;
isDragging = false;
floatingButton.style.transition = 'all 0.3s ease';
// 调整位置,使按钮居中对齐边缘
const buttonWidth = floatingButton.offsetWidth;
const buttonHeight = floatingButton.offsetHeight;
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const threshold = 20;
if (lastX < threshold) {
lastX = 0;
} else if (lastX + buttonWidth > windowWidth - threshold) {
lastX = windowWidth - buttonWidth;
}
if (lastY < threshold) {
lastY = 0;
} else if (lastY + buttonHeight > windowHeight - threshold) {
lastY = windowHeight - buttonHeight;
}
floatingButton.style.left = lastX + 'px';
floatingButton.style.top = lastY + 'px';
// 保存位置
GM_setValue('floatingButtonX', lastX);
GM_setValue('floatingButtonY', lastY);
// 检查是否需要隐藏按钮
checkAndHideButton();
// 如果拖动距离小于5像素且时间小于200ms,则认为是点击
if (dragDistance < 5 && dragDuration < 200) {
document.getElementById('config-modal').style.display = 'block';
document.getElementById('config-modal-overlay').style.display = 'block';
}
}
function checkAndHideButton() {
const threshold = 20; // 距离边缘多少像素时触发隐藏
const buttonWidth = floatingButton.offsetWidth;
const buttonHeight = floatingButton.offsetHeight;
const buttonRight = lastX + buttonWidth;
const buttonBottom = lastY + buttonHeight;
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
// 检查各个边缘
if (buttonRight > windowWidth - threshold) {
// 右边缘
hideButton('right');
} else if (lastX < threshold) {
// 左边缘
hideButton('left');
} else if (lastY < threshold) {
// 上边缘
hideButton('top');
} else if (buttonBottom > windowHeight - threshold) {
// 下边缘
hideButton('bottom');
}
}
function hideButton(direction) {
isHidden = true;
floatingButton.style.transition = 'all 0.3s ease';
// 添加金光动画
floatingButton.style.animation = 'none';
floatingButton.offsetHeight; // 触发重绘
floatingButton.style.animation = 'glowing 1.5s ease-in-out infinite';
floatingButton.style.background = 'linear-gradient(135deg, #5a5a42 0%, #3a3a2c 100%)';
switch (direction) {
case 'right':
floatingButton.style.transform = 'translateY(-50%) translateX(60%)';
floatingButton.style.borderRadius = '25px 0 0 25px';
break;
case 'left':
floatingButton.style.transform = 'translateY(-50%) translateX(-60%)';
floatingButton.style.borderRadius = '0 25px 25px 0';
break;
case 'top':
floatingButton.style.transform = 'translateX(-50%) translateY(-60%)';
floatingButton.style.borderRadius = '0 0 25px 25px';
break;
case 'bottom':
floatingButton.style.transform = 'translateX(-50%) translateY(60%)';
floatingButton.style.borderRadius = '25px 25px 0 0';
break;
}
}
function showButton() {
isHidden = false;
floatingButton.style.transition = 'all 0.3s ease';
floatingButton.style.animation = 'normalGlowing 2s ease-in-out infinite';
floatingButton.style.background = 'linear-gradient(135deg, #3c3c28 0%, #2a2a1c 100%)';
floatingButton.style.transform = 'scale(1)';
floatingButton.style.borderRadius = '25px';
}
// 添加金光动画样式
const glowingStyle = document.createElement('style');
glowingStyle.textContent = `
@keyframes normalGlowing {
0% {
box-shadow: 0 0 20px rgba(0,0,0,0.5),
inset 0 0 8px rgba(255, 215, 0, 0.3),
0 0 30px rgba(255, 215, 0, 0.2);
border-color: rgba(255, 215, 0, 0.4);
color: #ffd700;
text-shadow: 0 0 10px rgba(255, 215, 0, 0.6);
}
50% {
box-shadow: 0 0 25px rgba(0,0,0,0.5),
inset 0 0 12px rgba(255, 215, 0, 0.4),
0 0 40px rgba(255, 215, 0, 0.3),
0 0 60px rgba(255, 215, 0, 0.2);
border-color: rgba(255, 215, 0, 0.5);
color: #ffe44d;
text-shadow: 0 0 15px rgba(255, 215, 0, 0.7);
}
100% {
box-shadow: 0 0 20px rgba(0,0,0,0.5),
inset 0 0 8px rgba(255, 215, 0, 0.3),
0 0 30px rgba(255, 215, 0, 0.2);
border-color: rgba(255, 215, 0, 0.4);
color: #ffd700;
text-shadow: 0 0 10px rgba(255, 215, 0, 0.6);
}
}
@keyframes glowing {
0% {
box-shadow: 0 0 20px rgba(0,0,0,0.5),
inset 0 0 8px rgba(255, 215, 0, 0.5),
0 0 30px rgba(255, 215, 0, 0.4),
0 0 50px rgba(255, 215, 0, 0.2);
border-color: rgba(255, 215, 0, 0.6);
color: #ffd700;
text-shadow: 0 0 15px rgba(255, 215, 0, 0.8);
}
50% {
box-shadow: 0 0 30px rgba(0,0,0,0.6),
inset 0 0 20px rgba(255, 215, 0, 0.8),
0 0 60px rgba(255, 215, 0, 0.6),
0 0 100px rgba(255, 215, 0, 0.4),
0 0 150px rgba(255, 215, 0, 0.2);
border-color: rgba(255, 223, 0, 1);
color: #ffe44d;
text-shadow: 0 0 25px rgba(255, 215, 0, 1),
0 0 35px rgba(255, 215, 0, 0.7),
0 0 45px rgba(255, 215, 0, 0.4);
}
100% {
box-shadow: 0 0 20px rgba(0,0,0,0.5),
inset 0 0 8px rgba(255, 215, 0, 0.5),
0 0 30px rgba(255, 215, 0, 0.4),
0 0 50px rgba(255, 215, 0, 0.2);
border-color: rgba(255, 215, 0, 0.6);
color: #ffd700;
text-shadow: 0 0 15px rgba(255, 215, 0, 0.8);
}
}
`;
document.head.appendChild(glowingStyle);
// 监听窗口大小变化
window.addEventListener('resize', () => {
if (!isDragging) {
// 确保按钮不会超出窗口
const maxX = window.innerWidth - floatingButton.offsetWidth;
const maxY = window.innerHeight - floatingButton.offsetHeight;
lastX = Math.min(lastX, maxX);
lastY = Math.min(lastY, maxY);
floatingButton.style.left = lastX + 'px';
floatingButton.style.top = lastY + 'px';
// 检查是否需要隐藏按钮
checkAndHideButton();
}
});
floatingButton.addEventListener('mousedown', dragStart);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
// 恢复上次保存的位置或使用默认位置
const savedX = GM_getValue('floatingButtonX', window.innerWidth - 70);
const savedY = GM_getValue('floatingButtonY', window.innerHeight / 2);
lastX = savedX;
lastY = savedY;
floatingButton.style.right = 'auto';
floatingButton.style.top = lastY + 'px';
floatingButton.style.left = lastX + 'px';
floatingButton.style.transform = 'scale(1)';
// 检查初始位置是否需要隐藏
setTimeout(checkAndHideButton, 100);
return floatingButton;
}
function createControls() {
const floatingButton = createConfigButton();
document.body.appendChild(floatingButton);
// 创建搜索框但不立即显示
createSearchBox();
// 添加快捷键监听
document.addEventListener('keydown', (e) => {
if (e.altKey && !e.ctrlKey && !e.shiftKey && e.key.toLowerCase() === 'f') {
e.preventDefault(); // 阻止默认行为
toggleSearchBox();
}
});
}
// 切换搜索框显示状态
function toggleSearchBox() {
const searchBox = document.querySelector('.search-box-container');
if (searchBox) {
searchBox.style.display = searchBox.style.display === 'none' ? 'flex' : 'none';
if (searchBox.style.display === 'flex') {
// 当显示搜索框时,自动聚焦到输入框
const searchInput = searchBox.querySelector('input');
if (searchInput) {
searchInput.focus();
}
}
}
}
// 创建搜索框
function createSearchBox(handleInput) {
const searchBoxContainer = document.createElement('div');
searchBoxContainer.className = 'search-box-container';
searchBoxContainer.style.cssText = `
position: fixed;
top: 10px;
left: 20px;
z-index: 9999;
background: rgba(28, 28, 28, 0.95);
padding: 15px 10px 10px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
border: 1px solid #444;
display: none;
flex-direction: column;
gap: 8px;
`;
const searchRow = document.createElement('div');
searchRow.style.cssText = `
display: flex;
gap: 8px;
align-items: center;
`;
const navigationRow = document.createElement('div');
navigationRow.style.cssText = `
display: flex;
gap: 8px;
align-items: center;
`;
// 创建搜索输入框
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.placeholder = '输入关键词(用;分隔)';
searchInput.dataset.isSearchInput = 'true';
searchInput.style.cssText = `
width: 250px;
padding: 5px 10px;
border: 1px solid #666;
border-radius: 4px;
background: #2d2d2d;
color: #fff;
`;
// 添加搜索事件
searchInput.addEventListener('input', (e) => {
if (!e?.target?.value) return;
// 如果页面是繁体模式,则将输入转换为繁体
if (!STATE.pageSimplified) {
const cursorPosition = e.target.selectionStart;
const text = e.target.value;
requestAnimationFrame(() => {
try {
const convertedText = window.converter.toTraditional(text);
if (text === convertedText) return;
e.target.value = convertedText;
if (typeof cursorPosition === 'number') {
e.target.setSelectionRange(cursorPosition, cursorPosition);
}
} catch (error) {}
});
}
});
const searchButton = document.createElement('button');
searchButton.textContent = '搜索';
searchButton.style.cssText = `
padding: 5px 15px;
background: #4a90e2;
border: none;
border-radius: 4px;
color: #fff;
cursor: pointer;
transition: background 0.2s;
`;
// 添加预设下拉框
const presetSelectContainer = document.createElement('div');
presetSelectContainer.style.cssText = `
position: relative;
width: 120px;
`;
const presetInput = document.createElement('input');
presetInput.readOnly = true;
presetInput.placeholder = '选择预设';
presetInput.style.cssText = `
width: 100%;
padding: 5px;
background: #2d2d2d;
border: 1px solid #666;
border-radius: 4px;
color: #fff;
cursor: pointer;
`;
// 修改预设选择框的样式
const presetDropdown = document.createElement('div');
presetDropdown.style.cssText = `
display: none;
position: absolute;
top: 100%;
left: 0;
width: 200px;
max-height: 300px;
overflow-y: auto;
background: #2d2d2d;
border: 1px solid #666;
border-radius: 4px;
z-index: 10000;
margin-top: 4px;
padding-top: 30px; // 为搜索框留出空间
color: #fff; // 添加默认文字颜色
`;
// 添加预设搜索框
const presetSearchInput = document.createElement('input');
presetSearchInput.type = 'text';
presetSearchInput.placeholder = '搜索预设...';
presetSearchInput.style.cssText = `
position: absolute;
top: 0;
left: 0;
width: calc(100% - 16px);
margin: 8px;
padding: 4px 8px;
background: #3d3d3d;
border: 1px solid #666;
border-radius: 3px;
color: #fff;
font-size: 12px;
`;
// 阻止搜索框的点击事件冒泡,避免关闭下拉框
presetSearchInput.addEventListener('click', (e) => {
e.stopPropagation();
});
// 添加搜索框的输入事件
presetSearchInput.addEventListener('input', (e) => {
const searchText = e.target.value.toLowerCase();
const options = presetDropdown.querySelectorAll('.preset-option');
options.forEach(option => {
const name = option.querySelector('span').textContent.toLowerCase();
if (name.includes(searchText)) {
option.style.display = '';
} else {
option.style.display = 'none';
}
});
});
presetDropdown.appendChild(presetSearchInput);
// 添加滚动条样式
const styleSheet = document.createElement('style');
styleSheet.textContent = `
.preset-dropdown::-webkit-scrollbar {
width: 8px;
}
.preset-dropdown::-webkit-scrollbar-track {
background: #1a1a1a;
}
.preset-dropdown::-webkit-scrollbar-thumb {
background: #444;
border-radius: 4px;
}
.preset-dropdown::-webkit-scrollbar-thumb:hover {
background: #555;
}
`;
document.head.appendChild(styleSheet);
let selectedPresets = new Set();
// 修改预设选项的样式
function updatePresetOptions() {
// 保留搜索框
presetDropdown.innerHTML = '';
presetDropdown.appendChild(presetSearchInput);
presetSearchInput.value = ''; // 清空搜索框
// 创建分隔线
const createDivider = () => {
const divider = document.createElement('div');
divider.style.cssText = `
height: 1px;
background: #666;
margin: 5px 0;
`;
return divider;
};
// 创建预设选项
const createPresetOption = (name, keywords, isSelected) => {
const option = document.createElement('div');
option.className = 'preset-option'; // 添加类名以便搜索
option.style.cssText = `
padding: 6px 10px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s;
color: #fff;
${isSelected ? 'background: #2a4a6d;' : ''}
`;
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = isSelected;
checkbox.style.cssText = `
margin: 0;
cursor: pointer;
`;
const label = document.createElement('span');
label.textContent = name;
label.style.cssText = `
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #fff;
`;
option.appendChild(checkbox);
option.appendChild(label);
option.addEventListener('mouseover', () => {
option.style.backgroundColor = isSelected ? '#2a5a8d' : '#3d3d3d';
});
option.addEventListener('mouseout', () => {
option.style.backgroundColor = isSelected ? '#2a4a6d' : 'transparent';
});
option.addEventListener('click', (e) => {
e.stopPropagation();
checkbox.checked = !checkbox.checked;
if (checkbox.checked) {
selectedPresets.add(name);
} else {
selectedPresets.delete(name);
}
updateSelectedPresets();
// 重新渲染预设列表以更新顺序
updatePresetOptions();
});
return option;
};
// 获取所有预设并分类
const selectedOptions = [];
const unselectedOptions = [];
Object.entries(STATE.searchPresets).forEach(([name, keywords]) => {
const isSelected = selectedPresets.has(name);
const option = createPresetOption(name, keywords, isSelected);
if (isSelected) {
selectedOptions.push(option);
} else {
unselectedOptions.push(option);
}
});
// 添加已选择的预设
if (selectedOptions.length > 0) {
const selectedTitle = document.createElement('div');
selectedTitle.style.cssText = `
padding: 5px 10px;
color: #999;
font-size: 12px;
background: #262626;
`;
selectedTitle.textContent = '已选择的预设';
presetDropdown.appendChild(selectedTitle);
selectedOptions.forEach(option => presetDropdown.appendChild(option));
}
// 添加未选择的预设
if (selectedOptions.length > 0 && unselectedOptions.length > 0) {
presetDropdown.appendChild(createDivider());
}
if (unselectedOptions.length > 0) {
if (selectedOptions.length > 0) {
const unselectedTitle = document.createElement('div');
unselectedTitle.style.cssText = `
padding: 5px 10px;
color: #999;
font-size: 12px;
background: #262626;
`;
unselectedTitle.textContent = '未选择的预设';
presetDropdown.appendChild(unselectedTitle);
}
unselectedOptions.forEach(option => presetDropdown.appendChild(option));
}
}
function updateSelectedPresets() {
if (selectedPresets.size === 0) {
presetInput.value = '';
presetInput.placeholder = '选择预设';
} else {
const names = Array.from(selectedPresets).join(', ');
presetInput.value = names;
presetInput.placeholder = '';
}
}
function applySelectedPresets() {
if (selectedPresets.size === 0) return;
const keywords = Array.from(selectedPresets)
.map(name => STATE.searchPresets[name])
.join(';');
searchInput.value = keywords;
// 手动触发输入转换
if (!STATE.pageSimplified) {
try {
const convertedText = window.converter.toTraditional(keywords);
if (keywords !== convertedText) {
searchInput.value = convertedText;
}
} catch (error) {}
}
// 触发搜索
performSearch();
// 清空选择
selectedPresets.clear();
updateSelectedPresets();
}
// 点击输入框时显示/隐藏下拉框
presetInput.addEventListener('click', (e) => {
e.stopPropagation();
const isVisible = presetDropdown.style.display === 'block';
presetDropdown.style.display = isVisible ? 'none' : 'block';
if (!isVisible) {
updatePresetOptions();
// 聚焦搜索框
setTimeout(() => {
presetSearchInput.focus();
}, 0);
}
});
// 点击其他地方时隐藏下拉框
document.addEventListener('click', () => {
presetDropdown.style.display = 'none';
});
// 添加搜索事件
const performSearch = () => {
// 获取所有预设关键词
const presetKeywords = Array.from(selectedPresets)
.map(name => STATE.searchPresets[name])
.join(';');
// 获取输入框关键词
const inputKeywords = searchInput.value.trim();
// 合并关键词
const combinedKeywords = [presetKeywords, inputKeywords]
.filter(k => k) // 过滤掉空字符串
.join(';');
// 如果页面是繁体模式,则将关键词转换为繁体
let searchKeywords = combinedKeywords;
if (!STATE.pageSimplified) {
try {
searchKeywords = window.converter.toTraditional(combinedKeywords);
} catch (error) {
console.error('转换繁体失败:', error);
}
}
// 使用;或;作为分隔符
const keywords = searchKeywords.toLowerCase().split(/[;;]/);
// 过滤掉空字符串
const filteredKeywords = keywords.filter(k => k.trim());
if (!filteredKeywords.length) {
clearHighlights();
matchCounter.textContent = '';
return;
}
searchAndHighlight(filteredKeywords, matchCounter);
};
searchInput.addEventListener('keyup', (e) => {
if (e.key === 'Enter') {
performSearch();
}
});
searchButton.addEventListener('click', performSearch);
// 添加导航按钮
const prevButton = document.createElement('button');
prevButton.textContent = '上一个';
prevButton.style.cssText = `
padding: 5px 15px;
background: #2d2d2d;
border: 1px solid #666;
border-radius: 4px;
color: #fff;
cursor: pointer;
transition: background 0.2s;
flex: 1;
`;
const nextButton = document.createElement('button');
nextButton.textContent = '下一个';
nextButton.style.cssText = `
padding: 5px 15px;
background: #2d2d2d;
border: 1px solid #666;
border-radius: 4px;
color: #fff;
cursor: pointer;
transition: background 0.2s;
flex: 1;
`;
const matchCounter = document.createElement('span');
matchCounter.className = 'search-counter';
matchCounter.style.cssText = `
color: #fff;
font-size: 12px;
padding: 0 10px;
min-width: 60px;
text-align: center;
`;
// 添加导航事件
prevButton.addEventListener('click', () => {
navigateHighlight('prev');
});
nextButton.addEventListener('click', () => {
navigateHighlight('next');
});
// 添加hover效果
[searchButton, prevButton, nextButton].forEach(button => {
button.addEventListener('mouseover', () => {
button.style.background = button === searchButton ? '#357abd' : '#3d3d3d';
});
button.addEventListener('mouseout', () => {
button.style.background = button === searchButton ? '#4a90e2' : '#2d2d2d';
});
});
// 组装界面
presetSelectContainer.appendChild(presetInput);
presetSelectContainer.appendChild(presetDropdown);
searchRow.appendChild(presetSelectContainer);
searchRow.appendChild(searchInput);
searchRow.appendChild(searchButton);
navigationRow.appendChild(prevButton);
navigationRow.appendChild(matchCounter);
navigationRow.appendChild(nextButton);
searchBoxContainer.appendChild(searchRow);
searchBoxContainer.appendChild(navigationRow);
// 添加选项行
const optionsRow = document.createElement('div');
optionsRow.style.cssText = `
display: flex;
gap: 8px;
align-items: center;
padding: 0 5px;
`;
const showOnlyMatchedLabel = document.createElement('label');
showOnlyMatchedLabel.style.cssText = `
display: flex;
align-items: center;
gap: 5px;
color: #fff;
font-size: 12px;
cursor: pointer;
`;
const showOnlyMatchedCheckbox = document.createElement('input');
showOnlyMatchedCheckbox.type = 'checkbox';
showOnlyMatchedCheckbox.checked = STATE.showOnlyMatched;
showOnlyMatchedCheckbox.style.cssText = `
margin: 0;
cursor: pointer;
`;
showOnlyMatchedLabel.appendChild(showOnlyMatchedCheckbox);
showOnlyMatchedLabel.appendChild(document.createTextNode('只显示匹配项'));
showOnlyMatchedCheckbox.addEventListener('change', (e) => {
STATE.showOnlyMatched = e.target.checked;
GM_setValue('showOnlyMatched', STATE.showOnlyMatched);
if (STATE.matchedCards.length > 0) {
updateCardVisibility();
}
});
optionsRow.appendChild(showOnlyMatchedLabel);
searchBoxContainer.insertBefore(optionsRow, navigationRow);
document.body.appendChild(searchBoxContainer);
// 添加关闭按钮
const closeButton = document.createElement('button');
closeButton.textContent = '×';
closeButton.style.cssText = `
position: absolute;
top: -10px;
right: -10px;
width: 20px;
height: 20px;
line-height: 1;
padding: 0;
background: #2d2d2d;
border: 1px solid #666;
border-radius: 50%;
color: #999;
font-size: 16px;
font-weight: bold;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
z-index: 10000;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
`;
closeButton.addEventListener('mouseover', () => {
closeButton.style.color = '#fff';
closeButton.style.background = '#3d3d3d';
closeButton.style.borderColor = '#999';
});
closeButton.addEventListener('mouseout', () => {
closeButton.style.color = '#999';
closeButton.style.background = '#2d2d2d';
closeButton.style.borderColor = '#666';
});
closeButton.addEventListener('click', () => {
searchBoxContainer.style.display = 'none';
clearHighlights();
searchInput.value = '';
matchCounter.textContent = '';
// 不清除选择的预设
});
searchBoxContainer.insertBefore(closeButton, searchBoxContainer.firstChild);
// 添加键盘快捷键
document.addEventListener('keydown', (e) => {
if (e.key === 'F3' || (e.ctrlKey && e.key === 'g')) {
e.preventDefault();
if (e.shiftKey) {
navigateHighlight('prev');
} else {
navigateHighlight('next');
}
}
});
// 在搜索框显示时更新预设选项
const originalToggleSearchBox = toggleSearchBox;
toggleSearchBox = function() {
const searchBox = document.querySelector('.search-box-container');
if (searchBox) {
const isCurrentlyHidden = searchBox.style.display === 'none';
if (isCurrentlyHidden) {
updatePresetOptions();
// 不清除选择的预设
updateSelectedPresets();
}
searchBox.style.display = isCurrentlyHidden ? 'flex' : 'none';
if (isCurrentlyHidden) {
const searchInput = searchBox.querySelector('input[type="text"]');
if (searchInput) {
searchInput.focus();
}
}
}
};
return updatePresetOptions;
}
// 清除所有高亮
function clearHighlights() {
const highlights = document.querySelectorAll('.st-highlight');
highlights.forEach(highlight => {
const parent = highlight.parentNode;
parent.replaceChild(document.createTextNode(highlight.textContent), highlight);
});
// 清除所有高亮样式
document.querySelectorAll('.current-highlight, .matched-card').forEach(card => {
card.classList.remove('current-highlight', 'matched-card');
});
// 重置导航状态
STATE.matchedCards = [];
STATE.currentMatchIndex = -1;
// 恢复所有卡片的可见性
const allCards = document.querySelectorAll('.row[data-id]');
allCards.forEach(card => {
card.style.display = '';
});
}
// 修改 searchAndHighlight 函数中的关键词处理部分
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function searchAndHighlight(keywords, matchCounter) {
clearHighlights();
const itemCards = document.querySelectorAll('.row[data-id]');
STATE.matchedCards = [];
let hasMatch = false;
// 预处理关键词,处理特殊字符和通配符
const processedKeywords = keywords.map(keyword => {
keyword = keyword.trim();
// 处理带条件的通配符
// 匹配模式:(&>2) 或 (&>=2) 或 (&<2) 或 (&<=2) 或 (&=2)
// 或带加号的版本:+(&>2) 等
const conditionalPattern = /(\(?&(>=|<=|>|<|=)(\d+)\)?)/;
if (conditionalPattern.test(keyword)) {
const match = keyword.match(conditionalPattern);
const fullMatch = match[0];
const operator = match[2];
const targetNum = parseInt(match[3]);
// 将关键词分成前后两部分
const [before, after] = keyword.split(fullMatch);
// 构建正则表达式和验证函数
const numPattern = '(\\d+)';
const beforePattern = before ? escapeRegExp(before) : '';
const afterPattern = after ? escapeRegExp(after) : '';
return {
pattern: beforePattern + numPattern + afterPattern,
validate: (foundNum) => {
const num = parseInt(foundNum);
switch(operator) {
case '>': return num > targetNum;
case '>=': return num >= targetNum;
case '<': return num < targetNum;
case '<=': return num <= targetNum;
case '=': return num === targetNum;
default: return false;
}
}
};
}
// 处理简单通配符
if (keyword.includes('&')) {
// 处理带加号的通配符
if (keyword.includes('+&')) {
keyword = escapeRegExp(keyword).replace(/\\\+&/g, '\\+[0-9]+');
} else {
// 处理不带加号的通配符
keyword = escapeRegExp(keyword).replace(/&/g, '[0-9]+');
}
} else {
// 处理其他特殊字符
keyword = escapeRegExp(keyword).replace(/\\\+/g, '[++]');
}
return { pattern: keyword };
}).filter(k => k);
itemCards.forEach(card => {
const cardText = card.textContent;
const matches = processedKeywords.map(keyword => {
if (!keyword.validate) {
// 简单模式匹配
const regex = new RegExp(keyword.pattern, 'i');
return regex.test(cardText);
} else {
// 条件模式匹配
const regex = new RegExp(keyword.pattern, 'i');
const match = cardText.match(regex);
if (!match) return false;
// 提取数字并验证条件
const foundNum = match[1];
return keyword.validate(foundNum);
}
});
const allMatch = matches.every(match => match);
if (allMatch) {
hasMatch = true;
STATE.matchedCards.push(card);
highlightKeywords(card, processedKeywords.map(k => k.pattern));
} else if (STATE.showOnlyMatched) {
card.style.display = 'none';
}
});
if (!hasMatch) {
alert('未找到匹配的结果');
if (matchCounter) {
matchCounter.textContent = '0/0';
}
} else {
STATE.currentMatchIndex = 0;
updateHighlightNavigation();
updateCardVisibility();
}
}
// 修改 highlightKeywords 函数
function highlightKeywords(element, patterns) {
const walker = document.createTreeWalker(
element,
NodeFilter.SHOW_TEXT,
{
acceptNode: function(node) {
if (node.parentNode.nodeName === 'SCRIPT' ||
node.parentNode.nodeName === 'STYLE' ||
node.parentNode.classList.contains('st-highlight')) {
return NodeFilter.FILTER_REJECT;
}
const text = node.textContent;
const containsAnyKeyword = patterns.some(pattern => {
const regex = new RegExp(pattern, 'i');
return regex.test(text);
});
return containsAnyKeyword ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
}
}
);
const nodes = [];
let node;
while (node = walker.nextNode()) {
nodes.push(node);
}
nodes.forEach(textNode => {
let text = textNode.textContent;
let tempText = text;
patterns.forEach(pattern => {
const regex = new RegExp(`(${pattern})`, 'gi');
if (regex.test(text)) {
tempText = tempText.replace(regex, (match) => {
return `<span class="st-highlight">${match}</span>`;
});
}
});
if (tempText !== text) {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = tempText;
const fragment = document.createDocumentFragment();
while (tempDiv.firstChild) {
fragment.appendChild(tempDiv.firstChild);
}
textNode.parentNode.replaceChild(fragment, textNode);
}
});
}
// 更新高亮导航
function updateHighlightNavigation() {
const matchCounter = document.querySelector('.search-counter');
if (!matchCounter) return;
// 更新计数器
matchCounter.textContent = `${STATE.currentMatchIndex + 1}/${STATE.matchedCards.length}`;
// 移除之前的当前高亮
document.querySelectorAll('.current-highlight, .matched-card').forEach(card => {
card.classList.remove('current-highlight', 'matched-card');
});
// 添加新的当前高亮
const currentCard = STATE.matchedCards[STATE.currentMatchIndex];
if (currentCard) {
currentCard.classList.add('current-highlight');
// 滚动到当前卡片
currentCard.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}
}
// 导航到上一个/下一个高亮
function navigateHighlight(direction) {
if (STATE.matchedCards.length === 0) return;
if (direction === 'next') {
STATE.currentMatchIndex = (STATE.currentMatchIndex + 1) % STATE.matchedCards.length;
} else {
STATE.currentMatchIndex = (STATE.currentMatchIndex - 1 + STATE.matchedCards.length) % STATE.matchedCards.length;
}
updateHighlightNavigation();
}
// 修改样式
const style = document.createElement('style');
style.textContent = `
.current-highlight {
background-color: rgba(255, 215, 0, 0.3) !important;
}
.matched-card {
background-color: rgba(255, 215, 0, 0.1) !important;
}
.st-highlight {
background-color: #ffd700;
color: #000;
border-radius: 2px;
padding: 0 2px;
}
`;
document.head.appendChild(style);
function watchSearchResults(converter) {
let lastUrl = location.href;
const urlObserver = setInterval(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
STATE.originalTexts = new WeakMap();
setTimeout(() => {
convertPageText(converter);
}, 500);
}
}, 100);
// 监视搜索结果变化
const resultObserver = new MutationObserver((mutations) => {
let needsConversion = false;
for (const mutation of mutations) {
if (mutation.type === 'childList' || mutation.type === 'characterData') {
needsConversion = true;
break;
}
}
if (needsConversion) {
setTimeout(() => convertPageText(converter), 100);
}
});
const resultsContainer = document.querySelector('.results-container');
if (resultsContainer) {
resultObserver.observe(resultsContainer, {
childList: true,
subtree: true,
characterData: true
});
}
}
function findReactInstance(element) {
const key = Object.keys(element).find(key => key.startsWith('__reactFiber$'));
return key ? element[key] : null;
}
function findLoadMoreHandler() {
const loadMoreBtn = document.querySelector('.load-more-btn');
if (!loadMoreBtn) {
console.log('未找到加载更多按钮');
return null;
}
// 尝试获取React实例
const instance = findReactInstance(loadMoreBtn);
if (!instance) {
console.log('未找到React实例');
return null;
}
// 遍历查找onClick处理函数
let current = instance;
while (current) {
if (current.memoizedProps && current.memoizedProps.onClick) {
return current.memoizedProps.onClick;
}
current = current.return;
}
console.log('未找到onClick处理函数');
return null;
}
function clickLoadMoreIfExists() {
// 使用正确的选择器
const loadMoreBtn = document.querySelector('.btn.load-more-btn');
if (!loadMoreBtn) {
console.log('未找到加载更多按钮');
return false;
}
const results = document.querySelectorAll('.resultset, .trade-result, [class*="result-item"]');
const currentResultCount = results.length;
if (currentResultCount >= 100) {
return false;
}
try {
// 尝试多种方式触发点击
// 1. 原生点击
loadMoreBtn.click();
// 2. 模拟鼠标事件序列
['mousedown', 'mouseup', 'click'].forEach(eventType => {
const event = new MouseEvent(eventType, {
bubbles: true,
cancelable: true,
buttons: 1
});
loadMoreBtn.dispatchEvent(event);
});
// 3. 尝试点击内部的span
const spanInButton = loadMoreBtn.querySelector('span');
if (spanInButton) {
spanInButton.click();
['mousedown', 'mouseup', 'click'].forEach(eventType => {
const event = new MouseEvent(eventType, {
bubbles: true,
cancelable: true,
buttons: 1
});
spanInButton.dispatchEvent(event);
});
}
// 4. 使用 HTMLElement 的 click 方法
HTMLElement.prototype.click.call(loadMoreBtn);
return true;
} catch (error) {
console.log('触发加载更多时出错:', error);
return false;
}
}
function autoLoadAllResults() {
let attempts = 0;
const maxAttempts = 20;
let lastResultCount = 0;
function tryLoadMore() {
const results = document.querySelectorAll('.resultset');
const currentResultCount = results.length;
if (currentResultCount >= 100 || attempts >= maxAttempts ||
(currentResultCount === lastResultCount && attempts > 0)) {
return;
}
if (clickLoadMoreIfExists()) {
lastResultCount = currentResultCount;
attempts++;
setTimeout(tryLoadMore, 1000); // 增加间隔时间到1秒
}
}
setTimeout(tryLoadMore, 1000);
}
// 检查URL是否是搜索结果页面
function isSearchResultPage() {
const isPOE2Trade = window.location.href.includes('pathofexile.com/trade2/search/poe2');
const hasResults = document.querySelector('.results-container, .trade-results, .search-results, [class*="results"]') !== null;
return isPOE2Trade && hasResults;
}
async function init() {
try {
await new Promise(resolve => setTimeout(resolve, 100));
// 监听URL变化
let lastUrl = location.href;
const urlCheckInterval = setInterval(() => {
const currentUrl = location.href;
if ((currentUrl !== lastUrl || currentUrl.includes('pathofexile.com/trade2/search/poe2')) && STATE.autoLoadEnabled) {
lastUrl = currentUrl;
setTimeout(() => {
if (isSearchResultPage()) {
autoLoadAllResults();
}
}, 100);
}
}, 100);
// 初始检查
setTimeout(() => {
if (isSearchResultPage() && STATE.autoLoadEnabled) {
autoLoadAllResults();
}
}, 100);
const OpenCC = await waitForOpenCC();
const converter = createConverters(OpenCC);
window.converter = converter;
// 先创建 handleInput 函数
const handleInput = createInputHandler(converter);
const observer = createObserver(handleInput, converter);
observer.observe(document.body, {
childList: true,
subtree: true
});
attachInputListener(handleInput);
// 将 handleInput 作为参数传递给 createSearchBox
const updatePresetOptions = createSearchBox(handleInput);
createConfigModal();
createControls();
if (STATE.pageSimplified) {
convertPageText(converter);
}
watchSearchResults(converter);
setInterval(() => {
if (STATE.pageSimplified) {
convertPageText(converter);
}
}, 1000);
// 在保存或删除预设后更新搜索框的预设选项
const originalSaveSearchPreset = saveSearchPreset;
saveSearchPreset = function() {
originalSaveSearchPreset();
updatePresetOptions();
};
const originalUpdatePresetList = updatePresetList;
updatePresetList = function() {
originalUpdatePresetList();
updatePresetOptions();
};
} catch (error) {
console.log('初始化时出错:', error);
}
}
// 修改 updateConfig 函数
function updateConfig(category, name) {
if (confirm(`确定要用当前页面更新配置 "${name}" 吗?`)) {
STATE.configs[category][name] = {
url: window.location.href
};
GM_setValue('savedConfigs', STATE.configs);
updateConfigList();
}
}
// 添加预设关键词相关函数
function saveSearchPreset() {
const nameInput = document.getElementById('preset-name');
const keywordsInput = document.getElementById('preset-keywords');
const name = nameInput.value.trim();
const keywords = keywordsInput.value.trim();
const saveBtn = document.getElementById('save-preset');
if (!name || !keywords) {
alert('请输入预设名称和关键词');
return;
}
// 检查是否在编辑模式
if (nameInput.dataset.editMode === 'true') {
const originalName = nameInput.dataset.originalName;
// 如果名称改变了,删除旧的预设
if (originalName !== name) {
delete STATE.searchPresets[originalName];
}
// 清除编辑模式标记
delete nameInput.dataset.editMode;
delete nameInput.dataset.originalName;
saveBtn.textContent = '保存预设';
} else if (STATE.searchPresets[name] && !confirm(`预设 "${name}" 已存在,是否覆盖?`)) {
return;
}
STATE.searchPresets[name] = keywords;
GM_setValue('searchPresets', STATE.searchPresets);
updatePresetList();
nameInput.value = '';
keywordsInput.value = '';
}
function updatePresetList() {
const presetList = document.getElementById('preset-list');
presetList.innerHTML = '';
Object.entries(STATE.searchPresets).forEach(([name, keywords]) => {
const presetItem = document.createElement('div');
presetItem.style.cssText = `
display: grid;
grid-template-columns: 1fr auto auto;
align-items: center;
padding: 8px;
margin: 5px 0;
background: #3d3d3d;
border-radius: 4px;
gap: 10px;
`;
const nameSpan = document.createElement('span');
nameSpan.textContent = name; // 只显示预设名称
nameSpan.title = keywords; // 将关键词设置为提示文本
nameSpan.style.cssText = `
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: help; // 添加提示光标
`;
const editBtn = document.createElement('button');
editBtn.textContent = '编辑';
editBtn.style.cssText = `
background: #27ae60;
border: none;
color: #fff;
padding: 3px 12px;
cursor: pointer;
border-radius: 3px;
`;
editBtn.onclick = () => {
const presetEditModal = document.getElementById('preset-edit-modal');
const presetEditOverlay = document.getElementById('preset-edit-overlay');
const presetEditTitle = document.getElementById('preset-edit-title');
const nameInput = document.getElementById('preset-name');
const keywordsInput = document.getElementById('preset-keywords');
presetEditTitle.textContent = '编辑预设';
nameInput.value = name;
keywordsInput.value = keywords;
nameInput.dataset.editMode = 'true';
nameInput.dataset.originalName = name;
presetEditModal.style.display = 'block';
presetEditOverlay.style.display = 'block';
};
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '删除';
deleteBtn.style.cssText = `
background: #e74c3c;
border: none;
color: #fff;
padding: 3px 12px;
cursor: pointer;
border-radius: 3px;
`;
deleteBtn.onclick = () => {
if (confirm(`确定要删除预设 "${name}" 吗?`)) {
delete STATE.searchPresets[name];
GM_setValue('searchPresets', STATE.searchPresets);
updatePresetList();
}
};
presetItem.appendChild(nameSpan);
presetItem.appendChild(editBtn);
presetItem.appendChild(deleteBtn);
presetList.appendChild(presetItem);
});
}
setTimeout(init, 2000);
// 在clearHighlights函数后添加
function updateCardVisibility() {
const allCards = document.querySelectorAll('.row[data-id]');
if (STATE.showOnlyMatched) {
// 如果启用了"只显示匹配项",隐藏所有非匹配卡片
allCards.forEach(card => {
if (STATE.matchedCards.includes(card)) {
card.style.display = '';
} else {
card.style.display = 'none';
}
});
} else {
// 如果禁用了"只显示匹配项",显示所有卡片
allCards.forEach(card => {
card.style.display = '';
});
}
}
})();