// ==UserScript== // @name 拯救deric的懒癌 // @namespace https://www.milkywayidle.com/ // @version 0.512 // @description 按角色存储 + 全技能整合收藏 + 多选筛选 + 完整物品卡片(支持跨技能拖拽自定义排序)+ Tab记忆 + 一键清除 // @author baozhi powerby_Grok // @match https://www.milkywayidle.com/* // @match https://www.milkywayidlecn.com/* // @match https://test.milkywayidle.com/* // @grant none // @icon https://www.milkywayidle.com/favicon.svg // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/557976/%E6%8B%AF%E6%95%91deric%E7%9A%84%E6%87%92%E7%99%8C.user.js // @updateURL https://update.greasyfork.icu/scripts/557976/%E6%8B%AF%E6%95%91deric%E7%9A%84%E6%87%92%E7%99%8C.meta.js // ==/UserScript== (function () { 'use strict'; // 1. 扩展允许的技能列表,新增 '挤奶' 和 '伐木' const allowedSkills = ['采摘', '奶酪锻造', '制作', '缝纫', '烹饪', '冲泡', '挤奶', '伐木']; // 统一存储键 const MWI_MAIN_STORAGE_KEY = 'mwi_main_data_'; // 旧的存储键(用于数据迁移) const OLD_STORAGE_KEY = 'mwi_all_favorites_'; const OLD_LAST_TAB_KEY = 'mwi_last_tabs_'; const OLD_SELECTED_SKILLS_KEY = 'mwi_selected_skills_'; const CACHE_KEY = 'mwi_item_cache_'; const PENDING_CLICK_KEY = 'mwi_pending_global_click'; let currentCharacterId = null; let selectedSkills = new Set(); let isProcessingClick = false; let updateTimeout; let tabRestoreTimeout; let lastObservedSkill = null; // V4.1: 新增状态跟踪,用于判断是否为新的技能面板加载 // ==================== 辅助函数:防抖 ==================== /** * 防抖更新 UI,用于合并短时间内的多次收藏/取消收藏操作。 */ function debounceUpdate() { clearTimeout(updateTimeout); updateTimeout = setTimeout(() => { updateFavoritesPanelIfOpen(); updateAllFavoriteButtons(); }, 200); } // ==================== 角色ID获取 ==================== function hookCharacterId() { const originalGet = Object.getOwnPropertyDescriptor(MessageEvent.prototype, "data").get; function hookedGet() { const result = originalGet.call(this); if (this.currentTarget instanceof WebSocket) { try { const msg = JSON.parse(result); if (msg?.type === "init_character_data" && msg.character?.id) { const newId = msg.character.id.toString(); if (newId !== currentCharacterId) { currentCharacterId = newId; setTimeout(() => { selectedSkills = new Set(getSelectedSkills()); debounceUpdate(); }, 300); } } } catch (e) { /* ignore */ } } return result; } Object.defineProperty(MessageEvent.prototype, "data", { get: hookedGet, configurable: true }); } function getCharacterId() { if (currentCharacterId) return currentCharacterId; if (window.mwi?.character?.id) return window.mwi.character.id.toString(); return 'default'; } // ==================== 统一存储操作 (主存储) ==================== function getMainStorageKey() { return `${MWI_MAIN_STORAGE_KEY}${getCharacterId()}`; } // 将 allFavorites 结构根据 globalOrder 同步,以确保数据一致性 (谁拥有哪个物品) function syncFavoritesToGlobalOrder(data) { // 1. 映射所有现有物品到它们的技能 const itemToSkillMap = {}; for (const skill in data.allFavorites) { data.allFavorites[skill].forEach(name => { itemToSkillMap[name] = skill; }); } // 2. 清空 allFavorites 并根据 globalOrder 和 itemToSkillMap 重建 const newFavorites = {}; allowedSkills.forEach(skill => newFavorites[skill] = []); data.globalOrder.forEach(name => { const skill = itemToSkillMap[name]; // 使用物品的原始技能 if (skill && allowedSkills.includes(skill)) { newFavorites[skill].push(name); } }); data.allFavorites = newFavorites; } // 获取所有角色数据,并处理迁移和 globalOrder 初始化 function getCharacterData() { const key = getMainStorageKey(); let data = { allFavorites: {}, globalOrder: [], lastTabs: {}, selectedSkills: [] }; try { const raw = localStorage.getItem(key); if (raw) { data = JSON.parse(raw); } } catch (e) { /* ignore */ } allowedSkills.forEach(skill => { data.allFavorites[skill] = data.allFavorites[skill] || []; }); data.globalOrder = data.globalOrder || []; const isMigrated = migrateOldData(data); // 如果 globalOrder 是空的,则用 allFavorites 初始化它 (保持 skill -> item 顺序) if (data.globalOrder.length === 0) { let uniqueItems = new Set(); allowedSkills.forEach(skill => { (data.allFavorites[skill] || []).forEach(name => { if (!uniqueItems.has(name)) { data.globalOrder.push(name); uniqueItems.add(name); } }); }); if (data.globalOrder.length > 0) { setCharacterData(data); } } else if (isMigrated) { syncFavoritesToGlobalOrder(data); setCharacterData(data); } return data; } function setCharacterData(data) { localStorage.setItem(getMainStorageKey(), JSON.stringify(data)); } function migrateOldData(data) { let isMigrated = false; const oldFavs = localStorage.getItem(`${OLD_STORAGE_KEY}${getCharacterId()}`); if (oldFavs) { try { const parsed = JSON.parse(oldFavs); const fullFavorites = {}; allowedSkills.forEach(skill => { fullFavorites[skill] = parsed[skill] || []; }); data.allFavorites = fullFavorites; localStorage.removeItem(`${OLD_STORAGE_KEY}${getCharacterId()}`); isMigrated = true; } catch (e) { /* ignore */ } } const oldSelected = localStorage.getItem(`${OLD_SELECTED_SKILLS_KEY}${getCharacterId()}`); if (oldSelected) { try { data.selectedSkills = JSON.parse(oldSelected); localStorage.removeItem(`${OLD_SELECTED_SKILLS_KEY}${getCharacterId()}`); isMigrated = true; } catch (e) { /* ignore */ } } const oldTabs = localStorage.getItem(`${OLD_LAST_TAB_KEY}${getCharacterId()}`); if (oldTabs) { try { data.lastTabs = JSON.parse(oldTabs); localStorage.removeItem(`${OLD_LAST_TAB_KEY}${getCharacterId()}`); isMigrated = true; } catch (e) { /* ignore */ } } return isMigrated; } // ==================== 全局排序操作 ==================== function getGlobalOrder() { return getCharacterData().globalOrder; } function setGlobalOrder(newOrderArray) { const data = getCharacterData(); data.globalOrder = newOrderArray; syncFavoritesToGlobalOrder(data); setCharacterData(data); } function findItemSkill(itemName) { const allFavorites = getCharacterData().allFavorites; for (const [skill, items] of Object.entries(allFavorites)) { if (items.includes(itemName)) { return skill; } } return null; } function getFavoritesForSkill(skill) { return getCharacterData().allFavorites[skill] || []; } function setFavoritesForSkill(skill, arr) { const data = getCharacterData(); data.allFavorites[skill] = arr; setCharacterData(data); } function addToGlobalOrder(name) { const data = getCharacterData(); if (!data.globalOrder.includes(name)) { data.globalOrder.push(name); syncFavoritesToGlobalOrder(data); setCharacterData(data); } } function removeFromGlobalOrder(name) { const data = getCharacterData(); const index = data.globalOrder.indexOf(name); if (index > -1) { data.globalOrder.splice(index, 1); syncFavoritesToGlobalOrder(data); setCharacterData(data); } } function saveSelectedSkills(skills) { const data = getCharacterData(); data.selectedSkills = skills; setCharacterData(data); } function getSelectedSkills() { const data = getCharacterData(); const allFavorites = data.allFavorites; const skillsWithFavorites = allowedSkills.filter(skill => (allFavorites[skill]?.length || 0) > 0); let validSkills = []; if (data.selectedSkills && data.selectedSkills.length > 0) { validSkills = data.selectedSkills.filter(skill => skillsWithFavorites.includes(skill)); } if (validSkills.length === 0 && skillsWithFavorites.length > 0) { validSkills = skillsWithFavorites; } return validSkills; } // ==================== 物品卡片缓存 (独立存储) ==================== function getCacheKey() { return `${CACHE_KEY}${getCharacterId()}`; } function cacheItemCard(item, skill) { const nameEl = item.querySelector('.SkillAction_name__2VPXa'); if (!nameEl) return; const name = nameEl.textContent.trim(); const isFavorited = !!findItemSkill(name); if (!isFavorited) return; const cleanItem = item.cloneNode(true); cleanItem.querySelector('.mwi-fav-btn')?.remove(); const clone = cleanItem.cloneNode(true); try { const cache = JSON.parse(localStorage.getItem(getCacheKey()) || '{}'); if (!cache[skill]) cache[skill] = {}; cache[skill][name] = clone.outerHTML; localStorage.setItem(getCacheKey(), JSON.stringify(cache)); } catch (e) { /* ignore */ } } function getCachedItem(skill, name) { try { const cache = JSON.parse(localStorage.getItem(getCacheKey()) || '{}'); let cachedHtml = cache[skill]?.[name] || null; if (!cachedHtml) { for (const sk in cache) { if (cache[sk][name]) { cachedHtml = cache[sk][name]; break; } } } return cachedHtml; } catch (e) { return null; } } function removeCacheForItem(skill, name) { try { const cache = JSON.parse(localStorage.getItem(getCacheKey()) || '{}'); if (cache[skill] && cache[skill][name]) { delete cache[skill][name]; localStorage.setItem(getCacheKey(), JSON.stringify(cache)); } } catch (e) { /* ignore */ } } // ==================== 收藏按钮(技能页面)==================== function addFavoriteButton(item, currentSkill) { if (item.querySelector('.mwi-fav-btn')) return; const nameEl = item.querySelector('.SkillAction_name__2VPXa'); if (!nameEl) return; const name = nameEl.textContent.trim(); const btn = document.createElement('button'); btn.className = 'mwi-fav-btn'; let itemSkill = findItemSkill(name); const isFavorited = !!itemSkill; btn.textContent = isFavorited ? '⭐' : '☆'; Object.assign(btn.style, { position: 'absolute', top: '4px', right: '4px', background: 'none', border: 'none', fontSize: '18px', cursor: 'pointer', zIndex: '10', color: isFavorited ? 'rgb(255, 215, 0)' : 'rgb(170, 170, 170)' }); btn.onclick = e => { e.stopPropagation(); const itemSkill = findItemSkill(name); if (itemSkill) { // 取消收藏 const arr = getFavoritesForSkill(itemSkill); const idx = arr.indexOf(name); if (idx > -1) { arr.splice(idx, 1); setFavoritesForSkill(itemSkill, arr); removeFromGlobalOrder(name); removeCacheForItem(itemSkill, name); btn.textContent = '☆'; btn.style.color = 'rgb(170, 170, 170)'; debounceUpdate(); updateOtherSkillFavoriteButtons(name, false); } } else { // 添加收藏到当前技能 const arr = getFavoritesForSkill(currentSkill); if (!arr.includes(name)) { arr.push(name); setFavoritesForSkill(currentSkill, arr); addToGlobalOrder(name); cacheItemCard(item, currentSkill); btn.textContent = '⭐'; btn.style.color = 'rgb(255, 215, 0)'; debounceUpdate(); } } }; item.style.position = 'relative'; item.appendChild(btn); } // 更新其他技能的收藏按钮状态 function updateOtherSkillFavoriteButtons(itemName, isFavorited = null) { if (isFavorited === null) { isFavorited = !!findItemSkill(itemName); } const allItems = document.querySelectorAll('.SkillAction_skillAction__1esCp:not(.SkillAction_opaque__s9Yeq)'); allItems.forEach(item => { const nameEl = item.querySelector('.SkillAction_name__2VPXa'); if (nameEl && nameEl.textContent.trim() === itemName) { const btn = item.querySelector('.mwi-fav-btn'); if (btn) { btn.textContent = isFavorited ? '⭐' : '☆'; btn.style.color = isFavorited ? 'rgba(255, 254, 249, 1)' : 'rgb(170, 170, 170)'; } } }); } function updateAllFavoriteButtons() { const allItems = document.querySelectorAll('.SkillAction_skillAction__1esCp:not(.SkillAction_opaque__s9Yeq)'); const currentSkill = document.querySelector('.NavigationBar_active__3R-QS .NavigationBar_label__1uH-y')?.textContent.trim(); allItems.forEach(item => { const btn = item.querySelector('.mwi-fav-btn'); if (btn) { const nameEl = item.querySelector('.SkillAction_name__2VPXa'); if (nameEl) { const name = nameEl.textContent.trim(); const isFavorited = !!findItemSkill(name); btn.textContent = isFavorited ? '⭐' : '☆'; btn.style.color = isFavorited ? 'rgb(255, 215, 0)' : 'rgb(170, 170, 170)'; if (isFavorited && currentSkill && !getCachedItem(currentSkill, name)) { setTimeout(() => { cacheItemCard(item, currentSkill); }, 10); } } } }); } // ==================== Tab 切换和跨技能跳转 ==================== function getLastTabIndex(skill) { const data = getCharacterData(); return data.lastTabs[skill] !== undefined ? data.lastTabs[skill] : 0; } function setLastTabIndex(skill, idx) { const data = getCharacterData(); data.lastTabs[skill] = idx; setCharacterData(data); } /** * 为“挤奶”和“伐木”这种没有原生标签页的技能注入 Mui Tab 结构 */ function injectTabStructure(root, skill) { // 检查是否已经注入 if (root.querySelector('.MuiTabs-root')) { return; } const tabsComponentContainer = root.querySelector('.GatheringProductionSkillPanel_tabsComponentContainer__3Ua1T'); if (!tabsComponentContainer) return; const tabsComponent = tabsComponentContainer.querySelector('.TabsComponent_tabsComponent__3PqGp'); const panelsContainer = tabsComponentContainer.querySelector('.TabsComponent_tabPanelsContainer__26mzo'); if (!tabsComponent || !panelsContainer) return; // 1. 创建 Tab 结构 const tabsContainer = document.createElement('div'); tabsContainer.className = 'TabsComponent_tabsContainer__3BDUp TabsComponent_wrap__3fEC7'; const muiTabsRoot = document.createElement('div'); muiTabsRoot.className = 'MuiTabs-root css-orq8zk'; // 使用通用的 Mui Tabs 类 const muiTabsScroller = document.createElement('div'); muiTabsScroller.className = 'MuiTabs-scroller MuiTabs-fixed css-1anid1y'; muiTabsScroller.style.overflow = 'hidden'; muiTabsScroller.style.marginBottom = '0px'; const muiTabsFlexContainer = document.createElement('div'); muiTabsFlexContainer.className = 'MuiTabs-flexContainer css-k008qs'; muiTabsFlexContainer.setAttribute('role', 'tablist'); const indicator = document.createElement('span'); indicator.className = 'MuiTabs-indicator css-ttwr4n'; indicator.style.left = '0px'; indicator.style.width = '0px'; // 2. 创建第一个 Tab 按钮(原操作列表) const originalTabButton = document.createElement('button'); originalTabButton.className = 'MuiButtonBase-root MuiTab-root MuiTab-textColorPrimary css-1q2h7u5 Mui-selected'; originalTabButton.setAttribute('role', 'tab'); originalTabButton.setAttribute('aria-selected', 'true'); originalTabButton.setAttribute('tabindex', '0'); // V3.9: 添加自定义标识 originalTabButton.setAttribute('data-mwi-custom-tab', 'true'); originalTabButton.innerHTML = `${skill}`; // 3. 组装 Tab 栏 muiTabsFlexContainer.appendChild(originalTabButton); muiTabsScroller.appendChild(muiTabsFlexContainer); muiTabsScroller.appendChild(indicator); muiTabsRoot.appendChild(muiTabsScroller); tabsContainer.appendChild(muiTabsRoot); // 4. 注入 Tab 栏结构 tabsComponent.insertBefore(tabsContainer, panelsContainer); // 5. 调整原有的内容面板:使其成为第一个 Tab 的面板并显示 const originalPanel = panelsContainer.querySelector('.TabPanel_tabPanel__tXMJF'); if (originalPanel) { // 确保第一个面板可见 originalPanel.classList.remove('TabPanel_hidden__26UM3'); } } // 仅在有 Tab 结构的技能面板中创建“收藏”Tab function ensureFavoritesTab(tabsContainer, skill) { let favTab = tabsContainer.querySelector('.mwi-fav-tab'); if (favTab) return; const tabsFlex = tabsContainer.querySelector('.MuiTabs-flexContainer'); if (!tabsFlex) return; const tabsComponentContainer = tabsContainer.closest('.GatheringProductionSkillPanel_tabsComponentContainer__3Ua1T'); if (!tabsComponentContainer) return; const panelsContainer = tabsComponentContainer.querySelector('.TabsComponent_tabPanelsContainer__26mzo'); if (!panelsContainer) return; // --- 1. 创建 Tab 按钮 --- const tab = document.createElement('button'); tab.className = 'MuiButtonBase-root MuiTab-root MuiTab-textColorPrimary css-1q2h7u5 mwi-fav-tab'; tab.setAttribute('role', 'tab'); tab.setAttribute('aria-selected', 'false'); tab.setAttribute('tabindex', '-1'); // V3.9: 添加自定义标识 tab.setAttribute('data-mwi-custom-tab', 'true'); tab.innerHTML = `收藏`; tabsFlex.appendChild(tab); // --- 2. 创建 Tab 面板 --- const panel = document.createElement('div'); panel.className = 'TabPanel_tabPanel__tXMJF TabPanel_hidden__26UM3 mwi-fav-panel'; panel.style.padding = '24px'; panel.style.position = 'relative'; panelsContainer.appendChild(panel); } function switchToTab(skill, targetIdx) { const root = document.querySelector('.GatheringProductionSkillPanel_gatheringProductionSkillPanel__vG4M7'); if (!root) return; const tabsContainer = root.querySelector('.MuiTabs-root'); if (!tabsContainer) return; const tabsFlex = tabsContainer.querySelector('.MuiTabs-flexContainer'); if (!tabsFlex) return; const tabs = Array.from(tabsFlex.children); const indicator = tabsContainer.querySelector('.MuiTabs-indicator'); // 确保获取到所有面板,包括原生的和自定义的 const panels = root.querySelectorAll('.TabsComponent_tabPanelsContainer__26mzo > .TabPanel_tabPanel__tXMJF'); // 再次检查边界,防止出现越界错误 if (targetIdx < 0 || targetIdx >= tabs.length) { console.error(`[MWI Fav] Invalid target index: ${targetIdx} for skill ${skill}`); return; } // 更新按钮状态 tabs.forEach((t, i) => { t.classList.toggle('Mui-selected', i === targetIdx); t.setAttribute('aria-selected', i === targetIdx); t.setAttribute('tabindex', i === targetIdx ? '0' : '-1'); }); // 更新面板状态 (原生面板 + 收藏面板) panels.forEach((p, i) => { p.classList.toggle('TabPanel_hidden__26UM3', i !== targetIdx); }); // 更新指示器位置 if (indicator && tabs[targetIdx]) { // 计算目标 Tab 之前所有 Tab 的宽度总和 (用于确定 translateX) const left = tabs.slice(0, targetIdx).reduce((s, t) => s + t.offsetWidth, 0); const tabsRoot = root.querySelector('.MuiTabs-root'); // 获取 MuiTabs-root 的滚动位置,确保指示器在滚动时位置正确 const scrollLeft = tabsRoot ? tabsRoot.scrollLeft : 0; const indicatorX = left - scrollLeft; const indicatorW = tabs[targetIdx].offsetWidth; indicator.style.transform = `translateX(${indicatorX}px)`; indicator.style.width = `${indicatorW}px`; // 确保指示器的颜色也正确 (游戏原始颜色) indicator.style.backgroundColor = 'rgb(240, 240, 240)'; } // 关键:保存正确的 Tab 索引 setLastTabIndex(skill, targetIdx); if (targetIdx === tabs.length - 1 && targetIdx >= 0) { updateFavoritesPanel(skill); } } function setPendingClick(skill, itemName, selectedSkillsForTarget) { if (isProcessingClick) return; sessionStorage.setItem(PENDING_CLICK_KEY, JSON.stringify({ skill, itemName, selectedSkills: Array.from(selectedSkillsForTarget) })); } function getPendingClick() { const data = sessionStorage.getItem(PENDING_CLICK_KEY); sessionStorage.removeItem(PENDING_CLICK_KEY); return data ? JSON.parse(data) : null; } function handlePendingClick() { if (isProcessingClick) return; const pending = getPendingClick(); if (!pending) return; const { skill, itemName, selectedSkills: savedSelectedSkills } = pending; const currentSkillLabel = document.querySelector('.NavigationBar_active__3R-QS .NavigationBar_label__1uH-y'); if (!currentSkillLabel || currentSkillLabel.textContent.trim() !== skill) return; isProcessingClick = true; if (savedSelectedSkills && savedSelectedSkills.length > 0) { selectedSkills.clear(); savedSelectedSkills.forEach(s => selectedSkills.add(s)); } // V4.0: 使用 setTimeout 延迟,确保页面导航和 Tab 结构加载完成 setTimeout(() => { try { const root = document.querySelector('.GatheringProductionSkillPanel_gatheringProductionSkillPanel__vG4M7'); // 确保 Tab 结构已存在,特别是对于挤奶/伐木 if ((skill === '挤奶' || skill === '伐木') && !root.querySelector('.MuiTabs-root')) { injectTabStructure(root, skill); } const tabsContainer = root?.querySelector('.MuiTabs-root'); // 只有有 tab 的技能才需要切换 tab if (tabsContainer) { const tabsFlex = tabsContainer.querySelector('.MuiTabs-flexContainer'); if (tabsFlex) { const favTabIdx = tabsFlex.children.length - 1; const favTab = tabsFlex.children[favTabIdx]; if (favTab) { // 强制切换到收藏页 switchToTab(skill, favTabIdx); } } } setTimeout(() => { clickTargetItem(itemName, skill); isProcessingClick = false; }, 50); // 再次延迟 50ms 确保收藏 Tab 内容渲染完毕 } catch (e) { isProcessingClick = false; } }, 100); // 延迟 100ms 等待技能页面完全加载 } function clickTargetItem(itemName, skill) { const itemEl = Array.from(document.querySelectorAll('.SkillAction_skillAction__1esCp')).find(item => item.querySelector('.SkillAction_name__2VPXa')?.textContent.trim() === itemName ); if (itemEl && !itemEl.classList.contains('SkillAction_opaque__s9Yeq')) { itemEl.click(); } } // ==================== 收藏面板(多选版本)==================== function updateFavoritesPanel(currentSkill) { const panel = document.querySelector('.mwi-fav-panel'); if (!panel) return; const scrollTop = panel.scrollTop; panel.innerHTML = ''; const allFavorites = getCharacterData().allFavorites; const skillCounts = allowedSkills.reduce((acc, skill) => { acc[skill] = allFavorites[skill]?.length || 0; return acc; }, {}); const skillsWithFavorites = allowedSkills.filter(skill => skillCounts[skill] > 0); // --- 技能选择逻辑 --- if (selectedSkills.size === 0) { const savedSkills = getSelectedSkills(); savedSkills.forEach(skill => selectedSkills.add(skill)); if (selectedSkills.size > 0 && savedSkills.length !== Array.from(selectedSkills).length) { saveSelectedSkills(Array.from(selectedSkills)); } } else { selectedSkills.forEach(skill => { if (!skillsWithFavorites.includes(skill)) { selectedSkills.delete(skill); } }); if (selectedSkills.size === 0 && skillsWithFavorites.length > 0) { selectedSkills.add(skillsWithFavorites[0]); } saveSelectedSkills(Array.from(selectedSkills)); } // --- 筛选容器 --- const filterContainer = document.createElement('div'); filterContainer.style.marginBottom = '20px'; filterContainer.style.display = 'flex'; filterContainer.style.gap = '8px'; filterContainer.style.flexWrap = 'wrap'; filterContainer.style.alignItems = 'center'; filterContainer.className = 'mwi-filter-container'; const createFilterButton = (text, isSelected, clickHandler) => { const btn = document.createElement('button'); btn.className = 'MuiButtonBase-root MuiTab-root MuiTab-textColorPrimary mwi-filter-btn'; btn.textContent = text; Object.assign(btn.style, { minWidth: '0', padding: '6px 12px', margin: '0', border: 'none', cursor: 'pointer', background: isSelected ? '#3a3a3a' : 'none', color: isSelected ? 'white' : 'rgba(255, 255, 255, 0.7)', borderRadius: '4px', transition: 'all 0.2s', fontWeight: isSelected ? '500' : '400', textTransform: 'none' }); btn.addEventListener('mouseenter', () => { btn.style.background = isSelected ? '#3a3a3a' : 'rgba(255, 255, 255, 0.08)'; }); btn.addEventListener('mouseleave', () => { btn.style.background = isSelected ? '#3a3a3a' : 'none'; }); btn.addEventListener('click', clickHandler); return btn; }; const isAllSelected = selectedSkills.size === skillsWithFavorites.length && skillsWithFavorites.length > 0; filterContainer.appendChild(createFilterButton('全选', isAllSelected, () => { if (isAllSelected) { selectedSkills.clear(); } else { selectedSkills.clear(); skillsWithFavorites.forEach(skill => selectedSkills.add(skill)); } saveSelectedSkills(Array.from(selectedSkills)); updateFavoritesPanel(currentSkill); })); filterContainer.appendChild(createFilterButton('反选', false, () => { const allSelected = Array.from(selectedSkills); selectedSkills.clear(); skillsWithFavorites.forEach(skill => { if (!allSelected.includes(skill)) { selectedSkills.add(skill); } }); if (selectedSkills.size === 0 && skillsWithFavorites.length > 0) { selectedSkills.add(skillsWithFavorites[0]); } saveSelectedSkills(Array.from(selectedSkills)); updateFavoritesPanel(currentSkill); })); skillsWithFavorites.forEach(skill => { const isSelected = selectedSkills.has(skill); filterContainer.appendChild(createFilterButton(`${skill} (${skillCounts[skill]})`, isSelected, () => { if (selectedSkills.has(skill)) { if (selectedSkills.size > 1) { selectedSkills.delete(skill); } } else { selectedSkills.add(skill); } saveSelectedSkills(Array.from(selectedSkills)); updateFavoritesPanel(currentSkill); })); }); panel.appendChild(filterContainer); // 清空选中收藏按钮 const clearBtn = document.createElement('button'); clearBtn.className = 'mwi-clear-favs-btn'; clearBtn.textContent = '×'; clearBtn.title = '清空选中技能的收藏'; Object.assign(clearBtn.style, { position: 'absolute', top: '4px', right: '4px', background: 'none', border: 'none', color: '#999', fontSize: '20px', cursor: 'pointer', width: '24px', height: '24px', borderRadius: '12px', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '0', zIndex: '100', opacity: '0.5', transition: 'opacity 0.2s' }); clearBtn.addEventListener('mouseenter', () => { clearBtn.style.opacity = '1'; clearBtn.style.color = '#ff4444'; }); clearBtn.addEventListener('mouseleave', () => { clearBtn.style.opacity = '0.5'; clearBtn.style.color = '#999'; }); clearBtn.addEventListener('click', () => { if (selectedSkills.size === 0) return; const skillsList = Array.from(selectedSkills).join('、'); if (confirm(`确定要清空选中技能(${skillsList})的所有收藏吗?`)) { selectedSkills.forEach(skill => { getFavoritesForSkill(skill)?.forEach(name => { removeFromGlobalOrder(name); removeCacheForItem(skill, name); }); setFavoritesForSkill(skill, []); }); debounceUpdate(); selectedSkills.clear(); saveSelectedSkills(Array.from(selectedSkills)); updateFavoritesPanel(currentSkill); } }); panel.appendChild(clearBtn); // 创建物品容器 const grid = document.createElement('div'); grid.className = 'SkillActionGrid_skillActionGrid__1tJFk'; panel.appendChild(grid); // 使用 globalOrder 作为排序来源 const globalOrder = getGlobalOrder(); const displayItems = []; globalOrder.forEach(name => { const originalSkill = findItemSkill(name); if (originalSkill && selectedSkills.has(originalSkill)) { displayItems.push({ name: name, skill: originalSkill }); } }); if (displayItems.length === 0) { grid.innerHTML = `