微件:Card:修订间差异
跳转到导航
跳转到搜索
功能测试 |
小 功能测试 |
||
| 第1行: | 第1行: | ||
<includeonly> | |||
<style> | <style> | ||
.card-wrapper { | .card-wrapper { | ||
display: inline-block; | display: inline-block; | ||
| 第254行: | 第243行: | ||
50% { content: ".."; } | 50% { content: ".."; } | ||
75% { content: "..."; } | 75% { content: "..."; } | ||
} | |||
/* 加载进度 */ | |||
.spark-loading-progress { | |||
color: #aaa; | |||
font-size: 14px; | |||
margin-top: 10px; | |||
} | } | ||
| 第352行: | 第348行: | ||
.spark-modal-content::-webkit-scrollbar-thumb:hover { | .spark-modal-content::-webkit-scrollbar-thumb:hover { | ||
background: rgba(255, 255, 255, 0.5); | background: rgba(255, 255, 255, 0.5); | ||
} | |||
/* 预加载指示器 */ | |||
.preload-indicator { | |||
position: fixed; | |||
bottom: 10px; | |||
right: 10px; | |||
background: rgba(0, 0, 0, 0.7); | |||
color: #fff; | |||
padding: 5px 10px; | |||
border-radius: 4px; | |||
font-size: 12px; | |||
z-index: 9998; | |||
opacity: 0; | |||
transition: opacity 0.3s ease; | |||
pointer-events: none; | |||
} | |||
.preload-indicator.active { | |||
opacity: 1; | |||
} | } | ||
</style> | </style> | ||
| 第359行: | 第375行: | ||
'use strict'; | 'use strict'; | ||
// ==================== | // ==================== 缓存管理器 ==================== | ||
function | var SparkCache = { | ||
// | CACHE_KEY: 'spark_card_cache', | ||
CACHE_VERSION: 'v1', | |||
CACHE_EXPIRE: 24 * 60 * 60 * 1000, // 24小时过期 | |||
} | |||
// | // 获取完整的缓存键 | ||
getFullKey: function() { | |||
} | return this.CACHE_KEY + '_' + this.CACHE_VERSION; | ||
// | }, | ||
var | |||
var | // 获取所有缓存 | ||
getAll: function() { | |||
try { | |||
var data = localStorage.getItem(this.getFullKey()); | |||
if (!data) return {}; | |||
return JSON.parse(data); | |||
} catch (e) { | |||
console.warn('SparkCache: 读取缓存失败', e); | |||
return {}; | |||
} | |||
}, | |||
// 保存所有缓存 | |||
saveAll: function(data) { | |||
try { | |||
localStorage.setItem(this.getFullKey(), JSON.stringify(data)); | |||
} catch (e) { | |||
console.warn('SparkCache: 保存缓存失败', e); | |||
// 存储空间不足时,清理过期缓存 | |||
this.cleanup(); | |||
} | |||
}, | |||
// 获取单个卡牌的闪光列表缓存 | |||
get: function(cardId) { | |||
var cache = this.getAll(); | |||
var item = cache[cardId]; | |||
if (!item) return null; | |||
// 检查是否过期 | |||
if (Date.now() - item.timestamp > this.CACHE_EXPIRE) { | |||
delete cache[cardId]; | |||
this.saveAll(cache); | |||
return null; | |||
} | |||
return item.html; | |||
}, | |||
// 设置单个卡牌的闪光列表缓存 | |||
set: function(cardId, html) { | |||
var cache = this.getAll(); | |||
cache[cardId] = { | |||
html: html, | |||
timestamp: Date.now() | |||
}; | |||
this.saveAll(cache); | |||
}, | |||
// 检查是否有缓存 | |||
has: function(cardId) { | |||
return this.get(cardId) !== null; | |||
}, | |||
// 清理过期缓存 | |||
cleanup: function() { | |||
var cache = this.getAll(); | |||
var now = Date.now(); | |||
var cleaned = false; | |||
var | for (var cardId in cache) { | ||
if (cache.hasOwnProperty(cardId)) { | |||
if (now - cache[cardId].timestamp > this.CACHE_EXPIRE) { | |||
if ( | delete cache[cardId]; | ||
cleaned = true; | |||
} | |||
} | } | ||
}, | } | ||
if (cleaned) { | |||
this.saveAll(cache); | |||
} | |||
}, | |||
// 清空所有缓存 | |||
clear: function() { | |||
try { | |||
localStorage.removeItem(this.getFullKey()); | |||
} catch (e) { | |||
console.warn('SparkCache: 清空缓存失败', e); | |||
} | |||
} | } | ||
} | }; | ||
// ==================== | // ==================== 预加载队列管理器 ==================== | ||
var PreloadQueue = { | |||
queue: [], | |||
isProcessing: false, | |||
batchSize: 3, // 并发请求数 | |||
activeRequests: 0, | |||
maxRetries: 2, | |||
retryDelay: 1000, | |||
// 预加载指示器 | |||
indicator: null, | |||
// === | // 初始化指示器 | ||
initIndicator: function() { | |||
if (!this.indicator) { | |||
this.indicator = document.createElement('div'); | |||
this.indicator.className = 'preload-indicator'; | |||
this.indicator.textContent = '预加载中...'; | |||
document.body.appendChild(this.indicator); | |||
} | |||
}, | |||
function | // 显示/隐藏指示器 | ||
if ( | showIndicator: function(show) { | ||
this.initIndicator(); | |||
if (show) { | |||
this.indicator.classList.add('active'); | |||
} else { | |||
this.indicator.classList.remove('active'); | |||
} | } | ||
}, | |||
// 更新指示器文本 | |||
updateIndicator: function(current, total) { | |||
this.initIndicator(); | |||
this.indicator.textContent = '预加载: ' + current + '/' + total; | |||
}, | |||
// 添加到预加载队列 | |||
add: function(cardId, priority) { | |||
// 如果已缓存,跳过 | |||
if (SparkCache.has(cardId)) return; | |||
// 如果已在队列中,跳过 | |||
for (var i = 0; i < this.queue.length; i++) { | |||
if (this.queue[i].cardId === cardId) return; | |||
} | } | ||
this.queue.push({ | |||
cardId: cardId, | |||
priority: priority || 0, | |||
retries: 0 | |||
}); | |||
// 按优先级排序(高优先级在前) | |||
this.queue.sort(function(a, b) { | |||
return b.priority - a.priority; | |||
}); | |||
} | |||
function | this.process(); | ||
}, | |||
// 批量添加到预加载队列 | |||
addBatch: function(cardIds, priority) { | |||
var self = this; | |||
cardIds.forEach(function(cardId) { | |||
self.add(cardId, priority); | |||
}); | |||
}, | |||
// 提升优先级(用于用户即将查看的卡牌) | |||
prioritize: function(cardId) { | |||
for (var i = 0; i < this.queue.length; i++) { | |||
if (this.queue[i].cardId === cardId) { | |||
this.queue[i].priority = 100; | |||
break; | |||
} | } | ||
} | } | ||
this.queue.sort(function(a, b) { | |||
return b.priority - a.priority; | |||
}); | |||
}, | |||
// 处理队列 | |||
process: function() { | |||
var self = this; | |||
if (this.queue.length === 0) { | |||
this.showIndicator(false); | |||
return; | |||
} | } | ||
// 控制并发数 | |||
while (this.activeRequests < this.batchSize && this.queue.length > 0) { | |||
var item = this.queue.shift(); | |||
this.loadSparkList(item); | |||
} | } | ||
if (this.activeRequests > 0) { | |||
this.showIndicator(true); | |||
this.updateIndicator(this.activeRequests, this.queue.length + this.activeRequests); | |||
} | |||
}, | |||
} | |||
} | |||
// | // 加载闪光卡牌列表 | ||
loadSparkList: function(item) { | |||
var | var self = this; | ||
this.activeRequests++; | |||
var api = new mw.Api(); | |||
api.get({ | |||
action: 'parse', | |||
text: '{{Card/spark/list|' + item.cardId + '}}', | |||
prop: 'text', | |||
disablelimitreport: true, | |||
format: 'json' | |||
}).done(function(data) { | |||
self.activeRequests--; | |||
if (data.parse && data.parse.text) { | |||
if ( | var html = data.parse.text['*']; | ||
SparkCache.set(item.cardId, html); | |||
// 更新按钮状态 | |||
self.updateButtonState(item.cardId, true); | |||
} | } | ||
self.process(); | |||
}).fail(function() { | |||
self.activeRequests--; | |||
// 重试逻辑 | |||
if (item.retries < self.maxRetries) { | |||
item.retries++; | |||
} | setTimeout(function() { | ||
self.queue.unshift(item); | |||
self.process(); | |||
}, self.retryDelay * item.retries); | |||
} | |||
self.process(); | |||
}); | |||
}, | |||
// 更新按钮缓存状态 | |||
updateButtonState: function(cardId, cached) { | |||
var buttons = document.querySelectorAll('.spark-button[data-card-id="' + cardId + '"]'); | |||
buttons.forEach(function(btn) { | |||
if (cached) { | |||
btn.classList.add('cached'); | |||
btn.classList.remove('loading'); | |||
} | |||
}); | |||
} | |||
}; | |||
// ==================== 主逻辑 ==================== | |||
// 确保DOM加载完成 | |||
if (document.readyState === 'loading') { | |||
document.addEventListener('DOMContentLoaded', initCardWidget); | |||
} else { | |||
initCardWidget(); | |||
} | |||
function initCardWidget() { | |||
// 清理过期缓存 | |||
SparkCache.cleanup(); | |||
// 创建主模态框元素(如果不存在) | |||
var modalOverlay = document.querySelector('.card-modal-overlay'); | |||
if (!modalOverlay) { | |||
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); | |||
} | |||
// 创建闪光卡牌模态框(如果不存在) | |||
var sparkModalOverlay = document.querySelector('.spark-modal-overlay'); | |||
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); | |||
} | |||
var modalContent = modalOverlay.querySelector('.card-modal-content'); | |||
var closeButton = modalOverlay.querySelector('.card-modal-close'); | |||
var sparkModalContent = sparkModalOverlay.querySelector('.spark-modal-content'); | |||
var sparkCloseButton = sparkModalOverlay.querySelector('.spark-modal-close'); | |||
// 当前放大的闪光卡牌容器 | |||
var currentEnlargedContainer = null; | |||
// 关闭主模态框函数 | |||
function closeModal() { | |||
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); | |||
} | |||
// 关闭闪光模态框函数 | |||
function closeSparkModal() { | |||
// 先关闭放大的卡牌 | |||
if (currentEnlargedContainer) { | |||
currentEnlargedContainer.remove(); | |||
currentEnlargedContainer = null; | |||
} | } | ||
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; | |||
} | } | ||
} | |||
// 打开主模态框函数 | |||
function openModal(cardElement, cardId, sparkEnable) { | |||
var cardClone = cardElement.cloneNode(true); | |||
// 移除克隆卡牌的点击事件标记 | |||
delete cardClone.dataset.cardBound; | |||
modalContent.innerHTML = ''; | |||
modalContent.appendChild(cardClone); | |||
// 如果启用了闪光功能,添加闪光按钮 | |||
if (sparkEnable && sparkEnable.trim() !== '') { | |||
var sparkButton = document.createElement('button'); | |||
sparkButton.className = 'spark-button'; | |||
sparkButton.textContent = '闪光'; | |||
sparkButton.dataset.cardId = cardId; | |||
if ( | // 检查是否已缓存 | ||
if (SparkCache.has(cardId)) { | |||
sparkButton.classList.add('cached'); | |||
} else { | |||
// 提升优先级预加载 | |||
PreloadQueue.prioritize(cardId); | |||
} | } | ||
sparkButton.addEventListener('click', function() { | |||
openSparkModal(cardId); | |||
}); | |||
modalContent.appendChild(sparkButton); | |||
} | } | ||
modalOverlay.classList.add('active'); | |||
document.body.classList.add('card-modal-open'); | |||
function | // 强制重绘后添加fade-in | ||
requestAnimationFrame(function() { | |||
modalOverlay.classList.add('fade-in'); | |||
}); | |||
} | |||
// 打开闪光卡牌模态框 | |||
function openSparkModal(cardId) { | |||
// 首先检查缓存 | |||
var cachedHtml = SparkCache.get(cardId); | |||
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() { | requestAnimationFrame(function() { | ||
sparkModalOverlay.classList.add('fade-in'); | |||
}); | }); | ||
// 发起请求 | |||
loadSparkListDirect(cardId); | |||
} | } | ||
function showSparkContent(html, fromCache) { | function showSparkContent(html, fromCache) { | ||
// 检查是否有卡牌 | |||
var tempDiv = document.createElement('div'); | var tempDiv = document.createElement('div'); | ||
tempDiv.innerHTML = html; | tempDiv.innerHTML = html; | ||
| 第805行: | 第771行: | ||
if (cards.length > 0) { | if (cards.length > 0) { | ||
sparkModalContent.innerHTML = '<div class="spark-modal-title">闪光卡牌列表</div>' + html + cacheHint; | sparkModalContent.innerHTML = '<div class="spark-modal-title">闪光卡牌列表</div>' + html + cacheHint; | ||
// 为闪光列表中的卡牌绑定事件 | |||
bindSparkListCardEvents(); | |||
} else { | } else { | ||
sparkModalContent.innerHTML = '<div class="spark-modal-title">闪光卡牌列表</div><div class="spark-empty">暂无闪光卡牌</div>' + cacheHint; | sparkModalContent.innerHTML = '<div class="spark-modal-title">闪光卡牌列表</div><div class="spark-empty">暂无闪光卡牌</div>' + cacheHint; | ||
| 第817行: | 第785行: | ||
} | } | ||
function | function loadSparkListDirect(cardId) { | ||
var api = new mw.Api(); | |||
api.get({ | |||
var | action: 'parse', | ||
text: '{{Card/spark/list|' + cardId + '}}', | |||
prop: 'text', | |||
disablelimitreport: true, | |||
format: 'json' | |||
}).done(function(data) { | |||
if (data.parse && data.parse.text) { | |||
var html = data.parse.text['*']; | |||
// 存入缓存 | |||
SparkCache.set(cardId, html); | |||
if ( | |||
var | |||
// 更新按钮状态 | |||
PreloadQueue.updateButtonState(cardId, true); | |||
// 显示内容 | |||
showSparkContent(html, false); | |||
} | |||
}).fail(function() { | |||
sparkModalContent.innerHTML = '<div class="spark-modal-title">闪光卡牌列表</div><div class="spark-empty">加载失败,请重试</div>'; | |||
}); | }); | ||
} | } | ||
} | |||
} | |||
// | // 为闪光列表中的卡牌绑定点击事件 | ||
function bindSparkListCardEvents() { | |||
var | var sparkCards = sparkModalContent.querySelectorAll('.card-deck-trans'); | ||
sparkCards.forEach(function(card) { | |||
if (card.dataset.sparkBound) return; | |||
card.dataset.sparkBound = 'true'; | |||
if ( | |||
card.addEventListener('click', function(e) { | |||
e.preventDefault(); | |||
e.stopPropagation(); | |||
// 关闭之前放大的卡牌 | |||
closeEnlargedCard(); | |||
// 创建放大容器 | |||
var enlargedContainer = document.createElement('div'); | |||
enlargedContainer.className = 'spark-enlarged-container'; | |||
// 添加关闭按钮 | |||
var closeBtn = document.createElement('span'); | |||
closeBtn.className = 'spark-enlarged-close'; | |||
closeBtn.innerHTML = '×'; | |||
closeBtn.addEventListener('click', function(e) { | |||
e.stopPropagation(); | e.stopPropagation(); | ||
closeEnlargedCard(); | |||
}); | |||
enlargedContainer.appendChild(closeBtn); | |||
// 创建卡牌内容区 | |||
var cardContent = document.createElement('div'); | |||
cardContent.className = 'spark-enlarged-card'; | |||
var cardClone = this.cloneNode(true); | |||
delete cardClone.dataset.sparkBound; | |||
cardContent.appendChild(cardClone); | |||
enlargedContainer.appendChild(cardContent); | |||
// 点击背景关闭 | |||
enlargedContainer.addEventListener('click', function(e) { | |||
if (e.target === enlargedContainer) { | |||
closeEnlargedCard(); | |||
if ( | |||
} | } | ||
} | }); | ||
document.body.appendChild(enlargedContainer); | |||
currentEnlargedContainer = enlargedContainer; | |||
}); | |||
}); | |||
} | |||
// 绑定关闭按钮事件 | |||
closeButton.addEventListener('click', function(e) { | |||
e.stopPropagation(); | |||
closeModal(); | |||
}); | |||
sparkCloseButton.addEventListener('click', function(e) { | |||
e.stopPropagation(); | |||
closeSparkModal(); | |||
}); | |||
// 点击遮罩关闭 | |||
modalOverlay.addEventListener('click', function(e) { | |||
if (e.target === modalOverlay) { | |||
closeModal(); | |||
} | |||
}); | |||
sparkModalOverlay.addEventListener('click', function(e) { | |||
if (e.target === sparkModalOverlay) { | |||
closeSparkModal(); | |||
} | |||
}); | |||
// ESC键关闭 | |||
document.addEventListener('keydown', function(e) { | |||
if (e.key === 'Escape') { | |||
// 优先关闭放大的闪光卡牌 | |||
if (currentEnlargedContainer) { | |||
closeEnlargedCard(); | |||
} else if (sparkModalOverlay.classList.contains('active')) { | |||
closeSparkModal(); | |||
} else if (modalOverlay.classList.contains('active')) { | |||
closeModal(); | |||
} | |||
} | |||
}); | |||
// 收集页面上所有启用闪光的卡牌ID | |||
function collectSparkCardIds() { | |||
var cardIds = []; | |||
var cards = document.querySelectorAll('.card-deck-trans[data-spark-enable]'); | |||
cards.forEach(function(card) { | |||
var sparkEnable = card.dataset.sparkEnable; | |||
if (sparkEnable && sparkEnable.trim() !== '') { | |||
var wrapper = card.closest('.card-wrapper'); | |||
var cardId = wrapper ? wrapper.dataset.cardId : ''; | |||
if (cardId && cardIds.indexOf(cardId) === -1) { | |||
cardIds.push(cardId); | |||
} | } | ||
} | } | ||
}); | |||
return cardIds; | |||
} | |||
// 为所有卡牌绑定点击事件 | |||
function bindCardEvents() { | |||
var cards = document.querySelectorAll('.card-deck-trans'); | |||
var sparkCardIds = []; | |||
cards.forEach(function(card) { | |||
// 避免重复绑定 | |||
if (card.dataset.cardBound) return; | |||
card.dataset.cardBound = 'true'; | |||
// 排除模态框内的卡牌 | |||
if (card.closest('.card-modal-overlay') || card.closest('.spark-modal-overlay') || card.closest('.spark-enlarged-container')) return; | |||
// 收集启用闪光的卡牌ID用于预加载 | |||
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); | |||
} | } | ||
} | } | ||
card.addEventListener('click', function(e) { | |||
e.preventDefault(); | |||
// 从card-deck-trans元素获取spark_enable | |||
var sparkEnable = this.dataset.sparkEnable || ''; | |||
// 获取card-id从父级card-wrapper | |||
var wrapper = this.closest('.card-wrapper'); | |||
var cardId = wrapper ? wrapper.dataset.cardId : ''; | |||
openModal(this, cardId, sparkEnable); | |||
}); | }); | ||
// 鼠标悬停时预加载 | |||
if ( | card.addEventListener('mouseenter', function() { | ||
var sparkEnable = this.dataset.sparkEnable || ''; | |||
if (sparkEnable && sparkEnable.trim() !== '') { | |||
var wrapper = this.closest('.card-wrapper'); | |||
var cardId = wrapper ? wrapper.dataset.cardId : ''; | |||
if (cardId) { | |||
// 高优先级预加载 | |||
PreloadQueue.add(cardId, 50); | |||
} | |||
} | } | ||
}); | }); | ||
}); | |||
// 延迟低优先级预加载页面上所有闪光卡牌 | |||
if (sparkCardIds.length > 0) { | |||
setTimeout(function() { | |||
PreloadQueue.addBatch(sparkCardIds, 1); | |||
}, 2000); | |||
} | } | ||
} | |||
} | |||
// | // 初始绑定 | ||
bindCardEvents(); | |||
// 监听DOM变化,为动态加载的卡牌绑定事件 | |||
var observer = new MutationObserver(function(mutations) { | var observer = new MutationObserver(function(mutations) { | ||
var | var shouldBind = false; | ||
mutations.forEach(function(mutation) { | mutations.forEach(function(mutation) { | ||
if (mutation.addedNodes.length > 0) { | if (mutation.addedNodes.length > 0) { | ||
mutation.addedNodes.forEach(function(node) { | |||
if (node.nodeType === 1) { | |||
} | if (node.classList && node.classList.contains('card-deck-trans')) { | ||
shouldBind = true; | |||
} else if (node.querySelector && node.querySelector('.card-deck-trans')) { | |||
shouldBind = true; | |||
} | |||
} | |||
}); | |||
} | } | ||
}); | }); | ||
if (shouldBind) { | |||
bindCardEvents(); | |||
} | } | ||
}); | }); | ||
| 第1,088行: | 第1,005行: | ||
childList: true, | childList: true, | ||
subtree: true | subtree: true | ||
}); | |||
// 页面可见性变化时暂停/恢复预加载 | |||
document.addEventListener('visibilitychange', function() { | |||
if (document.hidden) { | |||
// 页面不可见时,暂停预加载(通过不处理队列) | |||
} else { | |||
// 页面可见时,恢复预加载 | |||
PreloadQueue.process(); | |||
} | |||
}); | }); | ||
} | } | ||
})(); | })(); | ||
</script> | </script> | ||
</includeonly> | </includeonly> | ||