微件

TierListMaker:修订间差异

来自卡厄思梦境WIKI

律Rhyme留言 | 贡献
无编辑摘要
律Rhyme留言 | 贡献
无编辑摘要
 
(未显示同一用户的6个中间版本)
第37行: 第37行:
.tier-row .tier-cell {
.tier-row .tier-cell {
   border: 1px solid #ccc;
   border: 1px solid #ccc;
   padding: 6px;
   padding: 0px;
}
}
.tier-tools {
.tier-tools {
第45行: 第45行:
   display: flex;
   display: flex;
   gap: 6px;
   gap: 6px;
}
/* 导出时隐藏工具按钮 */
.exporting .tier-tools {
  display: none !important;
}
}
.color-toggle {
.color-toggle {
第68行: 第72行:
   gap: 6px;
   gap: 6px;
   align-items: flex-start;
   align-items: flex-start;
  padding: 4px;
}
}
.tier-dropzone.pool {
.tier-dropzone.pool {
第74行: 第79行:
   border-radius: 6px;
   border-radius: 6px;
   background: #fafafa;
   background: #fafafa;
}
/* 导出时隐藏角色池 */
.exporting .pool-wrapper {
  display: none !important;
}
}
.pool-wrapper { margin-top: 12px; }
.pool-wrapper { margin-top: 12px; }
第150行: 第159行:
   outline: none;
   outline: none;
   cursor: text;
   cursor: text;
}
.exporting .tier-dropzone.pool {
  border: 0;
  background: transparent;
}
}
</style>
</style>
第165行: 第169行:
   }
   }
   function contrastColor(hex) {
   function contrastColor(hex) {
     hex = hex.replace('#','');
     hex = (hex || '').replace('#','');
     if (hex.length === 3) hex = hex.split('').map(c => c + c).join('');
     if (hex.length === 3) hex = hex.split('').map(c => c + c).join('');
    if (!/^[0-9a-fA-F]{6}$/.test(hex)) return '#fff';
     var r = parseInt(hex.substr(0,2), 16);
     var r = parseInt(hex.substr(0,2), 16);
     var g = parseInt(hex.substr(2,2), 16);
     var g = parseInt(hex.substr(2,2), 16);
第184行: 第189行:
       }
       }
     }, { offset: Number.NEGATIVE_INFINITY, element: null }).element;
     }, { offset: Number.NEGATIVE_INFINITY, element: null }).element;
  }
  function ensureCrossOrigin(img) {
    try {
      if (!img) return;
      if (!img.crossOrigin) img.crossOrigin = 'anonymous';
      if (!img.referrerPolicy) img.referrerPolicy = 'no-referrer';
    } catch(e) {}
   }
   }
   function makeDraggable(avatar) {
   function makeDraggable(avatar) {
    if (!avatar || avatar._draggableInit) return;
    avatar._draggableInit = true;
     avatar.setAttribute('draggable', 'true');
     avatar.setAttribute('draggable', 'true');
     avatar.classList.add('draggable-avatar');
     avatar.classList.add('draggable-avatar');
    avatar.querySelectorAll('img').forEach(ensureCrossOrigin);
     avatar.addEventListener('dragstart', function(e) {
     avatar.addEventListener('dragstart', function(e) {
       avatar.classList.add('dragging');
       avatar.classList.add('dragging');
      avatar._prevParent = avatar.parentNode;
       e.dataTransfer.setData('text/plain', avatar.dataset.id || avatar.querySelector('.avatar-name')?.textContent || '');
       e.dataTransfer.setData('text/plain', avatar.dataset.id || avatar.querySelector('.avatar-name')?.textContent || '');
      // allow move effect
       e.dataTransfer.effectAllowed = 'move';
       e.dataTransfer.effectAllowed = 'move';
     });
     });
     avatar.addEventListener('dragend', function() {
     avatar.addEventListener('dragend', function() {
       avatar.classList.remove('dragging');
       avatar.classList.remove('dragging');
      cleanupEmptyWrapper(avatar._prevParent);
      avatar._prevParent = null;
     });
     });
   }
   }
   function initAvatars(root) {
   function initAvatars(root) {
    if (!root) return;
     const avatars = root.querySelectorAll('.avatar-frame');
     const avatars = root.querySelectorAll('.avatar-frame');
     avatars.forEach(function(av) {
     avatars.forEach(function(av) {
      // Ensure data attributes exist
       if (!av.dataset.id) {
       if (!av.dataset.id) {
         const nameEl = av.querySelector('.avatar-name');
         const nameEl = av.querySelector('.avatar-name');
第207行: 第225行:
       }
       }
       makeDraggable(av);
       makeDraggable(av);
    });
  }
  function cleanupEmptyWrapper(node) {
    if (!node || node.nodeType !== 1) return;
    if (node.classList && node.classList.contains('tier-dropzone')) return;
    const hasElementChild = node.querySelector('.avatar-frame');
    const onlyWhitespace = !node.textContent || node.textContent.trim().length === 0;
    if (!hasElementChild && node.childElementCount === 0 && onlyWhitespace) {
      node.parentNode && node.parentNode.removeChild(node);
    }
  }
  function cleanupEmptyPlaceholdersIn(container) {
    if (!container) return;
    Array.from(container.children).forEach(function(child) {
      if (child.nodeType !== 1) return;
      if (child.classList.contains('avatar-frame')) return;
      const hasAvatarInside = !!child.querySelector('.avatar-frame');
      const onlyWhitespace = !child.textContent || child.textContent.trim().length === 0;
      if (!hasAvatarInside && child.childElementCount === 0 && onlyWhitespace) {
        child.remove();
      }
     });
     });
   }
   }
   function initDropzone(zone) {
   function initDropzone(zone) {
    if (!zone || zone._dropzoneInit) return;
    zone._dropzoneInit = true;
     zone.addEventListener('dragover', function(e) {
     zone.addEventListener('dragover', function(e) {
       e.preventDefault();
       e.preventDefault();
      e.dataTransfer.dropEffect = 'move';
       const afterElement = getDragAfterElement(zone, e.clientY);
       const afterElement = getDragAfterElement(zone, e.clientY);
       const dragging = document.querySelector('.avatar-frame.dragging');
       const dragging = document.querySelector('.avatar-frame.dragging');
       if (!dragging) return;
       if (!dragging) return;
       if (afterElement == null) {
       if (afterElement == null) {
         zone.appendChild(dragging);
         if (dragging.parentNode !== zone) zone.appendChild(dragging);
       } else {
       } else {
         zone.insertBefore(dragging, afterElement);
         if (afterElement !== dragging) zone.insertBefore(dragging, afterElement);
       }
       }
     });
     });
     zone.addEventListener('drop', function(e) {
     zone.addEventListener('drop', function(e) {
       e.preventDefault();
       e.preventDefault();
      e.stopPropagation();
       const dragging = document.querySelector('.avatar-frame.dragging');
       const dragging = document.querySelector('.avatar-frame.dragging');
       if (dragging && dragging.parentNode !== zone) {
       if (dragging) {
         zone.appendChild(dragging);
        if (dragging.parentNode !== zone) zone.appendChild(dragging);
        cleanupEmptyWrapper(dragging._prevParent);
         dragging._prevParent = null;
      }
      if (zone.classList.contains('pool')) {
        cleanupEmptyPlaceholdersIn(zone);
       }
       }
     });
     });
第244行: 第294行:
       const color = document.createElement('div');
       const color = document.createElement('div');
       color.className = 'color-toggle';
       color.className = 'color-toggle';
      color.title = '更改颜色';
       const del = document.createElement('div');
       const del = document.createElement('div');
       del.className = 'delete-row';
       del.className = 'delete-row';
       del.textContent = '删除';
       del.textContent = '删除';
      del.title = '删除该行';
       t.appendChild(color); t.appendChild(del);
       t.appendChild(color); t.appendChild(del);
       return t;
       return t;
第252行: 第304行:
     th.appendChild(label);
     th.appendChild(label);
     th.appendChild(toolsWrap);
     th.appendChild(toolsWrap);
    // initial colors
     const initialBg = th.getAttribute('data-initial-bg') || '#555';
     const initialBg = th.getAttribute('data-initial-bg') || 'gray';
    const fg = contrastColor(initialBg);
     th.style.background = initialBg;
     th.style.background = initialBg;
     th.style.color = fg;
     th.style.color = contrastColor(initialBg);
    // color palette
     attachColorPalette(toolsWrap.querySelector('.color-toggle'), th);
     attachColorPalette(toolsWrap.querySelector('.color-toggle'), th);
    // delete handler
     const delBtn = toolsWrap.querySelector('.delete-row');
     const delBtn = toolsWrap.querySelector('.delete-row');
     delBtn.addEventListener('click', function() {
     delBtn.addEventListener('click', function() {
第265行: 第313行:
       const dropzone = tr.querySelector('.tier-dropzone');
       const dropzone = tr.querySelector('.tier-dropzone');
       const pool = document.getElementById('character-pool');
       const pool = document.getElementById('character-pool');
      // move items back to pool
       Array.from(dropzone.querySelectorAll('.avatar-frame')).forEach(function(av) {
       Array.from(dropzone.querySelectorAll('.avatar-frame')).forEach(function(av) {
         pool.appendChild(av);
         pool.appendChild(av);
第273行: 第320行:
   }
   }
   function attachColorPalette(toggle, th) {
   function attachColorPalette(toggle, th) {
    if (!toggle) return;
     const palette = document.createElement('div');
     const palette = document.createElement('div');
     palette.className = 'color-palette';
     palette.className = 'color-palette';
第284行: 第332行:
       sw.className = 'color-swatch';
       sw.className = 'color-swatch';
       sw.style.background = c;
       sw.style.background = c;
       sw.addEventListener('click', function(e) {
       sw.addEventListener('click', function() {
         th.style.background = c;
         th.style.background = c;
         th.style.color = contrastColor(c);
         th.style.color = contrastColor(c);
第297行: 第345行:
       palette.style.top  = (window.scrollY + rect.bottom + 6) + 'px';
       palette.style.top  = (window.scrollY + rect.bottom + 6) + 'px';
     }
     }
     toggle.addEventListener('click', function() {
     toggle.addEventListener('click', function(e) {
      e.stopPropagation();
       if (palette.style.display === 'block') {
       if (palette.style.display === 'block') {
         palette.style.display = 'none';
         palette.style.display = 'none';
第317行: 第366行:
   }
   }
   function addNewRow() {
   function addNewRow() {
     const tbody = document.querySelector('#tierlist-table tbody');
     const tbody = document.querySelector('#tierlist-table tbody') || document.querySelector('#tierlist-table');
     const tr = document.createElement('tr');
     const tr = document.createElement('tr');
     tr.className = 'tier-row';
     tr.className = 'tier-row';
第323行: 第372行:
     th.className = 'tier-head';
     th.className = 'tier-head';
     th.setAttribute('data-initial-bg', '#8888ff');
     th.setAttribute('data-initial-bg', '#8888ff');
    th.setAttribute('data-initial-fg', 'white');
     th.textContent = '新行';
     th.textContent = '新行';
     const td = document.createElement('td');
     const td = document.createElement('td');
第347行: 第395行:
     document.body.appendChild(s);
     document.body.appendChild(s);
   }
   }
   function savePNG() {
   function downloadCanvas(canvas) {
     const table = document.getElementById('tierlist-table');
     function done(blobOrUrl) {
    document.getElementById('tierlist-maker').classList.add('exporting');
       try {
    ensureHtml2Canvas(function() {
       // Use higher scale for sharper image
      html2canvas(table, {backgroundColor: null, scale: 2}).then(function(canvas) {
         const link = document.createElement('a');
         const link = document.createElement('a');
         link.href = canvas.toDataURL('image/png');
        const isBlob = blobOrUrl instanceof Blob;
        const url = isBlob ? (URL.createObjectURL(blobOrUrl)) : blobOrUrl;
         link.href = url;
         link.download = 'tierlist.png';
         link.download = 'tierlist.png';
         document.body.appendChild(link);
         document.body.appendChild(link);
         link.click();
         link.click();
         document.body.removeChild(link);
         document.body.removeChild(link);
         document.getElementById('tierlist-maker').classList.remove('exporting');
         if (isBlob) setTimeout(() => URL.revokeObjectURL(url), 0);
       }).catch(function() {
      } catch (e) {
         document.getElementById('tierlist-maker').classList.remove('exporting');
        const dataUrl = canvas.toDataURL('image/png');
        alert('保存PNG失败。');
        window.open(dataUrl, '_blank');
      }
    }
    if (canvas.toBlob) {
      canvas.toBlob(function(blob) {
        if (blob) done(blob);
        else done(canvas.toDataURL('image/png'));
      }, 'image/png');
    } else {
      done(canvas.toDataURL('image/png'));
    }
  }
  function prepareImagesForExport(container) {
    if (!container) return;
    container.querySelectorAll('img').forEach(ensureCrossOrigin);
  }
  function savePNG() {
    const maker = document.getElementById('tierlist-maker');
    const table = document.getElementById('tierlist-table');
    if (!table) {
      alert('未找到要导出的表格。');
      return;
    }
   
    // 添加导出样式类
    maker.classList.add('exporting');
    prepareImagesForExport(table);
   
    ensureHtml2Canvas(function() {
      // 稍微延迟以确保CSS生效
       setTimeout(function() {
        html2canvas(table, {
          backgroundColor: '#ffffff',
          scale: 2,
          useCORS: true,
          allowTaint: false,
          imageTimeout: 15000
        }).then(function(canvas) {
          downloadCanvas(canvas);
        }).catch(function(err) {
          console.error('html2canvas 失败', err);
          alert('保存PNG失败:' + (err && err.message ? err.message : '未知错误'));
         }).finally(function() {
          maker.classList.remove('exporting');
        });
      }, 100);
    });
  }
  function resetAll() {
    const pool = document.getElementById('character-pool');
    if (!pool) return;
    const rows = document.querySelectorAll('#tierlist-table .tier-dropzone');
    rows.forEach(function(zone) {
      if (zone.classList.contains('pool')) return;
      Array.from(zone.querySelectorAll('.avatar-frame')).forEach(function(av) {
        pool.appendChild(av);
       });
       });
      cleanupEmptyPlaceholdersIn(zone);
     });
     });
    cleanupEmptyPlaceholdersIn(pool);
   }
   }
   ready(function() {
   ready(function() {
    // Initialize table heads editable and with tools
     document.querySelectorAll('#tierlist-table .tier-head').forEach(buildEditableHead);
     document.querySelectorAll('#tierlist-table .tier-head').forEach(buildEditableHead);
    // Initialize dropzones
     document.querySelectorAll('.tier-dropzone').forEach(initDropzone);
     document.querySelectorAll('.tier-dropzone').forEach(initDropzone);
    // Initialize avatars in pool
     initAvatars(document.getElementById('character-pool'));
     initAvatars(document.getElementById('character-pool'));
    // Controls
 
     document.getElementById('add-row').addEventListener('click', addNewRow);
     var addBtn = document.getElementById('add-row');
     document.getElementById('save-png').addEventListener('click', savePNG);
    if (addBtn) addBtn.addEventListener('click', addNewRow);
     // If avatars are loaded later (SMW), observe mutations to init new avatars
     var saveBtn = document.getElementById('save-png');
    if (saveBtn) saveBtn.addEventListener('click', savePNG);
 
     (function insertResetButton() {
      const controls = document.querySelector('#tierlist-maker .tierlist-controls');
      if (!controls || document.getElementById('reset-roles')) return;
      const resetBtn = document.createElement('div');
      resetBtn.id = 'reset-roles';
      resetBtn.className = 'btn';
      resetBtn.textContent = '重置';
      controls.insertBefore(resetBtn, controls.querySelector('#save-png') || null);
      resetBtn.addEventListener('click', resetAll);
    })();
 
     const pool = document.getElementById('character-pool');
     const pool = document.getElementById('character-pool');
     const obs = new MutationObserver(function(muts) {
     if (pool) {
      muts.forEach(function(m) {
      const obs = new MutationObserver(function(muts) {
        if (m.addedNodes && m.addedNodes.length) {
        muts.forEach(function(m) {
          m.addedNodes.forEach(function(n) {
          if (m.addedNodes && m.addedNodes.length) {
            if (n.nodeType === 1) {
            m.addedNodes.forEach(function(n) {
              if (n.classList.contains('avatar-frame')) makeDraggable(n);
              if (n.nodeType === 1) {
              // Also init any nested avatar frames
                if (n.classList.contains('avatar-frame')) {
              n.querySelectorAll && n.querySelectorAll('.avatar-frame').forEach(makeDraggable);
                  makeDraggable(n);
             }
                }
           });
                n.querySelectorAll && n.querySelectorAll('.avatar-frame').forEach(makeDraggable);
         }
                if (pool.contains(n)) cleanupEmptyPlaceholdersIn(pool);
              }
             });
           }
         });
       });
       });
    });
      obs.observe(pool, { childList: true, subtree: true });
    obs.observe(pool, { childList: true, subtree: true });
    }
   });
   });
})();
})();
</script>
</script>
</includeonly>
</includeonly>

2025年10月29日 (三) 14:50的最新版本