/* ============================================= 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 = '

请先启动 LOL 客户端

'; return; } page.innerHTML = `
${ROLES.map(r => `
${r.icon ?? ''}${r.label}
`).join('')}
加载英雄
`; 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 = '

加载失败

'; } } 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 = `共有 ${list.length} 名英雄`; renderChampGrid(list); } function renderChampGrid(list) { const grid = document.getElementById('champGrid'); if (!grid) return; if (list.length === 0) { grid.innerHTML = '

没有找到英雄

'; 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 `
${esc(c.name)}
${esc(c.name)}
`; }).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 = '
加载皮肤
'; // 先确定 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 = '

加载皮肤失败

'; 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 = 皮肤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 = `
${esc(_curChampName)} ${esc(_curChampName)} 共 ${_skins.length} 个皮肤
返回
`; 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 ` `; }).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 = `
${skin.chromas.length}
`; else if (skin.name.includes('联盟不朽')) corner = `
不朽
`; return `
${esc(skin.name)} ${corner}
${esc(skin.name)}
`; }).join(''); observeLazy(grid); }