|
|
| (未显示同一用户的3个中间版本) |
| 第1行: |
第1行: |
| <noinclude> | | <noinclude> |
| 此Widget为卡牌显示添加交互效果:
| | 此Widget用于为卡牌添加交互功能。 |
| * 鼠标悬停时的动画效果
| | 使用方法:在页面底部添加 {{#widget:Card}} |
| * 点击后显示放大的模态框
| | [[分类:Widget]] |
| * 自动检测spark_enable属性显示闪光按钮
| |
| * 支持闪光卡牌列表显示
| |
| * 预加载、批量加载和本地缓存优化
| |
| * 事件委托、DOM优化、批量API请求
| |
| | |
| 使用方法:在页面中添加 {{#widget:Card}}
| |
| </noinclude><includeonly> | | </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; |
| 第40行: |
第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; | | 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;
| |
| }
| |
|
| |
| .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-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; |
| 第279行: |
第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> |
| 第359行: |
第176行: |
| 'use strict'; | | 'use strict'; |
| | | |
| // ==================== 等待 MediaWiki 加载 ====================
| | function init() { |
| function waitForMw(callback) { | | var cardModal = document.getElementById('card-modal-overlay'); |
| // 检查 mw 和 mw.loader 是否存在 | | var cardModalContent = document.getElementById('card-modal-content'); |
| if (typeof mw !== 'undefined' && mw.loader && typeof mw.loader.using === 'function') {
| | var cardModalClose = document.getElementById('card-modal-close'); |
| mw.loader.using(['mediawiki.api']).then(callback);
| | var sparkModal = document.getElementById('spark-modal-overlay'); |
| } else if (typeof mw !== 'undefined' && mw.Api) {
| | var sparkModalContent = document.getElementById('spark-modal-content'); |
| // mw.Api 已经可用
| | var sparkModalClose = document.getElementById('spark-modal-close'); |
| 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 apiClient = null;
| |
| var apiAvailable = false; | |
|
| |
| function getApi() {
| |
| if (!apiAvailable) {
| |
| // 检查 API 是否可用
| |
| if (typeof mw !== 'undefined' && typeof mw.Api === 'function') {
| |
| apiAvailable = true;
| |
| } else {
| |
| return null;
| |
| }
| |
| }
| |
|
| |
| if (!apiClient && apiAvailable) {
| |
| try {
| |
| apiClient = new mw.Api();
| |
| } catch (e) {
| |
| console.warn('CardWidget: 无法创建 API 客户端', e);
| |
| apiAvailable = false;
| |
| return null;
| |
| }
| |
| }
| |
| return apiClient;
| |
| }
| |
|
| |
| // ==================== 缓存管理器 ====================
| |
| 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;
| |
| }
| |
|
| |
| 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 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 (hasChanges) {
| |
| isDirty = true;
| |
| scheduleSave();
| |
| }
| |
| }
| |
|
| |
| return {
| |
| get: function(cardId) {
| |
| var cache = loadFromStorage();
| |
| var item = cache[cardId];
| |
|
| |
| 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;
| |
| scheduleSave();
| |
| },
| |
|
| |
| has: function(cardId) {
| |
| var cache = loadFromStorage();
| |
| var item = cache[cardId];
| |
| return item && (Date.now() - item.timestamp <= CACHE_EXPIRE);
| |
| },
| |
|
| |
| cleanup: cleanup,
| |
|
| |
| flush: function() {
| |
| 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请求管理器 ==================== | | function bindCardEvents() { |
| var BatchRequestManager = (function() {
| | var cards = document.querySelectorAll('.card-deck-trans'); |
| var pendingRequests = {}; | |
| var requestQueue = [];
| |
| var isProcessing = false;
| |
| var batchSize = 5;
| |
| var batchDelay = 50;
| |
| | | |
| function processBatch() { | | cards.forEach(function(card) { |
| if (isProcessing || requestQueue.length === 0) return; | | if (card.dataset.bound || card.closest('.spark-modal-content') || card.closest('.card-modal-content')) { |
|
| |
| 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; | | return; |
| } | | } |
| | | |
| isProcessing = true; | | card.dataset.bound = 'true'; |
| | | |
| var batch = requestQueue.splice(0, batchSize); | | card.addEventListener('click', function(e) { |
| var promises = batch.map(function(cardId) {
| | e.preventDefault(); |
| return fetchSparkList(cardId, api);
| | e.stopPropagation(); |
| });
| |
|
| |
| 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) { | | var cardDeck = this.querySelector('.card-deck'); |
| return !SparkCache.has(cardId) &&
| | if (!cardDeck) return; |
| !pendingRequests[cardId] &&
| |
| requestQueue.indexOf(cardId) === -1;
| |
| }); | |
| | | |
| if (toLoad.length === 0) return; | | var clonedCard = document.createElement('div'); |
| | clonedCard.className = 'card-modal-card'; |
| | clonedCard.innerHTML = '<div class="card-deck">' + cardDeck.innerHTML + '</div>'; |
| | | |
| toLoad.forEach(function(cardId) { | | cardModalContent.innerHTML = ''; |
| pendingRequests[cardId] = [];
| | cardModalContent.appendChild(clonedCard); |
| requestQueue.push(cardId);
| |
| });
| |
| | | |
| if (!isProcessing) {
| | var sparkEnable = this.dataset.sparkEnable; |
| setTimeout(processBatch, 100);
| | var sort = this.dataset.sort; |
| }
| | var cardId = this.dataset.cardId; |
| },
| |
|
| |
| 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) {
| |
| 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);
| |
|
| |
| // 只有在 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)) { | | if (sparkEnable === '1' && sort !== '中立卡牌' && sort !== '怪物卡牌' && cardId) { |
| sparkButton.classList.add('cached'); | | var sparkButton = document.createElement('button'); |
| } else {
| | sparkButton.className = 'card-spark-button'; |
| BatchRequestManager.prioritize(cardId); | | sparkButton.textContent = '闪光'; |
| | sparkButton.addEventListener('click', function(e) { |
| | e.stopPropagation(); |
| | openSparkModal(cardId); |
| | }); |
| | cardModalContent.appendChild(sparkButton); |
| } | | } |
| | | |
| modalContent.appendChild(sparkButton); | | cardModal.classList.add('active'); |
| }
| | document.body.style.overflow = 'hidden'; |
|
| |
| 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);
| |
|
| |
| 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>';
| |
| });
| |
| }
| |
| }
| |
|
| |
| 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);
| |
|
| |
| 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 {
| |
| 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 openSparkModal(cardId) { |
| var EventManager = (function() {
| | sparkModalContent.innerHTML = '<div class="spark-modal-title">闪光卡牌</div><div class="spark-loading">加载中...</div>'; |
| var hoverTimer = null; | | sparkModal.classList.add('active'); |
| | | |
| function getCardInfo(cardElement) { | | var apiUrl = mw.config.get('wgScriptPath') + '/api.php'; |
| var wrapper = cardElement.closest('.card-wrapper');
| | var params = new URLSearchParams({ |
| return {
| | action: 'parse', |
| cardId: wrapper ? wrapper.dataset.cardId : '',
| | text: '{{Card/display/spark|' + cardId + '}}', |
| sparkEnable: cardElement.dataset.sparkEnable || ''
| | prop: 'text', |
| };
| | format: 'json', |
| }
| | contentmodel: 'wikitext' |
|
| | }); |
| 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 (inMainModal && !inSparkList) return null;
| |
| if (inEnlarged) return null;
| |
|
| |
| return card;
| |
| }
| |
| | | |
| function init() { | | fetch(apiUrl + '?' + params.toString()) |
| document.addEventListener('click', function(e) {
| | .then(function(response) { |
| var target = e.target;
| | return response.json(); |
|
| | }) |
| if (target.classList.contains('card-modal-close')) {
| | .then(function(data) { |
| e.stopPropagation();
| | if (data.parse && data.parse.text) { |
| DOMManager.closeModal();
| | var html = data.parse.text['*']; |
| return;
| | if (html.trim() === '' || html.indexOf('card-deck-trans') === -1) { |
| }
| | sparkModalContent.innerHTML = '<div class="spark-modal-title">闪光卡牌</div><div style="color: #fff;">暂无闪光卡牌</div>'; |
|
| |
| 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 { | | } else { |
| DOMManager.openModal(card, info.cardId, info.sparkEnable); | | sparkModalContent.innerHTML = '<div class="spark-modal-title">闪光卡牌</div>' + html; |
| }
| |
| }
| |
| }, 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).catch(function() {
| |
| // 忽略预加载错误
| |
| });
| |
| }
| |
| }, 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);
| |
|
| |
| 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();
| |
| } | | } |
| | } 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); |
| }); | | }); |
|
| | } |
| window.addEventListener('beforeunload', function() {
| | |
| SparkCache.flush();
| | cardModalClose.addEventListener('click', function() { |
| });
| | cardModal.classList.remove('active'); |
|
| | sparkModal.classList.remove('active'); |
| document.addEventListener('visibilitychange', function() {
| | document.body.style.overflow = ''; |
| if (document.hidden) {
| | }); |
| SparkCache.flush();
| | |
| }
| | sparkModalClose.addEventListener('click', function() { |
| }); | | sparkModal.classList.remove('active'); |
| | }); |
| | |
| | cardModal.addEventListener('click', function(e) { |
| | if (e.target === cardModal) { |
| | cardModal.classList.remove('active'); |
| | sparkModal.classList.remove('active'); |
| | document.body.style.overflow = ''; |
| } | | } |
|
| | }); |
| return {
| |
| init: init
| |
| };
| |
| })(); | |
| | | |
| // ==================== 初始化 ==================== | | sparkModal.addEventListener('click', function(e) { |
| SparkCache.cleanup();
| | if (e.target === sparkModal) { |
| DOMManager.createModals();
| | sparkModal.classList.remove('active'); |
| EventManager.init();
| | } |
| DOMManager.initExistingCards(); | | }); |
| | | |
| var observer = new MutationObserver(function(mutations) { | | document.addEventListener('keydown', function(e) { |
| var newNodes = []; | | if (e.key === 'Escape') { |
|
| | if (sparkModal.classList.contains('active')) { |
| mutations.forEach(function(mutation) {
| | sparkModal.classList.remove('active'); |
| if (mutation.addedNodes.length > 0) { | | } else if (cardModal.classList.contains('active')) { |
| for (var i = 0; i < mutation.addedNodes.length; i++) { | | cardModal.classList.remove('active'); |
| newNodes.push(mutation.addedNodes[i]);
| | document.body.style.overflow = ''; |
| }
| |
| } | |
| });
| |
|
| |
| if (newNodes.length > 0) {
| |
| if (window.requestIdleCallback) {
| |
| requestIdleCallback(function() {
| |
| DOMManager.processNewCards(newNodes);
| |
| }, { timeout: 500 });
| |
| } else {
| |
| setTimeout(function() { | |
| DOMManager.processNewCards(newNodes);
| |
| }, 100); | |
| } | | } |
| } | | } |
| | }); |
| | |
| | bindCardEvents(); |
| | |
| | var observer = new MutationObserver(function() { |
| | bindCardEvents(); |
| }); | | }); |
| | | |
| 第1,091行: |
第311行: |
| } | | } |
| | | |
| // ==================== 启动 ==================== | | if (document.readyState === 'loading') { |
| function start() {
| | document.addEventListener('DOMContentLoaded', init); |
| if (document.readyState === 'loading') {
| | } else { |
| document.addEventListener('DOMContentLoaded', function() {
| | init(); |
| waitForMw(initCardWidget);
| |
| });
| |
| } else {
| |
| waitForMw(initCardWidget);
| |
| }
| |
| } | | } |
|
| |
| start();
| |
| })(); | | })(); |
| </script> | | </script> |
| </includeonly> | | </includeonly> |