Files
lol-skin/.data/www/网页/js/skins.js
T

676 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================
skins.js — 皮肤库
英雄列表:/api/champions → LCU champion-summary.json
英雄头像:/api/heroicon?id= → LCU hero json → skins[0].tilePath
皮肤数据:/api/hero?id= → LCU hero json
图片代理:/api/img?path=
============================================= */
const ROLES = [
{ key: '', label: '全部', icon: '🌐' },
{ key: 'fighter', label: '战士', icon: '⚔️' },
{ key: 'assassin', label: '刺客', icon: '🗡️' },
{ key: 'mage', label: '法师', icon: '🔮' },
{ key: 'marksman', label: '射手', icon: '🏹' },
{ key: 'support', label: '辅助', icon: '💙' },
{ key: 'tank', label: '坦克', icon: '🛡️' },
];
let _champions = [];
let _curRole = '';
let _curQuery = '';
// ── 英雄列表页 ────────────────────────────────
async function renderSkinLib() {
const page = document.getElementById('page-skins');
resetLazyObserver();
if (!APP.summonerId) {
page.innerHTML = '<div class="placeholder-page"><p>请先启动 LOL 客户端</p></div>';
return;
}
page.innerHTML = `
<div class="skins-toolbar">
<div class="role-tabs" id="roleTabs">
${ROLES.map(r => `
<div class="role-tab ${r.key === _curRole ? 'active' : ''}"
data-role="${r.key}" onclick="filterRole('${r.key}')">
<span class="role-tab-icon">${r.icon ?? ''}</span><span>${r.label}</span>
</div>`).join('')}
</div>
<div class="champ-search-box">
<span class="champ-count" id="champCount"></span>
<input class="search-input" id="champSearch" placeholder="搜索英雄..."
value="${esc(_curQuery)}" oninput="filterSearch(this.value)">
</div>
</div>
<div class="champ-grid-card">
<div class="champ-grid-scroll">
<div id="champGrid" class="champ-grid"><div class="loading-spin">加载英雄</div></div>
</div>
</div>`;
if (_champions.length === 0) await loadChampions();
else applyFilter();
}
async function loadChampions() {
const cached = Cache.get('champions_lcu');
if (cached && cached.length > 0) {
_champions = cached;
applyFilter();
return;
}
try {
// 腾讯接口返回 { hero: [...] }
// 字段:heroId / name / title / alias / keywords / roles[]
const data = await API.get('champions');
const list = Array.isArray(data) ? data : (data.hero || []);
_champions = list.filter(c => (c.id || c.heroId) && parseInt(c.id || c.heroId) > 0);
Cache.set('champions_lcu', _champions);
applyFilter();
} catch(e) {
console.error('[loadChampions]', e);
const g = document.getElementById('champGrid');
if (g) g.innerHTML = '<div class="placeholder-page"><p>加载失败</p></div>';
}
}
function filterRole(role) {
_curRole = role;
document.querySelectorAll('.role-tab').forEach(el =>
el.classList.toggle('active', el.dataset.role === role));
applyFilter();
}
function filterSearch(q) { _curQuery = q.trim().toLowerCase(); applyFilter(); }
// 模糊搜索:中文名 / 英文名alias / 拼音缩写keywords
function champMatch(c, q) {
if (!q) return true;
const name = (c.name || '').toLowerCase();
const alias = (c.alias || '').toLowerCase();
const keywords = (c.keywords || '').toLowerCase(); // 腾讯接口:拼音全拼+缩写,如 "yasuo,ys,ysjh,疾风剑豪"
const title = (c.title || '').toLowerCase(); // 小名,如 "不败之地"
return name.includes(q) || alias.includes(q) || keywords.includes(q) || title.includes(q);
}
function applyFilter() {
let list = _champions;
if (_curRole) {
list = list.filter(c =>
Array.isArray(c.roles) &&
c.roles.some(r => r.toLowerCase() === _curRole)
);
}
if (_curQuery) {
list = list.filter(c => champMatch(c, _curQuery));
}
const countEl = document.getElementById('champCount');
if (countEl) countEl.innerHTML = `共有 <span class="champ-count-num">${list.length}</span> 名英雄`;
renderChampGrid(list);
}
function renderChampGrid(list) {
const grid = document.getElementById('champGrid');
if (!grid) return;
if (list.length === 0) {
grid.innerHTML = '<div class="placeholder-page" style="height:200px"><p>没有找到英雄</p></div>';
return;
}
grid.innerHTML = list.map(c => {
const cid = c.id || c.heroId;
// 有缓存直接写 src,避免切换职业时破图
const cached = Cache.get('tile_' + cid);
const imgAttr = cached
? `src="${esc(API.img(cached))}"`
: `data-champid="${cid}"`;
return `
<div class="champ-card" onclick="openChampSkins(${cid},'${esc(c.name)}','${esc(c.alias||'')}')">
<div class="champ-img-wrap">
<img ${imgAttr} alt="${esc(c.name)}"
onerror="this.onerror=null;this.style.background='#1a2d4a'">
</div>
<div class="champ-name">${esc(c.name)}</div>
</div>`;
}).join('');
// 只对还没加载的发请求
const pending = Array.from(grid.querySelectorAll('img[data-champid]'));
if (pending.length > 0) loadAllChampIcons(grid);
}
// 分批并发加载所有英雄头像
function loadAllChampIcons(grid) {
const imgs = Array.from(grid.querySelectorAll('img[data-champid]'));
const BATCH = 8;
let i = 0;
function next() {
const batch = imgs.slice(i, i + BATCH);
if (!batch.length) return;
i += BATCH;
batch.forEach(el => loadOneIcon(el, el.dataset.champid));
if (i < imgs.length) setTimeout(next, 80);
}
next();
}
async function loadOneIcon(el, id) {
try {
let tp = Cache.get('tile_' + id);
if (!tp) {
// 优先从已缓存的 hero json 取
const hero = Cache.get('hero_' + id);
if (hero && hero.skins && hero.skins[0]) {
tp = hero.skins[0].tilePath;
} else {
const data = await API.get('heroicon', { id });
if (data && data.skins && data.skins[0]) {
tp = data.skins[0].tilePath;
// 顺便把 hero json 也缓存了,打开皮肤时不用再请求
Cache.set('hero_' + id, data);
}
}
if (tp) Cache.set('tile_' + id, tp);
}
if (tp && el.isConnected) {
el.src = API.img(tp);
el.style.minHeight = '';
el.style.background = '';
}
} catch(e) { /* 静默失败 */ }
}
// ── 皮肤详情 ─────────────────────────────────
let _skins = [], _curChampId = 0, _curChampName = '', _curChampAlias = '';
let _activeSkinIdx = 0, _activeChromaIdx = -1, _thumbOffset = 0;
async function openChampSkins(champId, champName, champAlias) {
const page = document.getElementById('page-skins');
page.innerHTML = '<div class="loading-spin">加载皮肤</div>';
// 先确定 alias:优先用传入的,其次从缓存的 hero 里取
let hero = Cache.get('hero_' + champId);
const alias = champAlias || (hero && hero.alias) || '';
// 并发:英雄皮肤数据 + xc炫彩数据(直接读本地 xc/xc.txt,不依赖后端新接口)
const [heroResult, xcResult] = await Promise.allSettled([
hero ? Promise.resolve(hero) : API.get('hero', { id: champId }),
_loadXcData(alias),
]);
if (heroResult.status === 'rejected') {
page.innerHTML = '<div class="placeholder-page"><p>加载皮肤失败</p></div>';
return;
}
hero = heroResult.value;
Cache.set('hero_' + champId, hero);
// alias 再补一次 hero.alias(应对自动跳转时 champAlias 为空的情况)
const finalAlias = alias || hero.alias || '';
const xcData = xcResult.status === 'fulfilled' ? xcResult.value : [];
// 若首次 alias 为空导致 xcData 为空,用 hero.alias 补一次
const xcDataFinal = (xcData.length === 0 && finalAlias && finalAlias !== alias)
? await _loadXcData(finalAlias)
: xcData;
_skins = buildSkinList(hero, finalAlias, xcDataFinal);
_curChampId = champId;
_curChampName = champName;
_curChampAlias = finalAlias;
_activeSkinIdx = 0;
_activeChromaIdx = -1;
_thumbOffset = 0;
renderSkinDetail(page);
}
// 从内置 XC_DATA 读取指定英雄的炫彩条目(不依赖任何网络/文件接口)
// 从 XC_DATA 读取指定英雄的炫彩数据
// 返回 Map<skinIdBase, [{file, name, color}, ...]>
// skinIdBase = 皮肤ID末尾3位,精确对应 xcdata.js 里的 skinId
function _loadXcData(alias) {
if (!alias || typeof XC_DATA === 'undefined') return Promise.resolve(new Map());
const key = Object.keys(XC_DATA).find(k => k.toLowerCase() === alias.toLowerCase());
if (!key) return Promise.resolve(new Map());
const list = XC_DATA[key]; // 数组格式 [{skinId, chromas:[]}]
const result = new Map();
if (Array.isArray(list)) {
// 新格式:[{ skinId, chromas: [{file, name, color}] }]
list.forEach(entry => {
if (entry.skinId != null && Array.isArray(entry.chromas)) {
result.set(entry.skinId, entry.chromas);
}
});
}
return Promise.resolve(result);
}
function buildSkinList(hero, alias, xcMap) {
const skins = hero.skins || [];
const result = [];
const hallIdx = [];
skins.forEach((s, i) => {
// LCU 原生炫彩
const chromas = (s.chromas || []).map(ch => ({
id: ch.id,
name: ch.name || '',
chromaPath: ch.chromaPath || '',
color: (ch.colors && ch.colors[0]) || '#999999',
isXc: false,
}));
// 注入 xc 本地炫彩:用皮肤ID末尾3位精确匹配 xcMap 的 key
// 默认皮肤(isBase 或 id末尾=0)跳过
const base = s.id % 1000;
if (chromas.length === 0 && xcMap.size > 0 && !s.isBase && base !== 0) {
const xcChromas = xcMap.get(base);
if (xcChromas && xcChromas.length > 0) {
xcChromas.forEach((xc, idx) => {
// 从文件名提取炫彩ID,如 Viego994.png → 994
const xcId = parseInt((xc.file || '').replace(/[^0-9]/g, '')) || 0;
chromas.push({
id: xcId,
name: xc.name || `炫彩${idx + 1}`,
chromaPath: '',
xcFile: xc.file || '',
color: xc.color || '#999999',
isXc: true,
});
});
}
}
result.push({
id: s.id,
name: s.name || '',
splashPath: s.splashPath || '',
tilePath: s.tilePath || '',
description: s.description || '',
shortBio: hero.shortBio || '',
isBase: s.isBase || false,
hasChroma: chromas.length > 0,
chromas,
});
if ((s.name || '').includes('殿堂传奇')) hallIdx.push(i);
});
hallIdx.forEach(i => {
const qi = skins[i];
const tiers = qi?.questSkinInfo?.tiers;
if (!tiers || tiers.length < 2) return;
const t = tiers[1];
const immortalName = t.name || `联盟不朽 ${hero.name || _curChampName || ''}`.trim();
// 联盟不朽皮肤的 xc 炫彩注入
const immortalChromas = [];
if (xcMap.size > 0 && t.id) {
const base = t.id % 1000;
const xcChromas = xcMap.get(base);
if (xcChromas && xcChromas.length > 0) {
xcChromas.forEach((xc, idx) => {
const xcId = parseInt((xc.file || '').replace(/[^0-9]/g, '')) || 0;
immortalChromas.push({
id: xcId,
name: xc.name || `炫彩${idx + 1}`,
chromaPath: '',
xcFile: xc.file || '',
color: xc.color || '#999999',
isXc: true,
});
});
}
}
result.push({
id: t.id || qi.id,
name: immortalName,
splashPath: t.splashPath || '',
tilePath: t.tilePath || '',
description: t.description || '',
shortBio: hero.shortBio || '',
isBase: false,
hasChroma: immortalChromas.length > 0,
chromas: immortalChromas,
});
});
return result;
}
function renderSkinDetail(page) {
const tileCached = Cache.get('tile_' + _curChampId);
const iconSrc = tileCached ? API.img(tileCached)
: (_skins[0] && _skins[0].tilePath ? API.img(_skins[0].tilePath) : '');
page.innerHTML = `
<div class="sp-info-bar">
<div class="sp-info-left">
<img class="sp-info-avatar" src="${esc(iconSrc)}" alt="${esc(_curChampName)}" onerror="this.onerror=null;this.style.background='#e8eaed'">
<span class="sp-info-name">${esc(_curChampName)}</span>
<span class="sp-info-sub">共 ${_skins.length} 个皮肤</span>
</div>
<div class="sp-info-right">
<div class="sp-back-btn" onclick="renderSkinLib()">
<svg viewBox="0 0 24 24"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg>返回
</div>
</div>
</div>
<div class="sp-canvas" id="spCanvas">
<img class="sp-bg-img" id="spBgImg" src="" alt="">
<div style="position:absolute;bottom:0;left:0;right:0;z-index:8;
background:linear-gradient(to top,rgba(0,0,0,0.92) 0%,rgba(0,0,0,0.5) 55%,transparent 100%);
padding:80px 22px 18px;">
<div style="font-size:24px;font-weight:700;color:#fff;text-shadow:0 2px 8px rgba(0,0,0,0.7);margin-bottom:6px;" id="spSkinName"></div>
<div style="font-size:12px;color:rgba(255,255,255,0.75);line-height:1.7;max-width:60%;word-break:break-all;" id="spSkinDesc"></div>
<div style="display:flex;gap:10px;margin-top:14px;">
<button onclick="equipCurrentSkin()" class="sp-btn-apply">应用皮肤</button>
<button onclick="setWallpaper()" class="sp-btn-career">设置为生涯壁纸</button>
</div>
</div>
</div>
<!-- 炫彩面板:fixed定位,JS计算坐标 -->
<div id="spChromaWrap" style="display:none;position:fixed;z-index:9999;flex-direction:column;align-items:flex-end;gap:6px;">
<div id="spChromaRow" style="display:flex;gap:6px;background:rgba(0,0,0,0.5);padding:6px 10px;border-radius:20px;backdrop-filter:blur(6px);"></div>
<div id="spChromaCard" style="display:none;width:242px;background:rgba(255,255,255,0.08);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid rgba(255,255,255,0.15);border-radius:14px;overflow:hidden;box-shadow:0 8px 32px rgba(0,0,0,0.3);">
<img id="spChromaCardImg" style="width:242px;height:242px;object-fit:cover;display:block;" src="" alt="">
<div id="spChromaCardName" style="font-size:12px;color:rgba(255,255,255,0.9);padding:7px 10px;text-align:center;background:rgba(0,0,0,0.25);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;height:31px;line-height:17px;"></div>
</div>
</div>
<div class="sp-skin-grid-wrap">
<div class="sp-skin-grid-inner">
<div class="sp-skin-grid" id="spSkinGrid"></div>
</div>
</div>
<div style="min-height:14px;flex-shrink:0;width:100%;"></div>`;
updateSplash();
renderSkinGrid();
}
function updateSplash() {
const skin = _skins[_activeSkinIdx];
if (!skin) return;
const bg = document.getElementById('spBgImg');
if (bg && skin.splashPath) {
bg.style.opacity = '0';
const newSrc = API.img(skin.splashPath);
setTimeout(() => {
bg.src = newSrc;
bg.onload = () => { bg.style.opacity = '1'; };
bg.onerror = () => { bg.style.opacity = '1'; };
}, 200);
}
const nameEl = document.getElementById('spSkinName');
const descEl = document.getElementById('spSkinDesc');
if (nameEl) nameEl.textContent = skin.name;
if (descEl) descEl.textContent = skin.description || skin.shortBio || '';
const chromaWrap = document.getElementById('spChromaWrap');
const chromaRow = document.getElementById('spChromaRow');
if (!chromaWrap || !chromaRow) return;
if (skin.hasChroma && skin.chromas.length > 0) {
const wrap = document.getElementById('spChromaWrap');
const row = document.getElementById('spChromaRow');
const canvas = document.getElementById('spCanvas');
// fixed 定位到 canvas 右上角
const rect = canvas.getBoundingClientRect();
wrap.style.top = (rect.top + 12) + 'px';
wrap.style.right = (window.innerWidth - rect.right + 12) + 'px';
// 先重置到右边屏幕外,再 display:flex
wrap.style.transform = 'translateX(60px)';
wrap.style.opacity = '0';
wrap.style.transition = 'none';
wrap.style.display = 'flex';
row.innerHTML = skin.chromas.map((ch, ci) => {
// xc 炫彩用 color 字段显示圆点颜色,LCU 原生炫彩也用 color
const dotStyle = ch.isXc
? `background:${esc(ch.color)};border-color:rgba(255,255,255,0.5);`
: `background:${esc(ch.color)};`;
return `
<span onclick="selectChroma(${_activeSkinIdx},${ci})"
onmouseenter="hoverChroma(${_activeSkinIdx},${ci})"
onmouseleave="leaveChroma(${ci})"
title="${esc(ch.name)}"
style="width:20px;height:20px;border-radius:50%;${dotStyle}
cursor:pointer;border:2px solid rgba(255,255,255,0.3);
display:inline-block;flex-shrink:0;
box-shadow:0 1px 4px rgba(0,0,0,0.5);">
</span>`;
}).join('');
// 预填炫彩内容
const cardImg = document.getElementById('spChromaCardImg');
const cardName = document.getElementById('spChromaCardName');
const card = document.getElementById('spChromaCard');
const firstCh = skin.chromas[0];
if (cardImg && firstCh) {
cardImg.src = firstCh.isXc && firstCh.xcFile
? `xc/${encodeURIComponent(firstCh.xcFile)}`
: (firstCh.chromaPath ? API.img(firstCh.chromaPath) : '');
}
if (cardName) cardName.textContent = firstCh?.name || '';
if (card) card.style.display = 'none';
_activeChromaIdx = -1;
// 两帧后触发从右边弹入动画
requestAnimationFrame(() => requestAnimationFrame(() => {
wrap.style.transition = 'transform 0.45s cubic-bezier(.34,1.56,.64,1), opacity 0.3s ease';
wrap.style.transform = 'translateX(0)';
wrap.style.opacity = '1';
}));
} else {
const wrap = document.getElementById('spChromaWrap');
// 向右滑出
wrap.style.transition = 'transform 0.25s ease, opacity 0.2s ease';
wrap.style.transform = 'translateX(60px)';
wrap.style.opacity = '0';
setTimeout(() => { wrap.style.display = 'none'; }, 260);
_activeChromaIdx = -1;
}
}
// showChromaCard、chromaWrapEnter、chromaWrapLeave 已废弃,炫彩改用hover事件直接处理
function hoverChroma(skinIdx, ci) {
const ch = _skins[skinIdx]?.chromas?.[ci];
if (!ch) return;
// 圆点放大高亮(只是视觉,不改变 _activeChromaIdx)
const dots = document.querySelectorAll('#spChromaRow span');
dots.forEach((el, i) => {
el.style.transform = i === ci ? 'scale(1.35)' : 'scale(1)';
el.style.borderColor = i === ci ? '#fff' : 'rgba(255,255,255,0.3)';
el.style.transition = 'transform .15s, border-color .15s';
});
// 更新卡片内容
const card = document.getElementById('spChromaCard');
const img = document.getElementById('spChromaCardImg');
const name = document.getElementById('spChromaCardName');
if (img) {
img.src = ch.isXc && ch.xcFile
? `xc/${encodeURIComponent(ch.xcFile)}`
: (ch.chromaPath ? API.img(ch.chromaPath) : '');
}
if (name) name.textContent = ch.name || '';
// 显示卡片
if (card && card.style.display === 'none') {
card.style.transition = 'none';
card.style.transform = 'translateY(-10px) scale(0.88)';
card.style.opacity = '0';
card.style.display = 'block';
requestAnimationFrame(() => requestAnimationFrame(() => {
card.style.transition = 'transform 0.32s cubic-bezier(.34,1.56,.64,1), opacity 0.22s ease';
card.style.transform = 'translateY(0) scale(1)';
card.style.opacity = '1';
}));
} else if (card) {
card.style.transform = 'translateY(0) scale(1)';
card.style.opacity = '1';
}
}
function leaveChroma(ci) {
// 还原圆点样式
const dots = document.querySelectorAll('#spChromaRow span');
dots.forEach((el, i) => {
// 如果有选中的炫彩,保持选中态高亮
const isActive = _activeChromaIdx >= 0 && i === _activeChromaIdx;
el.style.transform = isActive ? 'scale(1.2)' : 'scale(1)';
el.style.borderColor = isActive ? '#fff' : 'rgba(255,255,255,0.3)';
});
// 如果没有选中任何炫彩,隐藏卡片
if (_activeChromaIdx < 0) {
const card = document.getElementById('spChromaCard');
if (card) {
card.style.transition = 'transform 0.2s ease, opacity 0.18s ease';
card.style.transform = 'translateY(-8px) scale(0.92)';
card.style.opacity = '0';
setTimeout(() => { if (card.style.opacity === '0') card.style.display = 'none'; }, 200);
}
}
}
function selectChroma(skinIdx, ci) {
const ch = _skins[skinIdx]?.chromas?.[ci];
if (!ch) return;
// 点击同一个已选中的 → 取消选中
if (_activeChromaIdx === ci) {
_activeChromaIdx = -1;
const dots = document.querySelectorAll('#spChromaRow span');
dots.forEach(el => { el.style.borderColor = 'rgba(255,255,255,0.3)'; el.style.transform = 'scale(1)'; });
return;
}
// 选中新的炫彩
_activeChromaIdx = ci;
hoverChroma(skinIdx, ci);
}
// renderThumbs、scrollThumbToActive、slideThumb 已废弃,皮肤详情改用网格布局
// 鼠标拖拽滑动
function initThumbDrag(track) {
let startX = 0, startOffset = 0, dragging = false;
track.addEventListener('mousedown', e => {
dragging = true;
startX = e.clientX;
startOffset = _thumbOffset;
track.classList.add('dragging');
e.preventDefault();
});
// 绑定在 track 容器上而非 document,避免全局泄漏
const onMove = e => {
if (!dragging) return;
const scroll = document.getElementById('spThumbScroll');
const maxOffset = Math.max(0, track.scrollWidth - (scroll?.offsetWidth || 0));
_thumbOffset = Math.max(0, Math.min(startOffset - (e.clientX - startX), maxOffset));
track.style.transform = `translateX(-${_thumbOffset}px)`;
};
const onUp = () => {
if (dragging) { dragging = false; track.classList.remove('dragging'); }
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
// 皮肤详情页销毁时自动解绑
track._cleanDrag = () => {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
};
}
function scrollThumbToActive() {} // 已废弃
function slideThumb() {} // 已废弃
function selectSkin(idx) {
_activeSkinIdx = idx; _activeChromaIdx = -1;
updateSplash();
document.querySelectorAll('#spSkinGrid .sp-thumb').forEach((el, i) =>
el.classList.toggle('active', i === idx));
const active = document.querySelector('#spSkinGrid .sp-thumb.active');
if (active) active.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
function prevSkin() { if (_skins.length) selectSkin((_activeSkinIdx - 1 + _skins.length) % _skins.length); }
function nextSkin() { if (_skins.length) selectSkin((_activeSkinIdx + 1) % _skins.length); }
async function equipCurrentSkin() {
const skin = _skins[_activeSkinIdx];
if (!skin) return;
if (skin.isBase || _activeSkinIdx === 0) {
showToast(`应用:"${skin.name}" 默认皮肤失败!`, 'err');
return;
}
let applyId = skin.id;
let applyName = skin.name;
if (_activeChromaIdx >= 0 && skin.chromas[_activeChromaIdx]) {
const ch = skin.chromas[_activeChromaIdx];
applyName = ch.name || skin.name;
// xc 炫彩和 LCU 原生炫彩都直接用 ch.id 传给后端
// xc 炫彩的 ch.id 就是文件名数字(如 Viego994.png → 994)
applyId = ch.id;
}
_chromaLocked = false;
const card = document.getElementById('spChromaCard');
if (card) card.style.display = 'none';
showLoadingToast('正在应用皮肤...');
try {
await API.get('applySkin', { skinId: applyId, heroId: _curChampId, alias: _curChampAlias });
hideLoadingToast();
showToast(`已应用皮肤:${applyName}`, 'ok');
} catch(e) {
hideLoadingToast();
showToast('请在英雄选择阶段使用此功能', 'err');
}
}
async function setWallpaper() {
const skin = _skins[_activeSkinIdx];
if (!skin) return;
try {
await API.get('setCareerBg', { skinId: skin.id, heroId: _curChampId, alias: _curChampAlias });
showToast('✓ 生涯壁纸已更新', 'ok');
} catch(e) { showToast('设置壁纸失败', 'err'); }
}
// chromaWrapEnter/Leave 已废弃
let _chromaLocked = false;
// ── 皮肤图标网格(替代横向滑动条)────────────
function renderSkinGrid() {
const grid = document.getElementById('spSkinGrid');
if (!grid) return;
grid.innerHTML = _skins.map((skin, idx) => {
let corner = '';
if (skin.hasChroma && skin.chromas.length > 0)
corner = `<div class="sp-thumb-corner"><span>${skin.chromas.length}</span></div>`;
else if (skin.name.includes('联盟不朽'))
corner = `<div class="sp-thumb-quest"><span>不朽</span></div>`;
return `
<div class="sp-thumb ${idx === _activeSkinIdx ? 'active' : ''}"
data-idx="${idx}" onclick="selectSkin(${idx})">
<div class="sp-thumb-img-wrap">
<img data-src="${esc(skin.tilePath ? API.img(skin.tilePath) : '')}"
alt="${esc(skin.name)}"
onerror="this.onerror=null;this.style.background='#e8eaed'">
${corner}
</div>
<div class="sp-thumb-name">${esc(skin.name)}</div>
</div>`;
}).join('');
observeLazy(grid);
}