TierListMaker:修订间差异
来自卡厄思梦境WIKI
无编辑摘要 |
无编辑摘要 |
||
| (未显示同一用户的6个中间版本) | |||
| 第37行: | 第37行: | ||
.tier-row .tier-cell { | .tier-row .tier-cell { | ||
border: 1px solid #ccc; | border: 1px solid #ccc; | ||
padding: | 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; | ||
} | } | ||
</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 || ''); | ||
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) { | ||
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 | if (dragging) { | ||
zone. | 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); | ||
const initialBg = th.getAttribute('data-initial-bg') || '#555'; | |||
const initialBg = th.getAttribute('data-initial-bg') || ' | |||
th.style.background = initialBg; | th.style.background = initialBg; | ||
th.style.color = | th.style.color = contrastColor(initialBg); | ||
attachColorPalette(toolsWrap.querySelector('.color-toggle'), th); | attachColorPalette(toolsWrap.querySelector('.color-toggle'), th); | ||
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'); | ||
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( | 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.textContent = '新行'; | th.textContent = '新行'; | ||
const td = document.createElement('td'); | const td = document.createElement('td'); | ||
| 第347行: | 第395行: | ||
document.body.appendChild(s); | document.body.appendChild(s); | ||
} | } | ||
function | function downloadCanvas(canvas) { | ||
function done(blobOrUrl) { | |||
try { | |||
const link = document.createElement('a'); | const link = document.createElement('a'); | ||
link.href = | 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. | if (isBlob) setTimeout(() => URL.revokeObjectURL(url), 0); | ||
}).catch(function() { | } catch (e) { | ||
document.getElementById('tierlist- | const dataUrl = canvas.toDataURL('image/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() { | ||
document.querySelectorAll('#tierlist-table .tier-head').forEach(buildEditableHead); | document.querySelectorAll('#tierlist-table .tier-head').forEach(buildEditableHead); | ||
document.querySelectorAll('.tier-dropzone').forEach(initDropzone); | document.querySelectorAll('.tier-dropzone').forEach(initDropzone); | ||
initAvatars(document.getElementById('character-pool')); | initAvatars(document.getElementById('character-pool')); | ||
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); | ||
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) { | ||
const obs = new MutationObserver(function(muts) { | |||
muts.forEach(function(m) { | |||
if (m.addedNodes && m.addedNodes.length) { | |||
m.addedNodes.forEach(function(n) { | |||
if (n.nodeType === 1) { | |||
if (n.classList.contains('avatar-frame')) { | |||
makeDraggable(n); | |||
} | } | ||
} | n.querySelectorAll && n.querySelectorAll('.avatar-frame').forEach(makeDraggable); | ||
} | if (pool.contains(n)) cleanupEmptyPlaceholdersIn(pool); | ||
} | |||
}); | |||
} | |||
}); | |||
}); | }); | ||
obs.observe(pool, { childList: true, subtree: true }); | |||
} | |||
}); | }); | ||
})(); | })(); | ||
</script> | </script> | ||
</includeonly> | </includeonly> | ||