微件:Card:修订间差异
跳转到导航
跳转到搜索
小 功能测试 |
小 功能测试 |
||
| 第1行: | 第1行: | ||
<includeonly> | <noinclude> | ||
此Widget为卡牌显示添加交互效果: | |||
* 鼠标悬停时的动画效果 | |||
* 点击后显示放大的模态框 | |||
* 自动检测spark_enable属性显示闪光按钮 | |||
* 支持闪光卡牌列表显示 | |||
* 预加载、批量加载和本地缓存优化 | |||
* 事件委托、DOM优化、批量API请求 | |||
使用方法:在页面中添加 {{#widget:Card}} | |||
</noinclude><includeonly> | |||
<style> | <style> | ||
/* 卡牌容器 */ | /* 卡牌容器 */ | ||
| 第353行: | 第363行: | ||
<script> | <script> | ||
(function() { | // 使用 mw.loader.using 确保 mediawiki.api 模块已加载 | ||
mw.loader.using(['mediawiki.api']).then(function() { | |||
'use strict'; | 'use strict'; | ||
// ==================== API 客户端 ==================== | |||
var apiClient = null; | |||
function getApi() { | |||
if (!apiClient) { | |||
apiClient = new mw.Api(); | |||
} | |||
return apiClient; | |||
} | |||
// ==================== 缓存管理器(优化版) ==================== | // ==================== 缓存管理器(优化版) ==================== | ||
| 第365行: | 第386行: | ||
var isDirty = false; | var isDirty = false; | ||
var saveTimer = null; | var saveTimer = null; | ||
// 检查 localStorage 是否可用 | |||
var storageAvailable = (function() { | |||
try { | |||
var test = '__storage_test__'; | |||
localStorage.setItem(test, test); | |||
localStorage.removeItem(test); | |||
return true; | |||
} catch (e) { | |||
return false; | |||
} | |||
})(); | |||
// 从 localStorage 加载到内存 | // 从 localStorage 加载到内存 | ||
function loadFromStorage() { | function loadFromStorage() { | ||
if (memoryCache !== null) return memoryCache; | if (memoryCache !== null) return memoryCache; | ||
if (!storageAvailable) { | |||
memoryCache = {}; | |||
return memoryCache; | |||
} | |||
try { | try { | ||
| 第381行: | 第419行: | ||
// 延迟批量写入 localStorage | // 延迟批量写入 localStorage | ||
function scheduleSave() { | function scheduleSave() { | ||
if (saveTimer) return; | if (!storageAvailable || saveTimer) return; | ||
saveTimer = setTimeout(function() { | saveTimer = setTimeout(function() { | ||
| 第394行: | 第432行: | ||
} | } | ||
} | } | ||
}, 1000); | }, 1000); | ||
} | } | ||
| 第404行: | 第442行: | ||
var keys = Object.keys(cache); | var keys = Object.keys(cache); | ||
if (force && keys.length > 10) { | if (force && keys.length > 10) { | ||
var items = keys.map(function(k) { | var items = keys.map(function(k) { | ||
| 第418行: | 第455行: | ||
} | } | ||
} else { | } else { | ||
for (var cardId in cache) { | for (var cardId in cache) { | ||
if (cache.hasOwnProperty(cardId) && now - cache[cardId].timestamp > CACHE_EXPIRE) { | if (cache.hasOwnProperty(cardId) && now - cache[cardId].timestamp > CACHE_EXPIRE) { | ||
| 第465行: | 第501行: | ||
}, | }, | ||
getMultiple: function(cardIds) { | getMultiple: function(cardIds) { | ||
var cache = loadFromStorage(); | var cache = loadFromStorage(); | ||
| 第492行: | 第527行: | ||
}, | }, | ||
setMultiple: function(dataMap) { | setMultiple: function(dataMap) { | ||
var cache = loadFromStorage(); | var cache = loadFromStorage(); | ||
| 第512行: | 第546行: | ||
cleanup: cleanup, | cleanup: cleanup, | ||
flush: function() { | flush: function() { | ||
if (!storageAvailable) return; | |||
if (saveTimer) { | if (saveTimer) { | ||
clearTimeout(saveTimer); | clearTimeout(saveTimer); | ||
| 第530行: | 第565行: | ||
// ==================== 批量API请求管理器 ==================== | // ==================== 批量API请求管理器 ==================== | ||
var BatchRequestManager = (function() { | var BatchRequestManager = (function() { | ||
var pendingRequests = {}; | var pendingRequests = {}; | ||
var requestQueue = []; | var requestQueue = []; | ||
var isProcessing = false; | var isProcessing = false; | ||
var batchSize = 5; | var batchSize = 5; | ||
var batchDelay = 50; | var batchDelay = 50; | ||
function processBatch() { | function processBatch() { | ||
| 第541行: | 第576行: | ||
isProcessing = true; | isProcessing = true; | ||
var batch = requestQueue.splice(0, batchSize); | var batch = requestQueue.splice(0, batchSize); | ||
var promises = batch.map(function(cardId) { | var promises = batch.map(function(cardId) { | ||
| 第557行: | 第591行: | ||
function fetchSparkList(cardId) { | function fetchSparkList(cardId) { | ||
return new Promise(function(resolve) { | return new Promise(function(resolve) { | ||
getApi().get({ | |||
action: 'parse', | action: 'parse', | ||
text: '{{Card/spark/list|' + cardId + '}}', | text: '{{Card/spark/list|' + cardId + '}}', | ||
| 第571行: | 第604行: | ||
} | } | ||
var callbacks = pendingRequests[cardId] || []; | var callbacks = pendingRequests[cardId] || []; | ||
delete pendingRequests[cardId]; | delete pendingRequests[cardId]; | ||
| 第592行: | 第624行: | ||
return { | return { | ||
request: function(cardId) { | request: function(cardId) { | ||
return new Promise(function(resolve, reject) { | return new Promise(function(resolve, reject) { | ||
var cached = SparkCache.get(cardId); | var cached = SparkCache.get(cardId); | ||
if (cached !== null) { | if (cached !== null) { | ||
| 第602行: | 第632行: | ||
} | } | ||
if (pendingRequests[cardId]) { | if (pendingRequests[cardId]) { | ||
pendingRequests[cardId].push({ resolve: resolve, reject: reject }); | pendingRequests[cardId].push({ resolve: resolve, reject: reject }); | ||
| 第608行: | 第637行: | ||
} | } | ||
pendingRequests[cardId] = [{ resolve: resolve, reject: reject }]; | pendingRequests[cardId] = [{ resolve: resolve, reject: reject }]; | ||
requestQueue.push(cardId); | requestQueue.push(cardId); | ||
if (!isProcessing) { | if (!isProcessing) { | ||
setTimeout(processBatch, 10); | setTimeout(processBatch, 10); | ||
| 第619行: | 第646行: | ||
}, | }, | ||
preload: function(cardIds) { | preload: function(cardIds) { | ||
var toLoad = cardIds.filter(function(cardId) { | var toLoad = cardIds.filter(function(cardId) { | ||
return !SparkCache.has(cardId) && | return !SparkCache.has(cardId) && | ||
| 第630行: | 第655行: | ||
if (toLoad.length === 0) return; | if (toLoad.length === 0) return; | ||
toLoad.forEach(function(cardId) { | toLoad.forEach(function(cardId) { | ||
pendingRequests[cardId] = []; | pendingRequests[cardId] = []; | ||
| 第636行: | 第660行: | ||
}); | }); | ||
if (!isProcessing) { | if (!isProcessing) { | ||
setTimeout(processBatch, 100); | setTimeout(processBatch, 100); | ||
| 第642行: | 第665行: | ||
}, | }, | ||
prioritize: function(cardId) { | prioritize: function(cardId) { | ||
var idx = requestQueue.indexOf(cardId); | var idx = requestQueue.indexOf(cardId); | ||
| 第660行: | 第682行: | ||
var sparkModalContent = null; | var sparkModalContent = null; | ||
var currentEnlargedContainer = null; | var currentEnlargedContainer = null; | ||
var processedCards = new WeakSet(); | var processedCards = new WeakSet(); | ||
function createModals() { | function createModals() { | ||
if (!modalOverlay) { | if (!modalOverlay) { | ||
| 第681行: | 第702行: | ||
} | } | ||
function closeModal() { | function closeModal() { | ||
if (!modalOverlay) return; | if (!modalOverlay) return; | ||
| 第693行: | 第713行: | ||
} | } | ||
function closeSparkModal() { | function closeSparkModal() { | ||
closeEnlargedCard(); | closeEnlargedCard(); | ||
| 第706行: | 第725行: | ||
} | } | ||
function closeEnlargedCard() { | function closeEnlargedCard() { | ||
if (currentEnlargedContainer) { | if (currentEnlargedContainer) { | ||
| 第714行: | 第732行: | ||
} | } | ||
function openModal(cardElement, cardId, sparkEnable) { | function openModal(cardElement, cardId, sparkEnable) { | ||
createModals(); | createModals(); | ||
| 第722行: | 第739行: | ||
modalContent.appendChild(cardClone); | modalContent.appendChild(cardClone); | ||
if (sparkEnable && sparkEnable.trim() !== '') { | if (sparkEnable && sparkEnable.trim() !== '') { | ||
var sparkButton = document.createElement('button'); | var sparkButton = document.createElement('button'); | ||
| 第746行: | 第762行: | ||
} | } | ||
function openSparkModal(cardId) { | function openSparkModal(cardId) { | ||
createModals(); | createModals(); | ||
| 第791行: | 第806行: | ||
} | } | ||
function showEnlargedCard(cardElement) { | function showEnlargedCard(cardElement) { | ||
closeEnlargedCard(); | closeEnlargedCard(); | ||
| 第812行: | 第826行: | ||
} | } | ||
function processNewCards(nodes) { | function processNewCards(nodes) { | ||
var sparkCardIds = []; | var sparkCardIds = []; | ||
| 第831行: | 第844行: | ||
cards.forEach(function(card) { | cards.forEach(function(card) { | ||
if (processedCards.has(card)) return; | if (processedCards.has(card)) return; | ||
if (card.closest('.card-modal-overlay') || | if (card.closest('.card-modal-overlay') || | ||
| 第839行: | 第851行: | ||
processedCards.add(card); | processedCards.add(card); | ||
var sparkEnable = card.dataset.sparkEnable; | var sparkEnable = card.dataset.sparkEnable; | ||
if (sparkEnable && sparkEnable.trim() !== '') { | if (sparkEnable && sparkEnable.trim() !== '') { | ||
| 第851行: | 第862行: | ||
}); | }); | ||
if (sparkCardIds.length > 0) { | if (sparkCardIds.length > 0) { | ||
setTimeout(function() { | setTimeout(function() { | ||
| 第859行: | 第869行: | ||
} | } | ||
function initExistingCards() { | function initExistingCards() { | ||
var cards = document.querySelectorAll('.card-deck-trans'); | var cards = document.querySelectorAll('.card-deck-trans'); | ||
| 第888行: | 第897行: | ||
var hoverTimer = null; | var hoverTimer = null; | ||
function getCardInfo(cardElement) { | function getCardInfo(cardElement) { | ||
var wrapper = cardElement.closest('.card-wrapper'); | var wrapper = cardElement.closest('.card-wrapper'); | ||
| 第897行: | 第905行: | ||
} | } | ||
function isValidCardClick(target) { | function isValidCardClick(target) { | ||
var card = target.closest('.card-deck-trans'); | var card = target.closest('.card-deck-trans'); | ||
if (!card) return null; | if (!card) return null; | ||
var inMainModal = card.closest('.card-modal-content'); | var inMainModal = card.closest('.card-modal-content'); | ||
var inSparkList = card.closest('.spark-card-list'); | var inSparkList = card.closest('.spark-card-list'); | ||
| 第913行: | 第919行: | ||
} | } | ||
function init() { | function init() { | ||
document.addEventListener('click', function(e) { | document.addEventListener('click', function(e) { | ||
var target = e.target; | var target = e.target; | ||
if (target.classList.contains('card-modal-close')) { | if (target.classList.contains('card-modal-close')) { | ||
e.stopPropagation(); | e.stopPropagation(); | ||
| 第938行: | 第941行: | ||
} | } | ||
if (target.classList.contains('spark-button')) { | if (target.classList.contains('spark-button')) { | ||
e.preventDefault(); | e.preventDefault(); | ||
| 第948行: | 第950行: | ||
} | } | ||
if (target.classList.contains('card-modal-overlay')) { | if (target.classList.contains('card-modal-overlay')) { | ||
DOMManager.closeModal(); | DOMManager.closeModal(); | ||
| 第964行: | 第965行: | ||
} | } | ||
var card = isValidCardClick(target); | var card = isValidCardClick(target); | ||
if (card) { | if (card) { | ||
| 第973行: | 第973行: | ||
if (inSparkList) { | if (inSparkList) { | ||
DOMManager.showEnlargedCard(card); | DOMManager.showEnlargedCard(card); | ||
} else { | } else { | ||
DOMManager.openModal(card, info.cardId, info.sparkEnable); | DOMManager.openModal(card, info.cardId, info.sparkEnable); | ||
} | } | ||
| 第982行: | 第980行: | ||
}, true); | }, true); | ||
document.addEventListener('mouseenter', function(e) { | document.addEventListener('mouseenter', function(e) { | ||
var card = e.target.closest && e.target.closest('.card-deck-trans'); | var card = e.target.closest && e.target.closest('.card-deck-trans'); | ||
if (!card) return; | if (!card) return; | ||
if (card.closest('.card-modal-overlay') || | if (card.closest('.card-modal-overlay') || | ||
card.closest('.spark-modal-overlay') || | card.closest('.spark-modal-overlay') || | ||
| 第994行: | 第990行: | ||
var info = getCardInfo(card); | var info = getCardInfo(card); | ||
if (info.sparkEnable && info.sparkEnable.trim() !== '' && info.cardId) { | if (info.sparkEnable && info.sparkEnable.trim() !== '' && info.cardId) { | ||
if (hoverTimer) { | if (hoverTimer) { | ||
clearTimeout(hoverTimer); | clearTimeout(hoverTimer); | ||
} | } | ||
hoverTimer = setTimeout(function() { | hoverTimer = setTimeout(function() { | ||
BatchRequestManager.prioritize(info.cardId); | BatchRequestManager.prioritize(info.cardId); | ||
| 第1,017行: | 第1,011行: | ||
}, true); | }, true); | ||
document.addEventListener('keydown', function(e) { | document.addEventListener('keydown', function(e) { | ||
if (e.key === 'Escape') { | if (e.key === 'Escape') { | ||
| 第1,023行: | 第1,016行: | ||
var mainModal = DOMManager.getModalOverlay(); | var mainModal = DOMManager.getModalOverlay(); | ||
if (document.querySelector('.spark-enlarged-container')) { | if (document.querySelector('.spark-enlarged-container')) { | ||
DOMManager.closeEnlargedCard(); | DOMManager.closeEnlargedCard(); | ||
| 第1,034行: | 第1,026行: | ||
}); | }); | ||
window.addEventListener('beforeunload', function() { | window.addEventListener('beforeunload', function() { | ||
SparkCache.flush(); | SparkCache.flush(); | ||
}); | }); | ||
document.addEventListener('visibilitychange', function() { | document.addEventListener('visibilitychange', function() { | ||
if (document.hidden) { | if (document.hidden) { | ||
| 第1,054行: | 第1,044行: | ||
// ==================== 初始化 ==================== | // ==================== 初始化 ==================== | ||
function init() { | function init() { | ||
SparkCache.cleanup(); | SparkCache.cleanup(); | ||
DOMManager.createModals(); | DOMManager.createModals(); | ||
EventManager.init(); | EventManager.init(); | ||
DOMManager.initExistingCards(); | DOMManager.initExistingCards(); | ||
var observer = new MutationObserver(function(mutations) { | var observer = new MutationObserver(function(mutations) { | ||
var newNodes = []; | var newNodes = []; | ||
| 第1,079行: | 第1,061行: | ||
if (newNodes.length > 0) { | if (newNodes.length > 0) { | ||
if (window.requestIdleCallback) { | if (window.requestIdleCallback) { | ||
requestIdleCallback(function() { | requestIdleCallback(function() { | ||
| 第1,104行: | 第1,085行: | ||
init(); | init(); | ||
} | } | ||
} | }); | ||
</script> | </script> | ||
</includeonly> | </includeonly> | ||
2026年1月17日 (六) 10:25的版本
此Widget为卡牌显示添加交互效果:
- 鼠标悬停时的动画效果
- 点击后显示放大的模态框
- 自动检测spark_enable属性显示闪光按钮
- 支持闪光卡牌列表显示
- 预加载、批量加载和本地缓存优化
- 事件委托、DOM优化、批量API请求
使用方法:在页面中添加