|
|
| (未显示同一用户的5个中间版本) |
| 第1行: |
第1行: |
| <includeonly> | | <noinclude> |
| | 此Widget用于为卡牌添加交互功能。 |
| | 使用方法:在页面底部添加 {{#widget:Card}} |
| | [[分类:Widget]] |
| | </noinclude><includeonly> |
| <style> | | <style> |
| /* 卡牌容器 */ | | /* 卡牌悬停和点击动画 */ |
| .card-wrapper {
| |
| display: inline-block;
| |
| }
| |
| | |
| /* 卡牌悬停动画 */
| |
| .card-deck-trans { | | .card-deck-trans { |
| cursor: pointer; | | cursor: pointer; |
| transition: transform 0.3s ease, box-shadow 0.3s ease, filter 0.3s ease; | | transition: transform 0.2s ease; |
| } | | } |
|
| |
|
| .card-deck-trans:hover { | | .card-deck-trans:hover { |
| transform: translateY(-8px); | | transform: scale(1.05); |
| filter: brightness(1.1); | | z-index: 10; |
| } | | } |
|
| |
|
| .card-deck-trans:active { | | .card-deck-trans:active { |
| transform: translateY(-4px) scale(0.98); | | transform: scale(0.98); |
| } | | } |
|
| |
|
| /* 模态框通用样式 */ | | /* 模态框样式 */ |
| .card-modal-overlay, | | .card-modal-overlay { |
| .spark-modal-overlay {
| |
| display: none; | | display: none; |
| position: fixed; | | position: fixed; |
| 第30行: |
第28行: |
| width: 100%; | | width: 100%; |
| height: 100%; | | height: 100%; |
| background-color: rgba(0, 0, 0, 0.85); | | background-color: rgba(0, 0, 0, 0.8); |
| z-index: 9999; | | z-index: 9999; |
| justify-content: center; | | justify-content: center; |
| align-items: center; | | align-items: center; |
| opacity: 0; | | flex-direction: column; |
| transition: opacity 0.3s ease;
| |
| } | | } |
|
| |
|
| .spark-modal-overlay {
| | .card-modal-overlay.active { |
| z-index: 10010;
| |
| background-color: rgba(0, 0, 0, 0.9);
| |
| }
| |
| | |
| .card-modal-overlay.active, | |
| .spark-modal-overlay.active {
| |
| display: flex; | | display: flex; |
| opacity: 1;
| |
| }
| |
|
| |
| .card-modal-overlay.fade-in,
| |
| .spark-modal-overlay.fade-in {
| |
| opacity: 1;
| |
| }
| |
|
| |
| .card-modal-overlay.fade-out,
| |
| .spark-modal-overlay.fade-out {
| |
| opacity: 0;
| |
| } | | } |
|
| |
|
| /* 关闭按钮 */ | | /* 关闭按钮 */ |
| .card-modal-close, | | .card-modal-close { |
| .spark-modal-close {
| | position: absolute; |
| position: fixed; | |
| top: 20px; | | top: 20px; |
| right: 30px; | | right: 30px; |
| font-size: 48px; | | font-size: 40px; |
| font-weight: bold;
| | color: #fff; |
| color: #ffffff; | |
| cursor: pointer; | | cursor: pointer; |
| z-index: 10001; | | z-index: 10001; |
| line-height: 1; | | line-height: 1; |
| transition: color 0.2s ease, transform 0.2s ease;
| |
| text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
| |
| } | | } |
|
| |
|
| .spark-modal-close {
| | .card-modal-close:hover { |
| z-index: 10012;
| |
| }
| |
| | |
| .card-modal-close:hover, | |
| .spark-modal-close:hover {
| |
| color: #ff6b6b; | | color: #ff6b6b; |
| transform: scale(1.1);
| |
| } | | } |
|
| |
|
| /* 模态框中的卡牌容器 */ | | /* 模态框内容容器 */ |
| .card-modal-content { | | .card-modal-content { |
| position: relative;
| |
| z-index: 10000;
| |
| display: flex; | | display: flex; |
| flex-direction: column; | | flex-direction: column; |
| align-items: center; | | align-items: center; |
| animation: cardZoomIn 0.3s ease; | | justify-content: center; |
| } | | } |
|
| |
|
| .spark-modal-content { | | /* 放大后的卡牌容器 */ |
| | .card-modal-card { |
| position: relative; | | position: relative; |
| z-index: 10011;
| | width: 270px; |
| max-width: 90vw;
| | height: 384px; |
| max-height: 85vh;
| | display: inline-block; |
| overflow-y: auto;
| |
| padding: 20px;
| |
| animation: cardZoomIn 0.3s ease;
| |
| }
| |
| | |
| @keyframes cardZoomIn {
| |
| from {
| |
| transform: scale(0.8);
| |
| opacity: 0;
| |
| }
| |
| to {
| |
| transform: scale(1);
| |
| opacity: 1;
| |
| }
| |
| }
| |
| | |
| /* 放大后的卡牌样式 */
| |
| .card-modal-content > .card-deck-trans,
| |
| .card-modal-content > .card-wrapper > .card-deck-trans {
| |
| width: 270px !important; | |
| height: 384px !important; | |
| transform: none !important;
| |
| filter: none !important;
| |
| cursor: default; | |
| }
| |
| | |
| .card-modal-content > .card-deck-trans:hover,
| |
| .card-modal-content > .card-wrapper > .card-deck-trans:hover {
| |
| transform: none !important;
| |
| filter: none !important;
| |
| } | | } |
|
| |
|
| .card-modal-content > .card-deck-trans > .card-deck, | | .card-modal-card .card-deck { |
| .card-modal-content > .card-wrapper > .card-deck-trans > .card-deck {
| | position: absolute; |
| transform: scale(0.6) !important; | | width: 450px; |
| | height: 640px; |
| | transform: scale(0.6); |
| | transform-origin: top left; |
| } | | } |
|
| |
|
| /* 闪光按钮 */ | | /* 闪光按钮 */ |
| .spark-button { | | .card-spark-button { |
| margin-top: 20px; | | margin-top: 20px; |
| padding: 12px 36px; | | padding: 10px 30px; |
| font-size: 18px; | | background-color: #4a4a4a; |
| font-weight: bold;
| | color: #ffd700; |
| color: #fff;
| | border: 1px solid #ffd700; |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | | border-radius: 4px; |
| border: 2px solid #a78bfa; | |
| border-radius: 8px; | |
| cursor: pointer;
| |
| transition: all 0.3s ease;
| |
| box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
| |
| text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
| |
| }
| |
| | |
| .spark-button:hover {
| |
| background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
| |
| box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
| |
| transform: translateY(-2px);
| |
| }
| |
| | |
| .spark-button:active {
| |
| transform: translateY(0);
| |
| box-shadow: 0 2px 10px rgba(102, 126, 234, 0.4);
| |
| }
| |
| | |
| /* 闪光按钮状态 */
| |
| .spark-button.loading {
| |
| opacity: 0.7;
| |
| cursor: wait;
| |
| }
| |
| | |
| .spark-button.cached::after {
| |
| content: " ✓";
| |
| color: #90EE90;
| |
| }
| |
| | |
| /* 闪光图标动画 */
| |
| .spark-button:not(.loading)::before {
| |
| content: "✦ ";
| |
| animation: sparkle 1.5s ease-in-out infinite;
| |
| }
| |
| | |
| @keyframes sparkle {
| |
| 0%, 100% { opacity: 0.5; }
| |
| 50% { opacity: 1; }
| |
| }
| |
| | |
| /* 闪光卡牌列表样式 */
| |
| .spark-modal-title {
| |
| text-align: center;
| |
| color: #fff;
| |
| font-size: 24px;
| |
| font-weight: bold;
| |
| margin-bottom: 20px;
| |
| text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
| |
| }
| |
| | |
| .spark-modal-title::before {
| |
| content: "✦ ";
| |
| color: #a78bfa;
| |
| }
| |
| | |
| .spark-modal-title::after {
| |
| content: " ✦";
| |
| color: #a78bfa;
| |
| }
| |
| | |
| .spark-card-list {
| |
| display: flex;
| |
| flex-wrap: wrap;
| |
| justify-content: center;
| |
| gap: 15px;
| |
| }
| |
| | |
| .spark-card-list .card-wrapper {
| |
| display: inline-block;
| |
| }
| |
| | |
| .spark-card-list .card-deck-trans {
| |
| cursor: pointer; | | cursor: pointer; |
| }
| |
|
| |
| .spark-card-list .card-deck-trans:hover {
| |
| transform: translateY(-8px);
| |
| filter: brightness(1.1);
| |
| }
| |
|
| |
| /* 加载提示 */
| |
| .spark-loading {
| |
| color: #fff;
| |
| font-size: 18px;
| |
| text-align: center;
| |
| padding: 40px;
| |
| }
| |
|
| |
| .spark-loading::after {
| |
| content: "";
| |
| animation: loadingDots 1.5s infinite;
| |
| }
| |
|
| |
| @keyframes loadingDots {
| |
| 0% { content: ""; }
| |
| 25% { content: "."; }
| |
| 50% { content: ".."; }
| |
| 75% { content: "..."; }
| |
| }
| |
|
| |
| /* 加载进度 */
| |
| .spark-loading-progress {
| |
| color: #aaa;
| |
| font-size: 14px;
| |
| margin-top: 10px;
| |
| }
| |
|
| |
| /* 空列表提示 */
| |
| .spark-empty {
| |
| color: #999;
| |
| font-size: 16px; | | font-size: 16px; |
| text-align: center;
| |
| padding: 40px;
| |
| } | | } |
|
| |
|
| /* 缓存状态提示 */
| | .card-spark-button:hover { |
| .spark-cache-hint { | | background-color: #5a5a5a; |
| color: #666; | |
| font-size: 12px;
| |
| text-align: center;
| |
| margin-top: 15px;
| |
| } | | } |
|
| |
|
| /* 放大的闪光卡牌 */ | | /* 闪光卡牌模态框 */ |
| .spark-enlarged-container { | | .spark-modal-overlay { |
| | display: none; |
| position: fixed; | | position: fixed; |
| top: 0; | | top: 0; |
| 第276行: |
第103行: |
| width: 100%; | | width: 100%; |
| height: 100%; | | height: 100%; |
| display: flex; | | background-color: rgba(0, 0, 0, 0.9); |
| | z-index: 10000; |
| justify-content: center; | | justify-content: center; |
| align-items: center; | | align-items: flex-start; |
| background: rgba(0, 0, 0, 0.95); | | overflow-y: auto; |
| z-index: 10015; | | padding: 60px 20px 20px 20px; |
| animation: cardZoomIn 0.3s ease; | | box-sizing: border-box; |
| } | | } |
|
| |
|
| .spark-enlarged-card { | | .spark-modal-overlay.active { |
| position: relative;
| |
| display: flex; | | display: flex; |
| flex-direction: column;
| |
| align-items: center;
| |
| } | | } |
|
| |
|
| .spark-enlarged-card .card-deck-trans { | | .spark-modal-close { |
| width: 270px !important;
| |
| height: 384px !important;
| |
| transform: none !important;
| |
| filter: none !important;
| |
| cursor: default;
| |
| }
| |
| | |
| .spark-enlarged-card .card-deck-trans:hover {
| |
| transform: none !important;
| |
| filter: none !important;
| |
| }
| |
| | |
| .spark-enlarged-card .card-deck {
| |
| transform: scale(0.6) !important;
| |
| }
| |
| | |
| .spark-enlarged-close {
| |
| position: fixed; | | position: fixed; |
| top: 20px; | | top: 20px; |
| right: 30px; | | right: 30px; |
| font-size: 48px; | | font-size: 40px; |
| font-weight: bold;
| | color: #fff; |
| color: #ffffff; | |
| cursor: pointer; | | cursor: pointer; |
| z-index: 10016; | | z-index: 10002; |
| line-height: 1; | | line-height: 1; |
| transition: color 0.2s ease, transform 0.2s ease;
| |
| text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
| |
| } | | } |
|
| |
|
| .spark-enlarged-close:hover { | | .spark-modal-close:hover { |
| color: #ff6b6b; | | color: #ff6b6b; |
| transform: scale(1.1);
| |
| } | | } |
|
| |
|
| /* 防止模态框打开时页面滚动 */
| | .spark-modal-content { |
| body.card-modal-open {
| | display: flex; |
| overflow: hidden; | | flex-wrap: wrap; |
| | justify-content: center; |
| | gap: 20px; |
| | max-width: 1200px; |
| } | | } |
|
| |
|
| /* 滚动条样式 */
| | .spark-modal-content .card-deck-trans { |
| .spark-modal-content::-webkit-scrollbar { | | cursor: default; |
| width: 8px; | | pointer-events: none; |
| } | | } |
|
| |
|
| .spark-modal-content::-webkit-scrollbar-track { | | .spark-modal-content .card-deck-trans:hover { |
| background: rgba(255, 255, 255, 0.1); | | transform: none; |
| border-radius: 4px;
| |
| } | | } |
|
| |
|
| .spark-modal-content::-webkit-scrollbar-thumb { | | .spark-modal-title { |
| background: rgba(255, 255, 255, 0.3); | | width: 100%; |
| border-radius: 4px; | | text-align: center; |
| | color: #ffd700; |
| | font-size: 24px; |
| | margin-bottom: 20px; |
| } | | } |
|
| |
|
| .spark-modal-content::-webkit-scrollbar-thumb:hover { | | .spark-loading { |
| background: rgba(255, 255, 255, 0.5); | | color: #fff; |
| | font-size: 18px; |
| } | | } |
| </style> | | </style> |
| | |
| | <div id="card-modal-overlay" class="card-modal-overlay"> |
| | <span class="card-modal-close" id="card-modal-close">×</span> |
| | <div class="card-modal-content" id="card-modal-content"></div> |
| | </div> |
| | |
| | <div id="spark-modal-overlay" class="spark-modal-overlay"> |
| | <span class="spark-modal-close" id="spark-modal-close">×</span> |
| | <div class="spark-modal-content" id="spark-modal-content"></div> |
| | </div> |
|
| |
|
| <script> | | <script> |
| 第356行: |
第176行: |
| 'use strict'; | | 'use strict'; |
| | | |
| // ==================== 缓存管理器(优化版) ==================== | | function init() { |
| var SparkCache = (function() {
| | var cardModal = document.getElementById('card-modal-overlay'); |
| var CACHE_KEY = 'spark_card_cache_v2'; | | var cardModalContent = document.getElementById('card-modal-content'); |
| var CACHE_EXPIRE = 24 * 60 * 60 * 1000; // 24小时 | | var cardModalClose = document.getElementById('card-modal-close'); |
| | var sparkModal = document.getElementById('spark-modal-overlay'); |
| | var sparkModalContent = document.getElementById('spark-modal-content'); |
| | var sparkModalClose = document.getElementById('spark-modal-close'); |
| | | |
| // 内存缓存层
| | function bindCardEvents() { |
| var memoryCache = null;
| | var cards = document.querySelectorAll('.card-deck-trans'); |
| var isDirty = false;
| |
| var saveTimer = null;
| |
|
| |
| // 从 localStorage 加载到内存
| |
| function loadFromStorage() { | |
| if (memoryCache !== null) return memoryCache; | |
| | | |
| try { | | cards.forEach(function(card) { |
| var data = localStorage.getItem(CACHE_KEY);
| | if (card.dataset.bound || card.closest('.spark-modal-content') || card.closest('.card-modal-content')) { |
| memoryCache = data ? JSON.parse(data) : {};
| | return; |
| } catch (e) {
| |
| memoryCache = {}; | |
| }
| |
| return memoryCache;
| |
| }
| |
|
| |
| // 延迟批量写入 localStorage
| |
| function scheduleSave() {
| |
| if (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); // 1秒后批量写入
| |
| }
| |
|
| |
| // 清理过期缓存
| |
| function cleanup(force) {
| |
| var cache = loadFromStorage();
| |
| 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); | | card.dataset.bound = 'true'; |
| 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 (hasChanges) {
| |
| isDirty = true;
| |
| scheduleSave();
| |
| }
| |
| }
| |
|
| |
| return {
| |
| get: function(cardId) {
| |
| var cache = loadFromStorage();
| |
| var item = cache[cardId];
| |
| | | |
| if (!item) return null; | | card.addEventListener('click', function(e) { |
| if (Date.now() - item.timestamp > CACHE_EXPIRE) {
| | e.preventDefault(); |
| delete cache[cardId];
| | e.stopPropagation(); |
| isDirty = true;
| | |
| scheduleSave();
| | var cardDeck = this.querySelector('.card-deck'); |
| return null;
| | if (!cardDeck) return; |
| }
| |
|
| |
| return item.html;
| |
| },
| |
|
| |
| set: function(cardId, html) {
| |
| var cache = loadFromStorage();
| |
| cache[cardId] = {
| |
| html: html, | |
| timestamp: Date.now()
| |
| };
| |
| isDirty = true;
| |
| scheduleSave();
| |
| },
| |
|
| |
| has: function(cardId) {
| |
| var cache = loadFromStorage();
| |
| var item = cache[cardId];
| |
| return item && (Date.now() - item.timestamp <= CACHE_EXPIRE);
| |
| },
| |
|
| |
| // 批量获取
| |
| getMultiple: function(cardIds) {
| |
| var cache = loadFromStorage();
| |
| var now = Date.now();
| |
| var result = {};
| |
| var hasExpired = false;
| |
|
| |
| cardIds.forEach(function(cardId) {
| |
| var item = cache[cardId];
| |
| if (item) {
| |
| if (now - item.timestamp <= CACHE_EXPIRE) {
| |
| result[cardId] = item.html;
| |
| } else {
| |
| delete cache[cardId];
| |
| hasExpired = true;
| |
| }
| |
| }
| |
| });
| |
|
| |
| if (hasExpired) {
| |
| isDirty = true; | |
| scheduleSave(); | |
| }
| |
|
| |
| return result;
| |
| },
| |
|
| |
| // 批量设置
| |
| setMultiple: function(dataMap) {
| |
| var cache = loadFromStorage();
| |
| var now = Date.now();
| |
|
| |
| for (var cardId in dataMap) {
| |
| if (dataMap.hasOwnProperty(cardId)) {
| |
| cache[cardId] = {
| |
| html: dataMap[cardId],
| |
| timestamp: now
| |
| };
| |
| }
| |
| }
| |
|
| |
| isDirty = true;
| |
| scheduleSave();
| |
| },
| |
|
| |
| cleanup: cleanup,
| |
|
| |
| // 页面卸载时立即保存
| |
| flush: function() {
| |
| 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 = {}; // cardId -> [resolve, reject][]
| |
| var requestQueue = [];
| |
| var isProcessing = false;
| |
| var batchSize = 5; // 每批并行请求数
| |
| var batchDelay = 50; // 批次间延迟
| |
|
| |
| function processBatch() {
| |
| if (isProcessing || requestQueue.length === 0) return;
| |
|
| |
| isProcessing = true;
| |
|
| |
| // 取出一批
| |
| var batch = requestQueue.splice(0, batchSize);
| |
| var promises = batch.map(function(cardId) {
| |
| return fetchSparkList(cardId);
| |
| });
| |
|
| |
| Promise.all(promises).then(function() {
| |
| isProcessing = false;
| |
| if (requestQueue.length > 0) {
| |
| setTimeout(processBatch, batchDelay);
| |
| }
| |
| });
| |
| }
| |
|
| |
| function fetchSparkList(cardId) {
| |
| return new Promise(function(resolve) {
| |
| var api = new mw.Api();
| |
| 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 clonedCard = document.createElement('div'); |
| var callbacks = pendingRequests[cardId] || []; | | clonedCard.className = 'card-modal-card'; |
| delete pendingRequests[cardId]; | | clonedCard.innerHTML = '<div class="card-deck">' + cardDeck.innerHTML + '</div>'; |
| callbacks.forEach(function(cb) { | |
| cb.resolve(html);
| |
| });
| |
| | | |
| resolve(); | | cardModalContent.innerHTML = ''; |
| }).fail(function(error) {
| | cardModalContent.appendChild(clonedCard); |
| var callbacks = pendingRequests[cardId] || [];
| |
| delete pendingRequests[cardId]; | |
| callbacks.forEach(function(cb) {
| |
| cb.reject(error);
| |
| });
| |
| | | |
| resolve(); | | var sparkEnable = this.dataset.sparkEnable; |
| });
| | var sort = this.dataset.sort; |
| });
| | var cardId = this.dataset.cardId; |
| }
| |
|
| |
| return {
| |
| // 请求单个卡牌(会自动合并到批量请求)
| |
| request: function(cardId) {
| |
| return new Promise(function(resolve, reject) {
| |
| // 先检查缓存
| |
| var cached = SparkCache.get(cardId); | |
| if (cached !== null) { | |
| resolve(cached);
| |
| return;
| |
| }
| |
| | | |
| // 检查是否已在请求中
| | if (sparkEnable === '1' && sort !== '中立卡牌' && sort !== '怪物卡牌' && cardId) { |
| if (pendingRequests[cardId]) { | | var sparkButton = document.createElement('button'); |
| pendingRequests[cardId].push({ resolve: resolve, reject: reject }); | | sparkButton.className = 'card-spark-button'; |
| return; | | sparkButton.textContent = '闪光'; |
| | sparkButton.addEventListener('click', function(e) { |
| | e.stopPropagation(); |
| | openSparkModal(cardId); |
| | }); |
| | cardModalContent.appendChild(sparkButton); |
| } | | } |
| | | |
| // 新请求 | | cardModal.classList.add('active'); |
| pendingRequests[cardId] = [{ resolve: resolve, reject: reject }];
| | document.body.style.overflow = 'hidden'; |
| requestQueue.push(cardId);
| |
| | |
| // 启动处理
| |
| if (!isProcessing) {
| |
| setTimeout(processBatch, 10);
| |
| }
| |
| });
| |
| },
| |
|
| |
| // 预加载多个卡牌(低优先级)
| |
| preload: function(cardIds) {
| |
| // 过滤掉已缓存和已在队列中的
| |
| 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) {
| |
| 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(); // 使用 WeakSet 跟踪已处理的卡牌
| |
|
| |
| // 创建模态框
| |
| function createModals() {
| |
| 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);
| |
| 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);
| |
| }
| |
|
| |
| // 关闭闪光模态框
| |
| function closeSparkModal() {
| |
| closeEnlargedCard();
| |
|
| |
| 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;
| |
| }
| |
| }
| |
|
| |
| // 打开主模态框
| |
| function openModal(cardElement, cardId, sparkEnable) {
| |
| createModals();
| |
|
| |
| var cardClone = cardElement.cloneNode(true);
| |
| 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 (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) { | | function openSparkModal(cardId) { |
| createModals(); | | sparkModalContent.innerHTML = '<div class="spark-modal-title">闪光卡牌</div><div class="spark-loading">加载中...</div>'; |
| | sparkModal.classList.add('active'); |
| | | |
| var cachedHtml = SparkCache.get(cardId); | | var apiUrl = mw.config.get('wgScriptPath') + '/api.php'; |
| | var params = new URLSearchParams({ |
| | action: 'parse', |
| | text: '{{Card/display/spark|' + cardId + '}}', |
| | prop: 'text', |
| | format: 'json', |
| | contentmodel: 'wikitext' |
| | }); |
| | | |
| if (cachedHtml) { | | fetch(apiUrl + '?' + params.toString()) |
| showSparkContent(cachedHtml, true);
| | .then(function(response) { |
| } else {
| | return response.json(); |
| sparkModalContent.innerHTML = '<div class="spark-modal-title">闪光卡牌列表</div><div class="spark-loading">加载中</div>';
| | }) |
|
| | .then(function(data) { |
| sparkModalOverlay.classList.add('active');
| | if (data.parse && data.parse.text) { |
| requestAnimationFrame(function() { | | var html = data.parse.text['*']; |
| sparkModalOverlay.classList.add('fade-in'); | | if (html.trim() === '' || html.indexOf('card-deck-trans') === -1) { |
| }); | | sparkModalContent.innerHTML = '<div class="spark-modal-title">闪光卡牌</div><div style="color: #fff;">暂无闪光卡牌</div>'; |
| | | } else { |
| BatchRequestManager.request(cardId).then(function(html) {
| | sparkModalContent.innerHTML = '<div class="spark-modal-title">闪光卡牌</div>' + html; |
| showSparkContent(html, false); | |
| }).catch(function() {
| |
| sparkModalContent.innerHTML = '<div class="spark-modal-title">闪光卡牌列表</div><div class="spark-empty">加载失败,请重试</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 = [];
| |
|
| |
| 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]);
| |
| }
| |
| }
| |
|
| |
| 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);
| |
|
| |
| // 收集需要预加载的卡牌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);
| |
| } | | } |
| | } else { |
| | sparkModalContent.innerHTML = '<div class="spark-modal-title">闪光卡牌</div><div style="color: #fff;">暂无闪光卡牌</div>'; |
| } | | } |
| | }) |
| | .catch(function(error) { |
| | sparkModalContent.innerHTML = '<div class="spark-modal-title">闪光卡牌</div><div style="color: #ff6b6b;">加载失败,请重试</div>'; |
| | console.error('Error loading spark cards:', error); |
| }); | | }); |
| });
| |
|
| |
| // 延迟预加载
| |
| if (sparkCardIds.length > 0) {
| |
| setTimeout(function() {
| |
| BatchRequestManager.preload(sparkCardIds);
| |
| }, 2000);
| |
| }
| |
| } | | } |
| | | |
| // 初始化页面上已有的卡牌 | | cardModalClose.addEventListener('click', function() { |
| function initExistingCards() {
| | cardModal.classList.remove('active'); |
| var cards = document.querySelectorAll('.card-deck-trans'); | | sparkModal.classList.remove('active'); |
| var nodeArray = []; | | document.body.style.overflow = ''; |
| cards.forEach(function(card) {
| | }); |
| nodeArray.push(card);
| |
| }); | |
| processNewCards(nodeArray);
| |
| }
| |
| | | |
| return { | | sparkModalClose.addEventListener('click', function() { |
| createModals: createModals,
| | sparkModal.classList.remove('active'); |
| closeModal: closeModal,
| | }); |
| closeSparkModal: closeSparkModal,
| |
| closeEnlargedCard: closeEnlargedCard,
| |
| openModal: openModal,
| |
| openSparkModal: openSparkModal,
| |
| showEnlargedCard: showEnlargedCard,
| |
| processNewCards: processNewCards,
| |
| initExistingCards: initExistingCards,
| |
| getModalOverlay: function() { return modalOverlay; },
| |
| getSparkModalOverlay: function() { return sparkModalOverlay; } | |
| }; | |
| })();
| |
|
| |
| // ==================== 事件委托管理器 ====================
| |
| var EventManager = (function() {
| |
| var hoverTimer = null;
| |
| | | |
| // 获取卡牌信息 | | cardModal.addEventListener('click', function(e) { |
| function getCardInfo(cardElement) {
| | if (e.target === cardModal) { |
| var wrapper = cardElement.closest('.card-wrapper'); | | cardModal.classList.remove('active'); |
| return {
| | sparkModal.classList.remove('active'); |
| cardId: wrapper ? wrapper.dataset.cardId : '', | | document.body.style.overflow = ''; |
| sparkEnable: cardElement.dataset.sparkEnable || '' | | } |
| }; | | }); |
| } | |
| | | |
| // 检查是否是有效的卡牌点击 | | sparkModal.addEventListener('click', function(e) { |
| function isValidCardClick(target) {
| | if (e.target === sparkModal) { |
| var card = target.closest('.card-deck-trans');
| | sparkModal.classList.remove('active'); |
| 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 (inMainModal && !inSparkList) return null;
| |
| if (inEnlarged) return null;
| |
|
| |
| return card;
| |
| }
| |
| | | |
| // 初始化事件监听 | | document.addEventListener('keydown', function(e) { |
| function init() {
| | if (e.key === 'Escape') { |
| // 使用事件委托处理所有点击事件
| | if (sparkModal.classList.contains('active')) { |
| document.addEventListener('click', function(e) {
| | sparkModal.classList.remove('active'); |
| var target = e.target;
| | } else if (cardModal.classList.contains('active')) { |
|
| | cardModal.classList.remove('active'); |
| // 关闭按钮
| | document.body.style.overflow = ''; |
| if (target.classList.contains('card-modal-close')) { | |
| e.stopPropagation(); | |
| DOMManager.closeModal(); | |
| 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);
| |
| 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);
| |
| }
| |
| }, 150);
| |
| }
| |
| }, true);
| |
|
| |
| document.addEventListener('mouseleave', function(e) {
| |
| var card = e.target.closest && e.target.closest('.card-deck-trans');
| |
| if (card && hoverTimer) {
| |
| clearTimeout(hoverTimer);
| |
| hoverTimer = null;
| |
| }
| |
| }, true);
| |
|
| |
| // ESC键关闭
| |
| document.addEventListener('keydown', function(e) {
| |
| 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 { | | bindCardEvents(); |
| init: init
| |
| };
| |
| })();
| |
|
| |
| // ==================== 初始化 ====================
| |
| function init() {
| |
| // 清理过期缓存
| |
| SparkCache.cleanup();
| |
| | | |
| // 创建模态框
| | var observer = new MutationObserver(function() { |
| DOMManager.createModals();
| | bindCardEvents(); |
|
| |
| // 初始化事件监听(事件委托)
| |
| EventManager.init();
| |
|
| |
| // 处理已有卡牌
| |
| DOMManager.initExistingCards();
| |
|
| |
| // 监听DOM变化 - 只处理新添加的节点
| |
| var observer = new MutationObserver(function(mutations) { | |
| var newNodes = []; | |
|
| |
| mutations.forEach(function(mutation) {
| |
| if (mutation.addedNodes.length > 0) {
| |
| for (var i = 0; i < mutation.addedNodes.length; i++) {
| |
| newNodes.push(mutation.addedNodes[i]);
| |
| }
| |
| }
| |
| });
| |
|
| |
| if (newNodes.length > 0) {
| |
| // 使用 requestIdleCallback 或 setTimeout 延迟处理
| |
| if (window.requestIdleCallback) {
| |
| requestIdleCallback(function() {
| |
| DOMManager.processNewCards(newNodes);
| |
| }, { timeout: 500 });
| |
| } else {
| |
| setTimeout(function() {
| |
| DOMManager.processNewCards(newNodes);
| |
| }, 100);
| |
| }
| |
| }
| |
| }); | | }); |
| | | |
| 第1,098行: |
第311行: |
| } | | } |
| | | |
| // 启动
| |
| if (document.readyState === 'loading') { | | if (document.readyState === 'loading') { |
| document.addEventListener('DOMContentLoaded', init); | | document.addEventListener('DOMContentLoaded', init); |