微件

TierListMaker:修订间差异

来自卡厄思梦境WIKI

律Rhyme留言 | 贡献
无编辑摘要
律Rhyme留言 | 贡献
无编辑摘要
第1行: 第1行:
<includeonly>
<includeonly>
<style>
<style>
.avatar-frame {
.tierlist-wrapper {
    position: relative;
  max-width: 1200px;
    display: inline-block;
  margin: 0 auto;
    vertical-align: top;
  padding: 8px;
    border: 3px solid #ccc;
}
    border-radius: 5px;
.tierlist-controls {
    overflow: hidden;
  display: flex;
    box-shadow: 0 2px 5px rgba(0,0,0,0.1);  
  gap: 8px;
    background: #f5f5f5;
  align-items: center;
    transition: transform 0.3s ease, box-shadow 0.3s ease;
  flex-wrap: wrap;
    cursor: move;
  margin-bottom: 8px;
    margin: 2px;
}
.control-btn {
  display: inline-block;
  padding: 6px 12px;
  background: #3498db;
  color: #fff;
  border-radius: 4px;
  cursor: pointer;
  user-select: none;
}
.control-btn:hover { background: #2980b9; }
.control-tips { color: #666; font-size: 12px; }
.tierlist-table {
  width: 100%;
  table-layout: fixed;
}
.tierlist-table th.tier-th {
  width: 120px;
  text-align: center;
  vertical-align: middle;
  color: #fff;
  position: relative;
  padding: 0;
}
}
.avatar-frame:hover {
.tier-label {
    transform: scale(1.05);
  padding: 6px 8px;
    box-shadow: 0 4px 8px rgba(0,0,0,0.2);
  font-size: 18px;
    z-index: 10;
  font-weight: bold;
  line-height: 1.2;
  outline: none;
  cursor: text;
}
}
.avatar-frame.dragging {
.tierlist-table td {
    opacity: 0.5;
  padding: 4px;
}
}
.avatar-frame img {
.tier-dropzone {
    display: block;
  min-height: 116px;
    width: 100px;
  border: 2px dashed #ddd;
    height: 100px;
  border-radius: 6px;
    object-fit: cover;
  background: #fafafa;
    pointer-events: none;
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 6px;
  padding: 6px;
}
}
.avatar-name {
.tier-dropzone.over {
    position: absolute;
  border-color: #3498db;
    left: 0;
  background: #f0f8ff;
    bottom: 0;
    padding: 2px 8px;
    color: white;
    font-size: 12px;
    font-weight: bold;
    text-shadow: 0 0 2px black, 0 0 2px black;
    white-space: nowrap;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    border-top-right-radius: 3px;
    pointer-events: none;
}
}
.tier-content {
.char-pool {
    min-height: 120px;
  margin-top: 10px;
    transition: background-color 0.3s;
  border: 2px solid #ddd;
  border-radius: 6px;
  padding: 6px;
  background: #fff;
}
}
.tier-content.drag-over {
.pool-title {
    background-color: #e3f2fd !important;
  font-weight: bold;
    border: 2px dashed #2196F3;
  margin-bottom: 6px;
}
}
.tier-header {
.pool-dropzone.over {
    position: relative;
  border-color: #3498db;
    user-select: none;
  background: #f0f8ff;
}
}
.tier-label {
.avatar-frame {
    display: inline-block;
  position: relative;
    padding: 5px;
  display: inline-block;
  vertical-align: top;
  border: 3px solid #ccc;
  border-radius: 5px;
  overflow: hidden;
  box-shadow: 0 2px 5px rgba(0,0,0,0.1);
  background: #f5f5f5;
  transition: transform 0.3s ease, box-shadow 0.3s ease;
  cursor: grab;
}
}
.tier-label:focus {
.avatar-frame.dragging { opacity: 0.6; }
    outline: 2px solid white;
.avatar-frame:hover {
    outline-offset: -2px;
  transform: scale(1.05);
  box-shadow: 0 4px 8px rgba(0,0,0,0.2);
  z-index: 10;
}
}
.color-picker-trigger {
.avatar-frame img {
    position: absolute;
  display: block;
    top: 5px;
  width: 100px;
    right: 5px;
  height: 100px;
    font-size: 16px;
  object-fit: cover;
    cursor: pointer;
    opacity: 0.7;
    transition: opacity 0.3s;
}
}
.color-picker-trigger:hover {
.avatar-name {
    opacity: 1;
  position: absolute;
  left: 0;
  bottom: 0;
  padding: 2px 8px;
  color: white;
  font-size: 12px;
  font-weight: bold;
  text-shadow: 0 0 2px black, 0 0 2px black;
  white-space: nowrap;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  border-top-right-radius: 3px;
}
}
.control-button:hover {
.color-palette {
    opacity: 0.9;
  position: absolute;
    transform: translateY(-1px);
  z-index: 99999;
  background: #fff;
  border: 1px solid #ccc;
  padding: 6px;
  border-radius: 6px;
  box-shadow: 0 4px 10px rgba(0,0,0,0.15);
  display: none;
}
}
.control-button:active {
.color-palette .colors {
    transform: translateY(0);
  display: grid;
  grid-template-columns: repeat(6, 20px);
  gap: 6px;
}
}
#tierlist-table {
.color-swatch {
    border: 2px solid #ddd;
  width: 20px;
  height: 20px;
  border-radius: 4px;
  border: 1px solid #ccc;
  cursor: pointer;
}
}
.tier-remove-btn {
.color-palette .actions {
    position: absolute;
  margin-top: 6px;
    top: 5px;
  display: flex;
    left: 5px;
  gap: 8px;
    background: #f44336;
    color: white;
    border-radius: 3px;
    padding: 2px 6px;
    font-size: 12px;
    cursor: pointer;
    opacity: 0.7;
}
}
.tier-remove-btn:hover {
.palette-close {
    opacity: 1;
  padding: 4px 8px;
  background: #eee;
  border-radius: 4px;
  cursor: pointer;
}
}
</style>
</style>


<script>
<script>
let draggedElement = null;
(function(){
let currentEditingTier = null;
  // Utility: ideal text color for background
 
  function idealTextColor(hex) {
// 默认层级配置
    const c = hex.replace('#','');
const defaultTiers = [
    let r, g, b;
     { label: 'S', bgColor: '#ff4444', textColor: '#ffffff' },
    if (c.length === 3) {
     { label: 'A', bgColor: '#ff9800', textColor: '#ffffff' },
      r = parseInt(c[0] + c[0], 16);
     { label: 'B', bgColor: '#ffeb3b', textColor: '#000000' },
      g = parseInt(c[1] + c[1], 16);
     { label: 'C', bgColor: '#8bc34a', textColor: '#ffffff' },
      b = parseInt(c[2] + c[2], 16);
    { label: 'D', bgColor: '#2196F3', textColor: '#ffffff' }
    } else {
];
      r = parseInt(c.substring(0,2), 16);
 
      g = parseInt(c.substring(2,4), 16);
// 初始化拖拽功能
      b = parseInt(c.substring(4,6), 16);
function initDragAndDrop() {
    }
     const avatars = document.querySelectorAll('.avatar-frame');
    // relative luminance
     const tierContents = document.querySelectorAll('.tier-content');
    const luminance = (0.2126*r + 0.7152*g + 0.0722*b) / 255;
      
     return luminance > 0.55 ? '#000' : '#fff';
     avatars.forEach(avatar => {
  }
        avatar.setAttribute('draggable', 'true');
  function applyThColor(th, color) {
       
    th.setAttribute('data-color', color);
        avatar.addEventListener('dragstart', function(e) {
    th.style.backgroundColor = color;
            draggedElement = this;
    th.style.color = idealTextColor(color);
            this.classList.add('dragging');
  }
            e.dataTransfer.effectAllowed = 'move';
  function setupThColors() {
            e.dataTransfer.setData('text/html', this.outerHTML);
    const ths = document.querySelectorAll('#tierlist-table th.tier-th');
        });
    ths.forEach(th => {
       
      const color = th.getAttribute('data-color') || '#888888';
        avatar.addEventListener('dragend', function(e) {
      applyThColor(th, color);
            this.classList.remove('dragging');
      th.addEventListener('click', (e) => {
        });
        openColorPalette(th, e);
      });
    });
  }
  // Color palette
  const paletteColors = [
     '#e74c3c','#f1963b','#ffef03','#c2d402','#2ecc71','#3498db',
     '#9b59b6','#34495e','#7f8c8d','#ffffff','#000000','#f39c12',
     '#1abc9c','#8e44ad','#d35400','#27ae60','#2980b9','#bdc3c7'
  ];
  let paletteEl = null;
  function buildPalette() {
    paletteEl = document.createElement('div');
    paletteEl.className = 'color-palette';
     const colorsWrap = document.createElement('div');
    colorsWrap.className = 'colors';
    paletteColors.forEach(col => {
      const s = document.createElement('div');
      s.className = 'color-swatch';
      s.style.backgroundColor = col;
      s.addEventListener('click', () => {
        if (paletteEl._targetTh) {
          applyThColor(paletteEl._targetTh, col);
        }
        paletteEl.style.display = 'none';
      });
      colorsWrap.appendChild(s);
    });
    const actions = document.createElement('div');
    actions.className = 'actions';
     const closeBtn = document.createElement('div');
    closeBtn.className = 'palette-close';
    closeBtn.textContent = '关闭';
    closeBtn.addEventListener('click', () => paletteEl.style.display = 'none');
    actions.appendChild(closeBtn);
    paletteEl.appendChild(colorsWrap);
    paletteEl.appendChild(actions);
     document.body.appendChild(paletteEl);
     document.addEventListener('click', (e) => {
      if (!paletteEl.contains(e.target)) paletteEl.style.display = 'none';
    });
  }
  function openColorPalette(th, evt) {
    if (!paletteEl) buildPalette();
    const rect = th.getBoundingClientRect();
    paletteEl.style.left = (window.scrollX + rect.left + 10) + 'px';
    paletteEl.style.top = (window.scrollY + rect.bottom + 8) + 'px';
    paletteEl.style.display = 'block';
    paletteEl._targetTh = th;
  }
  // Drag & Drop
  let draggedEl = null;
  function setupDraggable(el) {
    if (!el || el._draggableSetup) return;
    el._draggableSetup = true;
    el.setAttribute('draggable', 'true');
    el.addEventListener('dragstart', (e) => {
      draggedEl = el;
      el.classList.add('dragging');
      e.dataTransfer.effectAllowed = 'move';
      // ensure id
      if (!el.dataset.id) {
        const img = el.querySelector('img');
        if (img && img.src) {
          const m = img.src.match(/face_character_([^./?]+)\.png/i);
          if (m) el.dataset.id = m[1];
        }
      }
    });
    el.addEventListener('dragend', () => {
      el.classList.remove('dragging');
      draggedEl = null;
    });
  }
  function setupDropzone(zone) {
    zone.addEventListener('dragover', (e) => {
      if (draggedEl) e.preventDefault();
      zone.classList.add('over');
    });
    zone.addEventListener('dragleave', () => {
      zone.classList.remove('over');
    });
    zone.addEventListener('drop', (e) => {
      e.preventDefault();
      zone.classList.remove('over');
      if (!draggedEl) return;
      // move element
      zone.appendChild(draggedEl);
      // normalize styling when moved
      draggedEl.style.pointerEvents = 'auto';
     });
     });
   
  }
    tierContents.forEach(content => {
  function initDragAndDrop() {
        content.addEventListener('dragover', function(e) {
    // make existing avatars draggable
            e.preventDefault();
    document.querySelectorAll('.avatar-frame').forEach(setupDraggable);
            e.dataTransfer.dropEffect = 'move';
    // dropzones
            this.classList.add('drag-over');
    document.querySelectorAll('.tier-dropzone').forEach(setupDropzone);
        });
    const pool = document.getElementById('char-pool');
       
    setupDropzone(pool);
        content.addEventListener('dragleave', function(e) {
  }
            this.classList.remove('drag-over');
  // Observe dynamically added avatars (from ask or later)
         });
  const poolObserver = new MutationObserver((mutations) => {
       
    mutations.forEach((m) => {
        content.addEventListener('drop', function(e) {
      m.addedNodes.forEach((node) => {
             e.preventDefault();
         if (node.nodeType === 1) {
             this.classList.remove('drag-over');
          if (node.classList && node.classList.contains('avatar-frame')) {
           
             setupDraggable(node);
            if (draggedElement) {
          } else {
                this.appendChild(draggedElement);
             node.querySelectorAll && node.querySelectorAll('.avatar-frame').forEach(setupDraggable);
                draggedElement = null;
          }
            }
        }
        });
      });
     });
     });
}
  });
 
  function startObservers() {
// 添加新行
     const pool = document.getElementById('char-pool');
function addNewTier() {
     if (pool) {
     const table = document.getElementById('tierlist-table');
      poolObserver.observe(pool, { childList: true, subtree: true });
     const tbody = table.querySelector('tbody');
    const tierCount = document.querySelectorAll('.tier-header').length;
   
    const newRow = tbody.insertRow(-1);
   
    const headerCell = newRow.insertCell(0);
    headerCell.className = 'tier-header';
    headerCell.setAttribute('data-tier', tierCount);
    headerCell.style = 'width: 100px; background-color: #9e9e9e; color: white; font-size: 20px; text-align: center; cursor: pointer; position: relative;';
    headerCell.innerHTML = '<div class="tier-label" contenteditable="true">新行</div><div class="color-picker-trigger" onclick="showColorPicker(this, ' + tierCount + ')">🎨</div><div class="tier-remove-btn" onclick="removeTier(this)">删除</div>';
   
    const contentCell = newRow.insertCell(1);
    contentCell.className = 'tier-content';
    contentCell.setAttribute('data-tier', tierCount);
    contentCell.style = 'min-height: 120px; padding: 10px; background: #fff;';
   
    initDragAndDrop();
}
 
// 删除行
function removeTier(btn) {
    if (confirm('确定要删除这一行吗?')) {
        const row = btn.closest('tr');
        row.remove();
     }
     }
}
  }
 
  // Add row
// 重置功能
  function addRow() {
function resetTierList() {
    if (!confirm('确定要重置吗?所有角色将返回角色池,自定义行将被删除。')) {
        return;
    }
   
    const characterPool = document.getElementById('character-pool');
     const table = document.getElementById('tierlist-table');
     const table = document.getElementById('tierlist-table');
     const tbody = table.querySelector('tbody');
     const tr = document.createElement('tr');
      
    const th = document.createElement('th');
     // 收集所有角色
     th.className = 'tier-th';
     const allAvatars = document.querySelectorAll('.tier-content .avatar-frame');
     applyThColor(th, '#7f8c8d');
     allAvatars.forEach(avatar => {
     const label = document.createElement('div');
        characterPool.appendChild(avatar);
    label.className = 'tier-label';
    label.setAttribute('contenteditable', 'true');
    label.textContent = 'NEW';
    th.appendChild(label);
    const td = document.createElement('td');
    const dropzone = document.createElement('div');
    dropzone.className = 'tier-dropzone';
    dropzone.setAttribute('data-tier', label.textContent || 'NEW');
     td.appendChild(dropzone);
    tr.appendChild(th);
    tr.appendChild(td);
    table.tBodies[0].appendChild(tr);
    // Enable interactions
    th.addEventListener('click', (e) => openColorPalette(th, e));
    setupDropzone(dropzone);
    // Keep data-tier synced with label edits
    label.addEventListener('input', () => {
      dropzone.setAttribute('data-tier', label.textContent.trim());
     });
     });
   
  }
    // 清空表格
  // Export to PNG by drawing canvas (no external libs)
     tbody.innerHTML = '';
  function exportPNG() {
      
     const table = document.getElementById('tierlist-table');
     // 重建默认行
     const tableRect = table.getBoundingClientRect();
     defaultTiers.forEach((tier, index) => {
    const thWidth = table.querySelector('th.tier-th')?.getBoundingClientRect().width || 120;
        const newRow = tbody.insertRow(-1);
     // Collect rows
       
     const rows = Array.from(table.querySelectorAll('tr'));
        const headerCell = newRow.insertCell(0);
    const rowInfo = rows.map(row => {
        headerCell.className = 'tier-header';
      const th = row.querySelector('th.tier-th');
        headerCell.setAttribute('data-tier', index);
      const dz = row.querySelector('.tier-dropzone');
        headerCell.style = `width: 100px; background-color: ${tier.bgColor}; color: ${tier.textColor}; font-size: 20px; text-align: center; cursor: pointer; position: relative;`;
      const thRect = th.getBoundingClientRect();
        headerCell.innerHTML = `<div class="tier-label" contenteditable="true">${tier.label}</div><div class="color-picker-trigger" onclick="showColorPicker(this, ${index})">🎨</div>`;
      const dzRect = dz.getBoundingClientRect();
       
      const height = Math.max(thRect.height, dzRect.height);
         const contentCell = newRow.insertCell(1);
      const label = th.querySelector('.tier-label')?.textContent || '';
         contentCell.className = 'tier-content';
      const color = th.getAttribute('data-color') || '#888';
        contentCell.setAttribute('data-tier', index);
      const textColor = idealTextColor(color);
         contentCell.style = 'min-height: 120px; padding: 10px; background: #fff;';
      // collect avatars in this dropzone
      const avatars = Array.from(dz.querySelectorAll('.avatar-frame')).map(av => {
        const img = av.querySelector('img');
         const rect = av.getBoundingClientRect();
         return {
          imgSrc: img ? img.src : null,
          x: rect.left - tableRect.left,
          y: rect.top - tableRect.top,
          w: img ? img.width : 100,
          h: img ? img.height : 100,
          name: av.querySelector('.avatar-name')?.textContent || ''
         };
      });
      return { thRect, dzRect, height, label, color, textColor, avatars };
     });
     });
      
     const totalHeight = rowInfo.reduce((acc, r) => acc + r.height, 0);
    initDragAndDrop();
     const width = tableRect.width;
}
     const canvas = document.createElement('canvas');
 
     canvas.width = Math.ceil(width);
// 显示颜色选择器
     canvas.height = Math.ceil(totalHeight);
function showColorPicker(trigger, tier) {
     const ctx = canvas.getContext('2d');
    event.stopPropagation();
     // Background
     currentEditingTier = tier;
     ctx.fillStyle = '#ffffff';
   
     ctx.fillRect(0, 0, canvas.width, canvas.height);
     const header = document.querySelector('.tier-header[data-tier="' + tier + '"]');
    // Load all images
     const bgColor = rgbToHex(header.style.backgroundColor);
     const loadTasks = [];
     const textColor = rgbToHex(header.style.color);
    rowInfo.forEach(r => {
      
      r.avatars.forEach(a => {
    document.getElementById('bg-color-picker').value = bgColor;
         if (a.imgSrc) {
     document.getElementById('text-color-picker').value = textColor;
          loadTasks.push(new Promise((resolve) => {
     document.getElementById('color-picker-modal').style.display = 'block';
            const img = new Image();
     document.getElementById('modal-overlay').style.display = 'block';
            img.onload = () => resolve({ a, img });
}
            img.onerror = () => resolve({ a, img: null });
 
            img.src = a.imgSrc;
// 应用颜色
          }));
function applyColor() {
         }
     if (currentEditingTier !== null) {
      });
        const bgColor = document.getElementById('bg-color-picker').value;
        const textColor = document.getElementById('text-color-picker').value;
        const header = document.querySelector('.tier-header[data-tier="' + currentEditingTier + '"]');
       
        header.style.backgroundColor = bgColor;
         header.style.color = textColor;
    }
    closeColorPicker();
}
 
// 关闭颜色选择器
function closeColorPicker() {
    document.getElementById('color-picker-modal').style.display = 'none';
    document.getElementById('modal-overlay').style.display = 'none';
    currentEditingTier = null;
}
 
// RGB转HEX
function rgbToHex(rgb) {
    if (!rgb || rgb.indexOf('rgb') === -1) return '#000000';
   
    const values = rgb.match(/\d+/g);
    if (!values) return '#000000';
   
    const hex = values.map(x => {
        const hexValue = parseInt(x).toString(16);
         return hexValue.length === 1 ? '0' + hexValue : hexValue;
     });
     });
      
     Promise.all(loadTasks).then(results => {
    return '#' + hex.join('');
      const imgMap = new Map();
}
      results.forEach(({ a, img }) => {
 
        if (img) imgMap.set(a.imgSrc, img);
// 导出为PNG
      });
function exportToPNG() {
      // Draw rows
    const container = document.getElementById('tier-list-container');
      let yCursor = 0;
   
      rows.forEach((row, idx) => {
    // 使用html2canvas库
        const info = rowInfo[idx];
    if (typeof html2canvas === 'undefined') {
        // Left header
         // 动态加载html2canvas
        ctx.fillStyle = info.color;
        const script = document.createElement('script');
        ctx.fillRect(0, yCursor, thWidth, info.height);
         script.src = 'https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js';
        // Label text centered
         script.onload = function() {
        ctx.fillStyle = info.textColor;
            captureAndDownload(container);
        ctx.font = 'bold 20px sans-serif';
        };
         ctx.textAlign = 'center';
        document.head.appendChild(script);
         ctx.textBaseline = 'middle';
    } else {
        ctx.fillText(info.label, thWidth / 2, yCursor + info.height / 2);
        captureAndDownload(container);
        // Draw avatars (absolute positions relative to table)
    }
         info.avatars.forEach(a => {
}
          const img = imgMap.get(a.imgSrc);
 
          if (img) {
function captureAndDownload(container) {
            const x = Math.max(thWidth + 6, Math.round(a.x));
    html2canvas(container, {
            const y = Math.round(a.y);
        backgroundColor: '#ffffff',
            // If rect outside current row vertical span, clamp within this row
        scale: 2,
            const rowTop = yCursor;
        logging: false,
            const rowBottom = yCursor + info.height - a.h - 6;
        useCORS: true
            const yDraw = Math.min(Math.max(y, rowTop + 6), rowBottom);
    }).then(canvas => {
            // Frame background
        canvas.toBlob(function(blob) {
            ctx.fillStyle = '#f5f5f5';
             const url = URL.createObjectURL(blob);
            ctx.fillRect(x - 3, yDraw - 3, a.w + 6, a.h + 6);
             const a = document.createElement('a');
            // Border
            a.href = url;
            ctx.strokeStyle = '#cccccc';
            a.download = 'tier-list-' + new Date().getTime() + '.png';
            ctx.lineWidth = 3;
            a.click();
            ctx.strokeRect(x - 3, yDraw - 3, a.w + 6, a.h + 6);
            URL.revokeObjectURL(url);
            // Image
             ctx.drawImage(img, x, yDraw, a.w, a.h);
             // Name bar
            if (a.name) {
              ctx.fillStyle = 'rgba(0,0,0,0.6)';
              ctx.fillRect(x, yDraw + a.h - 18, a.w, 18);
              ctx.fillStyle = '#ffffff';
              ctx.font = 'bold 12px sans-serif';
              ctx.textAlign = 'left';
              ctx.textBaseline = 'middle';
              const nameText = a.name.length > 18 ? a.name.slice(0, 17) + '…' : a.name;
              ctx.fillText(nameText, x + 6, yDraw + a.h - 9);
            }
          }
         });
         });
        yCursor += info.height;
      });
      // Download
      try {
        const url = canvas.toDataURL('image/png');
        const a = document.createElement('a');
        a.href = url;
        a.download = 'TierList.png';
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
      } catch (e) {
        // Fallback: open in new tab
        const url = canvas.toDataURL('image/png');
        window.open(url, '_blank');
      }
     });
     });
}
  }
 
  function init() {
// 页面加载完成后初始化
     setupThColors();
if (document.readyState === 'loading') {
     document.addEventListener('DOMContentLoaded', initDragAndDrop);
} else {
     initDragAndDrop();
     initDragAndDrop();
}
    startObservers();
 
    // Keep data-tier in sync when user edits labels on existing rows
// 监听动态添加的元素
    document.querySelectorAll('#tierlist-table tr').forEach(tr => {
const observer = new MutationObserver(function(mutations) {
      const label = tr.querySelector('.tier-label');
    initDragAndDrop();
      const dz = tr.querySelector('.tier-dropzone');
});
      if (label && dz) {
 
        label.addEventListener('input', () => {
observer.observe(document.body, {
          dz.setAttribute('data-tier', label.textContent.trim());
     childList: true,
        });
     subtree: true
      }
});
    });
    // Controls
    const addRowBtn = document.getElementById('add-row');
    const saveBtn = document.getElementById('save-png');
    addRowBtn && addRowBtn.addEventListener('click', addRow);
    saveBtn && saveBtn.addEventListener('click', exportPNG);
  }
  if (document.readyState === 'complete' || document.readyState === 'interactive') {
     setTimeout(init, 0);
  } else {
     document.addEventListener('DOMContentLoaded', init);
  }
})();
</script>
</script>
</includeonly>
</includeonly>

2025年10月20日 (一) 22:25的版本