TierListMaker:修订间差异
来自卡厄思梦境WIKI
无编辑摘要 |
无编辑摘要 |
||
| (未显示同一用户的19个中间版本) | |||
| 第1行: | 第1行: | ||
<includeonly> | <includeonly> | ||
<style> | <style> | ||
. | .tierlist-controls { | ||
display: flex; | |||
gap: 8px; | |||
margin: 8px 0 12px 0; | |||
} | } | ||
.btn { | |||
display: inline-block; | |||
padding: 6px 12px; | |||
background: #f0f0f0; | |||
border: 1px solid #ccc; | |||
color: #333; | |||
border-radius: 4px; | |||
cursor: pointer; | |||
user-select: none; | |||
} | } | ||
.btn:hover { background: #e6e6e6; } | |||
.btn.primary { background: #4a8cf6; color: #fff; border-color: #3b78de; } | |||
.btn.primary:hover { background: #3b78de; } | |||
.tierlist-table { | .tierlist-table { | ||
width: 100%; | |||
table-layout: fixed; | |||
} | } | ||
.tier-row .tier-head { | |||
. | position: relative; | ||
width: 120px; | |||
color: #fff; | |||
text-align: center; | |||
font-size: 18px; | |||
font-weight: bold; | |||
padding: 8px; | |||
white-space: nowrap; | |||
border: 1px solid #ccc; | |||
background: #555; | |||
} | } | ||
.tier-row .tier-cell { | |||
. | border: 1px solid #ccc; | ||
padding: 0px; | |||
} | } | ||
.tier-tools { | |||
. | position: absolute; | ||
right: 6px; | |||
top: 6px; | |||
display: flex; | |||
gap: 6px; | |||
} | } | ||
/* 导出时隐藏工具按钮 */ | |||
. | .exporting .tier-tools { | ||
display: none !important; | |||
} | } | ||
.color-toggle { | |||
. | width: 18px; | ||
height: 18px; | |||
border-radius: 3px; | |||
border: 1px solid rgba(0,0,0,0.2); | |||
cursor: pointer; | |||
background: rgba(255,255,255,0.6); | |||
} | } | ||
.delete-row { | |||
font-size: 12px; | |||
padding: 2px 6px; | |||
border-radius: 3px; | |||
background: rgba(0,0,0,0.1); | |||
cursor: pointer; | |||
} | } | ||
.delete-row:hover { background: rgba(0,0,0,0.2); } | |||
. | .tier-dropzone { | ||
min-height: 112px; | |||
display: flex; | |||
flex-wrap: wrap; | |||
gap: 6px; | |||
align-items: flex-start; | |||
padding: 4px; | |||
} | } | ||
.tier-dropzone.pool { | |||
. | border: 2px dashed #ccc; | ||
padding: 8px; | |||
border-radius: 6px; | |||
background: #fafafa; | |||
} | } | ||
/* 导出时隐藏角色池 */ | |||
. | .exporting .pool-wrapper { | ||
display: none !important; | |||
} | } | ||
.pool-wrapper { margin-top: 12px; } | |||
. | .pool-header { | ||
font-weight: bold; | |||
margin-bottom: 6px; | |||
} | } | ||
.avatar-frame { | |||
position: relative; | |||
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.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; | |||
cursor: move; | |||
} | } | ||
.avatar-frame.dragging { | |||
. | opacity: 0.7; | ||
transform: scale(1.03); | |||
border-color: #4a8cf6; | |||
z-index: 10; | |||
} | } | ||
.avatar-frame img { | |||
display: block; | |||
width: 100px; | |||
height: 100px; | |||
object-fit: cover; | |||
pointer-events: none; | |||
-webkit-user-drag: none; | |||
} | } | ||
.avatar-name { | |||
. | 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; | |||
} | } | ||
.color-palette { | |||
. | position: absolute; | ||
display: none; | |||
gap: 6px; | |||
flex-wrap: wrap; | |||
width: 210px; | |||
padding: 8px; | |||
background: #fff; | |||
border: 1px solid #ccc; | |||
box-shadow: 0 2px 8px rgba(0,0,0,0.15); | |||
border-radius: 6px; | |||
z-index: 9999; | |||
} | } | ||
.color-swatch { | |||
width: 24px; height: 24px; | |||
border-radius: 4px; | |||
border: 1px solid rgba(0,0,0,0.2); | |||
cursor: pointer; | |||
} | } | ||
.color-swatch:hover { outline: 2px solid rgba(0,0,0,0.2); } | |||
. | .tier-label { | ||
display: inline-block; | |||
padding: 2px 6px; | |||
border-radius: 3px; | |||
background: rgba(255,255,255,0.15); | |||
} | } | ||
.tier-label[contenteditable="true"] { | |||
. | outline: none; | ||
cursor: text; | |||
} | } | ||
</style> | |||
. | <script> | ||
(function() { | |||
} | function ready(fn) { | ||
if (document.readyState !== 'loading') fn(); | |||
else document.addEventListener('DOMContentLoaded', fn); | |||
} | |||
function contrastColor(hex) { | |||
hex = (hex || '').replace('#',''); | |||
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 g = parseInt(hex.substr(2,2), 16); | |||
var b = parseInt(hex.substr(4,2), 16); | |||
var yiq = ((r*299)+(g*587)+(b*114))/1000; | |||
return yiq >= 128 ? '#000' : '#fff'; | |||
} | |||
function getDragAfterElement(container, y) { | |||
const elements = [...container.querySelectorAll('.avatar-frame:not(.dragging)')]; | |||
return elements.reduce((closest, child) => { | |||
const box = child.getBoundingClientRect(); | |||
const offset = y - box.top - box.height/2; | |||
if (offset < 0 && offset > closest.offset) { | |||
return { offset: offset, element: child }; | |||
} else { | |||
return closest; | |||
} | |||
}, { 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) { | |||
if (!avatar || avatar._draggableInit) return; | |||
avatar._draggableInit = true; | |||
avatar.setAttribute('draggable', 'true'); | |||
avatar.classList.add('draggable-avatar'); | |||
avatar.querySelectorAll('img').forEach(ensureCrossOrigin); | |||
. | avatar.addEventListener('dragstart', function(e) { | ||
avatar.classList.add('dragging'); | |||
} | avatar._prevParent = avatar.parentNode; | ||
e.dataTransfer.setData('text/plain', avatar.dataset.id || avatar.querySelector('.avatar-name')?.textContent || ''); | |||
e.dataTransfer.effectAllowed = 'move'; | |||
}); | |||
avatar.addEventListener('dragend', function() { | |||
avatar.classList.remove('dragging'); | |||
cleanupEmptyWrapper(avatar._prevParent); | |||
avatar._prevParent = null; | |||
}); | |||
} | |||
function initAvatars(root) { | |||
if (!root) return; | |||
const avatars = root.querySelectorAll('.avatar-frame'); | |||
avatars.forEach(function(av) { | |||
if (!av.dataset.id) { | |||
const nameEl = av.querySelector('.avatar-name'); | |||
av.dataset.name = nameEl ? nameEl.textContent.trim() : ''; | |||
} | |||
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) { | |||
if (!zone || zone._dropzoneInit) return; | |||
zone._dropzoneInit = true; | |||
zone.addEventListener('dragover', function(e) { | |||
. | e.preventDefault(); | ||
e.dataTransfer.dropEffect = 'move'; | |||
const afterElement = getDragAfterElement(zone, e.clientY); | |||
const dragging = document.querySelector('.avatar-frame.dragging'); | |||
if (!dragging) return; | |||
if (afterElement == null) { | |||
if (dragging.parentNode !== zone) zone.appendChild(dragging); | |||
} else { | |||
if (afterElement !== dragging) zone.insertBefore(dragging, afterElement); | |||
} | |||
}); | |||
. | zone.addEventListener('drop', function(e) { | ||
e.preventDefault(); | |||
e.stopPropagation(); | |||
const dragging = document.querySelector('.avatar-frame.dragging'); | |||
.color- | if (dragging) { | ||
if (dragging.parentNode !== zone) zone.appendChild(dragging); | |||
cleanupEmptyWrapper(dragging._prevParent); | |||
dragging._prevParent = null; | |||
} | |||
.color- | if (zone.classList.contains('pool')) { | ||
.color- | cleanupEmptyPlaceholdersIn(zone); | ||
} | |||
}); | |||
} | |||
function buildEditableHead(th) { | |||
const currentText = (th.childNodes[0] && th.childNodes[0].nodeType === 3) | |||
? th.childNodes[0].nodeValue.trim() | |||
. | : th.textContent.trim(); | ||
const tools = th.querySelector('.tier-tools'); | |||
th.innerHTML = ''; | |||
const label = document.createElement('div'); | |||
} | label.className = 'tier-label'; | ||
label.setAttribute('contenteditable', 'true'); | |||
. | label.textContent = currentText || '未命名'; | ||
const toolsWrap = tools || (function() { | |||
const t = document.createElement('div'); | |||
t.className = 'tier-tools'; | |||
const color = document.createElement('div'); | |||
color.className = 'color-toggle'; | |||
color.title = '更改颜色'; | |||
} | const del = document.createElement('div'); | ||
del.className = 'delete-row'; | |||
del.textContent = '删除'; | |||
del.title = '删除该行'; | |||
t.appendChild(color); t.appendChild(del); | |||
return t; | |||
})(); | |||
th.appendChild(label); | |||
th.appendChild(toolsWrap); | |||
const initialBg = th.getAttribute('data-initial-bg') || '#555'; | |||
th.style.background = initialBg; | |||
th.style.color = contrastColor(initialBg); | |||
attachColorPalette(toolsWrap.querySelector('.color-toggle'), th); | |||
const delBtn = toolsWrap.querySelector('.delete-row'); | |||
delBtn.addEventListener('click', function() { | |||
const tr = th.closest('tr'); | |||
const dropzone = tr.querySelector('.tier-dropzone'); | |||
const pool = document.getElementById('character-pool'); | |||
Array.from(dropzone.querySelectorAll('.avatar-frame')).forEach(function(av) { | |||
pool.appendChild(av); | |||
}); | |||
tr.parentNode.removeChild(tr); | |||
}); | |||
} | |||
function attachColorPalette(toggle, th) { | |||
if (!toggle) return; | |||
const palette = document.createElement('div'); | |||
palette.className = 'color-palette'; | |||
const colors = [ | |||
'#e53935','#d81b60','#8e24aa','#5e35b1','#3949ab','#1e88e5','#039be5','#00acc1', | |||
'#00897b','#43a047','#7cb342','#c0ca33','#fdd835','#fb8c00','#f4511e','#6d4c41', | |||
'#546e7a','#9e9e9e','#000000','#ffffff' | |||
]; | |||
colors.forEach(function(c) { | |||
const sw = document.createElement('div'); | |||
sw.className = 'color-swatch'; | |||
sw.style.background = c; | |||
sw.addEventListener('click', function() { | |||
th.style.background = c; | |||
th.style.color = contrastColor(c); | |||
palette.style.display = 'none'; | |||
}); | |||
palette.appendChild(sw); | |||
}); | |||
document.body.appendChild(palette); | |||
function placePalette() { | |||
const rect = toggle.getBoundingClientRect(); | |||
palette.style.left = (window.scrollX + rect.left) + 'px'; | |||
palette.style.top = (window.scrollY + rect.bottom + 6) + 'px'; | |||
} | |||
toggle.addEventListener('click', function(e) { | |||
e.stopPropagation(); | |||
if (palette.style.display === 'block') { | |||
palette.style.display = 'none'; | |||
} else { | |||
placePalette(); | |||
palette.style.display = 'block'; | |||
} | |||
}); | |||
document.addEventListener('click', function(e) { | |||
if (e.target === toggle || palette.contains(e.target)) return; | |||
palette.style.display = 'none'; | |||
}); | |||
window.addEventListener('scroll', function() { | |||
if (palette.style.display === 'block') placePalette(); | |||
}); | |||
window.addEventListener('resize', function() { | |||
if (palette.style.display === 'block') placePalette(); | |||
}); | |||
} | |||
function addNewRow() { | |||
const tbody = document.querySelector('#tierlist-table tbody') || document.querySelector('#tierlist-table'); | |||
const tr = document.createElement('tr'); | |||
tr.className = 'tier-row'; | |||
const th = document.createElement('th'); | |||
th.className = 'tier-head'; | |||
th.setAttribute('data-initial-bg', '#8888ff'); | |||
th.textContent = '新行'; | |||
const td = document.createElement('td'); | |||
td.className = 'tier-cell'; | |||
const dz = document.createElement('div'); | |||
dz.className = 'tier-dropzone'; | |||
dz.setAttribute('data-tier', 'CUSTOM'); | |||
td.appendChild(dz); | |||
tr.appendChild(th); | |||
tr.appendChild(td); | |||
tbody.appendChild(tr); | |||
initDropzone(dz); | |||
buildEditableHead(th); | |||
} | |||
function ensureHtml2Canvas(cb) { | |||
if (window.html2canvas) { cb(); return; } | |||
var s = document.createElement('script'); | |||
s.src = 'https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js'; | |||
s.onload = cb; | |||
s.onerror = function() { | |||
alert('加载截图库失败,请检查网络或跨域策略。'); | |||
}; | |||
document.body.appendChild(s); | |||
} | |||
function downloadCanvas(canvas) { | |||
function done(blobOrUrl) { | |||
try { | |||
const link = document.createElement('a'); | |||
const isBlob = blobOrUrl instanceof Blob; | |||
const url = isBlob ? (URL.createObjectURL(blobOrUrl)) : blobOrUrl; | |||
link.href = url; | |||
link.download = 'tierlist.png'; | |||
document.body.appendChild(link); | |||
link.click(); | |||
document.body.removeChild(link); | |||
if (isBlob) setTimeout(() => URL.revokeObjectURL(url), 0); | |||
} catch (e) { | |||
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() { | ||
document.querySelectorAll('#tierlist-table .tier-head').forEach(buildEditableHead); | |||
document.querySelectorAll('.tier-dropzone').forEach(initDropzone); | |||
initAvatars(document.getElementById('character-pool')); | |||
. | var addBtn = document.getElementById('add-row'); | ||
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'); | |||
( | if (pool) { | ||
const obs = new MutationObserver(function(muts) { | |||
muts.forEach(function(m) { | |||
if (m.addedNodes && m.addedNodes.length) { | |||
if ( | m.addedNodes.forEach(function(n) { | ||
if (n.nodeType === 1) { | |||
if (n.classList.contains('avatar-frame')) { | |||
makeDraggable(n); | |||
if ( | |||
} | } | ||
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> | ||