微件:Card:修订间差异
跳转到导航
跳转到搜索
小 功能测试 |
功能测试 |
||
| 第254行: | 第254行: | ||
50% { content: ".."; } | 50% { content: ".."; } | ||
75% { content: "..."; } | 75% { content: "..."; } | ||
} | } | ||
| 第363行: | 第356行: | ||
<script> | <script> | ||
(function() { | |||
'use strict'; | 'use strict'; | ||
// ==================== | // ==================== 等待 MediaWiki 加载 ==================== | ||
var | function waitForMw(callback) { | ||
// 检查 mw 和 mw.loader 是否存在 | |||
if (typeof mw !== 'undefined' && mw.loader && typeof mw.loader.using === 'function') { | |||
mw.loader.using(['mediawiki.api']).then(callback); | |||
} else if (typeof mw !== 'undefined' && mw.Api) { | |||
// mw.Api 已经可用 | |||
callback(); | |||
} else { | |||
// 等待 mw 加载 | |||
var checkCount = 0; | |||
var maxChecks = 50; // 最多等待5秒 | |||
var checkInterval = setInterval(function() { | |||
checkCount++; | |||
if (typeof mw !== 'undefined' && mw.loader && typeof mw.loader.using === 'function') { | |||
clearInterval(checkInterval); | |||
mw.loader.using(['mediawiki.api']).then(callback); | |||
} else if (typeof mw !== 'undefined' && mw.Api) { | |||
clearInterval(checkInterval); | |||
callback(); | |||
} else if (checkCount >= maxChecks) { | |||
clearInterval(checkInterval); | |||
console.warn('CardWidget: MediaWiki API 加载超时,闪光功能可能不可用'); | |||
// 仍然初始化基本功能 | |||
callback(); | |||
} | |||
}, 100); | |||
} | } | ||
} | } | ||
// ==================== | // ==================== 主程序 ==================== | ||
function initCardWidget() { | |||
// | // ==================== API 客户端 ==================== | ||
var | var apiClient = null; | ||
var | var apiAvailable = false; | ||
function getApi() { | |||
if (!apiAvailable) { | |||
// 检查 API 是否可用 | |||
if (typeof mw !== 'undefined' && typeof mw.Api === 'function') { | |||
apiAvailable = true; | |||
} else { | |||
return null; | |||
} | |||
} | } | ||
if (! | if (!apiClient && apiAvailable) { | ||
try { | |||
apiClient = new mw.Api(); | |||
} catch (e) { | |||
console.warn('CardWidget: 无法创建 API 客户端', e); | |||
apiAvailable = false; | |||
return null; | |||
} | |||
} | } | ||
return | return apiClient; | ||
} | } | ||
// | // ==================== 缓存管理器 ==================== | ||
function | var SparkCache = (function() { | ||
var CACHE_KEY = 'spark_card_cache_v2'; | |||
var CACHE_EXPIRE = 24 * 60 * 60 * 1000; | |||
var memoryCache = null; | |||
var isDirty = false; | |||
var saveTimer = null; | |||
var storageAvailable = (function() { | |||
try { | |||
var test = '__storage_test__'; | |||
localStorage.setItem(test, test); | |||
localStorage.removeItem(test); | |||
return true; | |||
} catch (e) { | |||
return false; | |||
} | } | ||
} | })(); | ||
function loadFromStorage() { | |||
if (memoryCache !== null) return memoryCache; | |||
if (!storageAvailable) { | |||
memoryCache = {}; | |||
return memoryCache; | |||
} | } | ||
try { | |||
var data = localStorage.getItem(CACHE_KEY); | |||
memoryCache = data ? JSON.parse(data) : {}; | |||
} catch (e) { | |||
} | memoryCache = {}; | ||
} | } | ||
return memoryCache; | |||
} | } | ||
if ( | function scheduleSave() { | ||
if (!storageAvailable || saveTimer) return; | |||
saveTimer = setTimeout(function() { | |||
saveTimer = null; | |||
if (isDirty && memoryCache) { | |||
try { | |||
localStorage.setItem(CACHE_KEY, JSON.stringify(memoryCache)); | |||
isDirty = false; | |||
} catch (e) { | |||
cleanup(true); | |||
} | |||
} | |||
}, 1000); | |||
} | } | ||
function cleanup(force) { | |||
var cache = loadFromStorage(); | var cache = loadFromStorage(); | ||
var | var now = Date.now(); | ||
var hasChanges = false; | |||
var keys = Object.keys(cache); | |||
if (force && keys.length > 10) { | |||
var items = keys.map(function(k) { | |||
return { key: k, time: cache[k].timestamp }; | |||
}).sort(function(a, b) { | |||
return a.time - b.time; | |||
}); | |||
var deleteCount = Math.floor(items.length / 2); | |||
for (var i = 0; i < deleteCount; i++) { | |||
delete cache[items[i].key]; | |||
hasChanges = true; | |||
} | |||
} else { | |||
for (var cardId in cache) { | |||
if (cache.hasOwnProperty(cardId) && now - cache[cardId].timestamp > CACHE_EXPIRE) { | |||
delete cache[cardId]; | |||
hasChanges = true; | |||
} | |||
} | |||
} | |||
if ( | if (hasChanges) { | ||
isDirty = true; | isDirty = true; | ||
scheduleSave(); | scheduleSave(); | ||
} | } | ||
} | |||
} | |||
return { | |||
get: function(cardId) { | |||
var cache = loadFromStorage(); | |||
var item = cache[cardId]; | var item = cache[cardId]; | ||
if (item) | |||
if (!item) return null; | |||
if (Date.now() - item.timestamp > CACHE_EXPIRE) { | |||
delete cache[cardId]; | |||
isDirty = true; | |||
scheduleSave(); | |||
return null; | |||
} | } | ||
} | |||
return item.html; | |||
}, | |||
set: function(cardId, html) { | |||
var cache = loadFromStorage(); | |||
cache[cardId] = { | |||
html: html, | |||
timestamp: Date.now() | |||
}; | |||
isDirty = true; | isDirty = true; | ||
scheduleSave(); | scheduleSave(); | ||
} | }, | ||
has: function(cardId) { | |||
var cache = loadFromStorage(); | |||
var item = cache[cardId]; | |||
return item && (Date.now() - item.timestamp <= CACHE_EXPIRE); | |||
}, | |||
cleanup: cleanup, | |||
flush: function() { | |||
if ( | if (!storageAvailable) return; | ||
if (saveTimer) { | |||
clearTimeout(saveTimer); | |||
} | saveTimer = null; | ||
} | |||
if (isDirty && memoryCache) { | |||
try { | |||
localStorage.setItem(CACHE_KEY, JSON.stringify(memoryCache)); | |||
isDirty = false; | |||
} catch (e) {} | |||
} | } | ||
} | } | ||
}; | |||
})(); | |||
// ==================== 批量API请求管理器 ==================== | |||
var BatchRequestManager = (function() { | |||
var pendingRequests = {}; | |||
var requestQueue = []; | |||
var isProcessing = false; | |||
var batchSize = 5; | |||
var batchDelay = 50; | |||
function processBatch() { | |||
if (isProcessing || requestQueue.length === 0) return; | |||
if ( | |||
if ( | var api = getApi(); | ||
if (!api) { | |||
// API 不可用,清空队列并拒绝所有请求 | |||
requestQueue.forEach(function(cardId) { | |||
var callbacks = pendingRequests[cardId] || []; | |||
delete pendingRequests[cardId]; | |||
callbacks.forEach(function(cb) { | |||
cb.reject(new Error('API not available')); | |||
} | }); | ||
}); | |||
requestQueue = []; | |||
return; | |||
} | } | ||
isProcessing = true; | |||
var batch = requestQueue.splice(0, batchSize); | |||
var promises = batch.map(function(cardId) { | |||
return fetchSparkList(cardId, api); | |||
}); | |||
Promise.all(promises).then(function() { | |||
isProcessing = false; | |||
if (requestQueue.length > 0) { | |||
setTimeout(processBatch, batchDelay); | |||
} | |||
}); | |||
} | } | ||
function fetchSparkList(cardId, api) { | |||
return new Promise(function(resolve) { | |||
api.get({ | |||
action: 'parse', | |||
text: '{{Card/spark/list|' + cardId + '}}', | |||
prop: 'text', | |||
disablelimitreport: true, | |||
format: 'json' | |||
}).done(function(data) { | |||
var html = ''; | |||
if (data.parse && data.parse.text) { | |||
html = data.parse.text['*']; | |||
SparkCache.set(cardId, html); | |||
} | |||
var callbacks = pendingRequests[cardId] || []; | |||
delete pendingRequests[cardId]; | |||
callbacks.forEach(function(cb) { | |||
cb.resolve(html); | |||
}); | |||
resolve(); | |||
}).fail(function(error) { | |||
var callbacks = pendingRequests[cardId] || []; | |||
delete pendingRequests[cardId]; | |||
callbacks.forEach(function(cb) { | |||
cb.reject(error); | |||
}); | |||
resolve(); | |||
}); | |||
}); | |||
} | |||
return { | |||
request: function(cardId) { | |||
return new Promise(function(resolve, reject) { | |||
var cached = SparkCache.get(cardId); | |||
if (cached !== null) { | |||
resolve(cached); | |||
return; | |||
} | |||
if (pendingRequests[cardId]) { | |||
pendingRequests[cardId].push({ resolve: resolve, reject: reject }); | |||
return; | |||
} | |||
pendingRequests[cardId] = [{ resolve: resolve, reject: reject }]; | |||
requestQueue.push(cardId); | |||
if (!isProcessing) { | |||
setTimeout(processBatch, 10); | |||
} | |||
}); | }); | ||
}, | |||
preload: function(cardIds) { | |||
if (!getApi()) return; // API 不可用时不预加载 | |||
var toLoad = cardIds.filter(function(cardId) { | |||
return !SparkCache.has(cardId) && | |||
!pendingRequests[cardId] && | |||
requestQueue.indexOf(cardId) === -1; | |||
}); | }); | ||
if (toLoad.length === 0) return; | |||
toLoad.forEach(function(cardId) { | |||
pendingRequests[cardId] = []; | |||
requestQueue.push(cardId); | |||
}); | |||
if (!isProcessing) { | if (!isProcessing) { | ||
setTimeout(processBatch, | setTimeout(processBatch, 100); | ||
} | } | ||
}, | |||
prioritize: function(cardId) { | |||
var idx = requestQueue.indexOf(cardId); | |||
if (idx > 0) { | |||
requestQueue.splice(idx, 1); | |||
requestQueue.unshift(cardId); | |||
} | |||
} | } | ||
} | }; | ||
})(); | |||
// ==================== DOM 管理器 ==================== | |||
var DOMManager = (function() { | |||
var modalOverlay = null; | |||
var sparkModalOverlay = null; | |||
var modalContent = null; | |||
var sparkModalContent = null; | |||
var currentEnlargedContainer = null; | |||
var processedCards = new WeakSet(); | |||
function createModals() { | |||
if (!modalOverlay) { | |||
if ( | modalOverlay = document.createElement('div'); | ||
modalOverlay.className = 'card-modal-overlay'; | |||
modalOverlay.innerHTML = '<span class="card-modal-close">×</span><div class="card-modal-content"></div>'; | |||
document.body.appendChild(modalOverlay); | |||
modalContent = modalOverlay.querySelector('.card-modal-content'); | |||
} | |||
if (!sparkModalOverlay) { | |||
sparkModalOverlay = document.createElement('div'); | |||
sparkModalOverlay.className = 'spark-modal-overlay'; | |||
sparkModalOverlay.innerHTML = '<span class="spark-modal-close">×</span><div class="spark-modal-content"></div>'; | |||
document.body.appendChild(sparkModalOverlay); | |||
sparkModalContent = sparkModalOverlay.querySelector('.spark-modal-content'); | |||
} | } | ||
} | } | ||
function closeModal() { | |||
if (!modalOverlay) return; | |||
modalOverlay.classList.add('fade-out'); | |||
setTimeout(function() { | |||
modalOverlay.classList.remove('active', 'fade-in', 'fade-out'); | |||
modalContent.innerHTML = ''; | |||
document.body.classList.remove('card-modal-open'); | |||
}, 300); | |||
} | } | ||
if (!sparkModalOverlay) | function closeSparkModal() { | ||
sparkModalOverlay | closeEnlargedCard(); | ||
sparkModalOverlay. | |||
if (!sparkModalOverlay) return; | |||
sparkModalOverlay.classList.add('fade-out'); | |||
setTimeout(function() { | |||
sparkModalOverlay.classList.remove('active', 'fade-in', 'fade-out'); | |||
sparkModalContent.innerHTML = ''; | |||
}, 300); | |||
} | } | ||
function closeEnlargedCard() { | |||
if (currentEnlargedContainer) { | |||
currentEnlargedContainer.remove(); | |||
currentEnlargedContainer = null; | |||
} | |||
} | } | ||
var cardClone = cardElement.cloneNode(true); | function openModal(cardElement, cardId, sparkEnable) { | ||
createModals(); | |||
var cardClone = cardElement.cloneNode(true); | |||
modalContent.innerHTML = ''; | |||
modalContent.appendChild(cardClone); | |||
if (SparkCache.has(cardId)) { | // 只有在 API 可用且启用闪光时才添加按钮 | ||
if (sparkEnable && sparkEnable.trim() !== '' && getApi()) { | |||
var sparkButton = document.createElement('button'); | |||
sparkButton.className = 'spark-button'; | |||
sparkButton.textContent = '闪光'; | |||
sparkButton.dataset.cardId = cardId; | |||
if (SparkCache.has(cardId)) { | |||
sparkButton.classList.add('cached'); | |||
} else { | |||
BatchRequestManager.prioritize(cardId); | |||
} | |||
modalContent.appendChild(sparkButton); | |||
} | } | ||
modalOverlay.classList.add('active'); | |||
document.body.classList.add('card-modal-open'); | |||
requestAnimationFrame(function() { | |||
modalOverlay.classList.add('fade-in'); | |||
}); | |||
} | } | ||
function openSparkModal(cardId) { | |||
createModals(); | |||
var cachedHtml = SparkCache.get(cardId); | |||
BatchRequestManager.request(cardId).then(function(html) { | if (cachedHtml) { | ||
showSparkContent(cachedHtml, true); | |||
} else { | |||
sparkModalContent.innerHTML = '<div class="spark-modal-title">闪光卡牌列表</div><div class="spark-loading">加载中</div>'; | |||
sparkModalOverlay.classList.add('active'); | |||
requestAnimationFrame(function() { | |||
sparkModalOverlay.classList.add('fade-in'); | |||
}); | |||
BatchRequestManager.request(cardId).then(function(html) { | |||
showSparkContent(html, false); | |||
}).catch(function() { | |||
sparkModalContent.innerHTML = '<div class="spark-modal-title">闪光卡牌列表</div><div class="spark-empty">加载失败,请重试</div>'; | |||
}); | |||
} | |||
} | } | ||
var cacheHint = fromCache ? '<div class="spark-cache-hint">已从缓存加载</div>' : ''; | function showSparkContent(html, fromCache) { | ||
var tempDiv = document.createElement('div'); | |||
tempDiv.innerHTML = html; | |||
var cards = tempDiv.querySelectorAll('.card-deck-trans'); | |||
var cacheHint = fromCache ? '<div class="spark-cache-hint">已从缓存加载</div>' : ''; | |||
if (cards.length > 0) { | |||
sparkModalContent.innerHTML = '<div class="spark-modal-title">闪光卡牌列表</div>' + html + cacheHint; | |||
} else { | |||
sparkModalContent.innerHTML = '<div class="spark-modal-title">闪光卡牌列表</div><div class="spark-empty">暂无闪光卡牌</div>' + cacheHint; | |||
} | |||
if (!sparkModalOverlay.classList.contains('active')) { | |||
sparkModalOverlay.classList.add('active'); | |||
requestAnimationFrame(function() { | |||
sparkModalOverlay.classList.add('fade-in'); | |||
}); | |||
} | |||
} | } | ||
function showEnlargedCard(cardElement) { | |||
closeEnlargedCard(); | |||
var enlargedContainer = document.createElement('div'); | |||
enlargedContainer.className = 'spark-enlarged-container'; | |||
var closeBtn = document.createElement('span'); | |||
closeBtn.className = 'spark-enlarged-close'; | |||
closeBtn.innerHTML = '×'; | |||
enlargedContainer.appendChild(closeBtn); | |||
var cardContent = document.createElement('div'); | |||
cardContent.className = 'spark-enlarged-card'; | |||
cardContent.appendChild(cardElement.cloneNode(true)); | |||
enlargedContainer.appendChild(cardContent); | |||
document.body.appendChild(enlargedContainer); | |||
currentEnlargedContainer = enlargedContainer; | |||
} | } | ||
function processNewCards(nodes) { | |||
var sparkCardIds = []; | |||
var cards = []; | nodes.forEach(function(node) { | ||
if (node.nodeType !== 1) return; | |||
var cards = []; | |||
if (node.classList && node.classList.contains('card-deck-trans')) { | |||
cards.push(node); | |||
} | |||
if (node.querySelectorAll) { | |||
var found = node.querySelectorAll('.card-deck-trans'); | |||
for (var i = 0; i < found.length; i++) { | |||
cards.push(found[i]); | |||
} | |||
} | } | ||
var sparkEnable = card.dataset.sparkEnable; | cards.forEach(function(card) { | ||
if (processedCards.has(card)) return; | |||
if (card.closest('.card-modal-overlay') || | |||
card.closest('.spark-modal-overlay') || | |||
card.closest('.spark-enlarged-container')) return; | |||
processedCards.add(card); | |||
var sparkEnable = card.dataset.sparkEnable; | |||
if (sparkEnable && sparkEnable.trim() !== '') { | |||
var wrapper = card.closest('.card-wrapper'); | |||
var cardId = wrapper ? wrapper.dataset.cardId : ''; | |||
if (cardId && sparkCardIds.indexOf(cardId) === -1) { | |||
sparkCardIds.push(cardId); | |||
} | |||
} | } | ||
} | }); | ||
}); | }); | ||
if (sparkCardIds.length > 0) { | |||
setTimeout(function() { | |||
BatchRequestManager.preload(sparkCardIds); | |||
}, 2000); | |||
} | |||
} | |||
function initExistingCards() { | |||
var cards = document.querySelectorAll('.card-deck-trans'); | |||
var nodeArray = []; | |||
} | cards.forEach(function(card) { | ||
nodeArray.push(card); | |||
}); | |||
processNewCards(nodeArray); | |||
} | } | ||
return { | return { | ||
createModals: createModals, | |||
closeModal: closeModal, | |||
closeSparkModal: closeSparkModal, | |||
closeEnlargedCard: closeEnlargedCard, | |||
openModal: openModal, | |||
openSparkModal: openSparkModal, | |||
showEnlargedCard: showEnlargedCard, | |||
processNewCards: processNewCards, | |||
initExistingCards: initExistingCards, | |||
getModalOverlay: function() { return modalOverlay; }, | |||
getSparkModalOverlay: function() { return sparkModalOverlay; } | |||
}; | }; | ||
} | })(); | ||
function | // ==================== 事件委托管理器 ==================== | ||
var | var EventManager = (function() { | ||
var hoverTimer = null; | |||
var | function getCardInfo(cardElement) { | ||
var wrapper = cardElement.closest('.card-wrapper'); | |||
return { | |||
cardId: wrapper ? wrapper.dataset.cardId : '', | |||
sparkEnable: cardElement.dataset.sparkEnable || '' | |||
}; | |||
} | |||
function isValidCardClick(target) { | |||
var card = target.closest('.card-deck-trans'); | |||
if (!card) return null; | |||
var inMainModal = card.closest('.card-modal-content'); | |||
var inSparkList = card.closest('.spark-card-list'); | |||
var inEnlarged = card.closest('.spark-enlarged-container'); | |||
if ( | if (inMainModal && !inSparkList) return null; | ||
if (inEnlarged) return null; | |||
if (target.classList.contains('spark-enlarged-close')) { | return card; | ||
} | |||
function init() { | |||
document.addEventListener('click', function(e) { | |||
var target = e.target; | |||
if (target.classList.contains('card-modal-close')) { | |||
e.stopPropagation(); | |||
if ( | DOMManager.closeModal(); | ||
DOMManager. | return; | ||
} | |||
if (target.classList.contains('spark-modal-close')) { | |||
e.stopPropagation(); | |||
DOMManager.closeSparkModal(); | |||
return; | |||
} | |||
if (target.classList.contains('spark-enlarged-close')) { | |||
e.stopPropagation(); | |||
DOMManager.closeEnlargedCard(); | |||
return; | |||
} | |||
if (target.classList.contains('spark-button')) { | |||
e.preventDefault(); | |||
var cardId = target.dataset.cardId; | |||
if (cardId) { | |||
DOMManager.openSparkModal(cardId); | |||
} | |||
return; | |||
} | |||
if (target.classList.contains('card-modal-overlay')) { | |||
DOMManager.closeModal(); | |||
return; | |||
} | |||
if (target.classList.contains('spark-modal-overlay')) { | |||
DOMManager.closeSparkModal(); | |||
return; | |||
} | |||
if (target.classList.contains('spark-enlarged-container')) { | |||
DOMManager.closeEnlargedCard(); | |||
return; | |||
} | |||
var card = isValidCardClick(target); | |||
if (card) { | |||
e.preventDefault(); | |||
var inSparkList = card.closest('.spark-card-list'); | |||
var info = getCardInfo(card); | |||
if (inSparkList) { | |||
DOMManager.showEnlargedCard(card); | |||
} else { | |||
DOMManager.openModal(card, info.cardId, info.sparkEnable); | |||
} | |||
} | } | ||
}, true); | |||
} | |||
document.addEventListener('mouseenter', function(e) { | |||
var card = e.target.closest && e.target.closest('.card-deck-trans'); | |||
if (!card) return; | |||
if (card.closest('.card-modal-overlay') || | |||
card.closest('.spark-modal-overlay') || | |||
card.closest('.spark-enlarged-container')) return; | |||
var info = getCardInfo(card); | var info = getCardInfo(card); | ||
if (info.sparkEnable && info.sparkEnable.trim() !== '' && info.cardId) { | |||
if (hoverTimer) { | |||
clearTimeout(hoverTimer); | |||
} | |||
hoverTimer = setTimeout(function() { | |||
BatchRequestManager.prioritize(info.cardId); | |||
if (!SparkCache.has(info.cardId)) { | |||
BatchRequestManager.request(info.cardId).catch(function() { | |||
// 忽略预加载错误 | |||
}); | |||
} | |||
}, 150); | |||
} | } | ||
}, true); | |||
document.addEventListener('mouseleave', function(e) { | |||
card.closest | var card = e.target.closest && e.target.closest('.card-deck-trans'); | ||
if (card && hoverTimer) { | |||
clearTimeout(hoverTimer); | clearTimeout(hoverTimer); | ||
hoverTimer = null; | |||
} | } | ||
}, true); | |||
document.addEventListener('keydown', function(e) { | |||
if ( | if (e.key === 'Escape') { | ||
var sparkModal = DOMManager.getSparkModalOverlay(); | |||
var mainModal = DOMManager.getModalOverlay(); | |||
if (document.querySelector('.spark-enlarged-container')) { | |||
DOMManager.closeEnlargedCard(); | |||
} else if (sparkModal && sparkModal.classList.contains('active')) { | |||
DOMManager.closeSparkModal(); | |||
} else if (mainModal && mainModal.classList.contains('active')) { | |||
DOMManager.closeModal(); | |||
} | } | ||
} | } | ||
} | }); | ||
window.addEventListener('beforeunload', function() { | |||
SparkCache.flush(); | |||
}); | |||
document.addEventListener('visibilitychange', function() { | |||
if (document.hidden) { | |||
SparkCache.flush(); | |||
} | |||
}); | |||
} | |||
return { | |||
init: init | |||
} | }; | ||
})(); | |||
// ==================== 初始化 ==================== | |||
SparkCache.cleanup(); | SparkCache.cleanup(); | ||
DOMManager.createModals(); | DOMManager.createModals(); | ||
| 第1,079行: | 第1,091行: | ||
} | } | ||
// 启动 | // ==================== 启动 ==================== | ||
if (document.readyState === 'loading') { | function start() { | ||
if (document.readyState === 'loading') { | |||
document.addEventListener('DOMContentLoaded', function() { | |||
waitForMw(initCardWidget); | |||
}); | |||
} else { | |||
waitForMw(initCardWidget); | |||
} | |||
} | } | ||
}); | |||
start(); | |||
})(); | |||
</script> | </script> | ||
</includeonly> | </includeonly> | ||
2026年1月17日 (六) 10:41的版本
此Widget为卡牌显示添加交互效果:
- 鼠标悬停时的动画效果
- 点击后显示放大的模态框
- 自动检测spark_enable属性显示闪光按钮
- 支持闪光卡牌列表显示
- 预加载、批量加载和本地缓存优化
- 事件委托、DOM优化、批量API请求
使用方法:在页面中添加