676 lines
25 KiB
JavaScript
676 lines
25 KiB
JavaScript
/* =============================================
|
||
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);
|
||
}
|