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

728 lines
28 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.
/* =============================================
app.js — 核心:API / 路由 / Toast
后端各接口独立路由:/api/<name>?params
图片代理:/api/img?path=<lcu路径>
英雄头像:/api/heroicon?id=<champId>
============================================= */
window.APP = { summonerId: null, summonerName: null, puuid: null };
// ── API 封装 ──────────────────────────────────
const API = {
async get(path, params = {}) {
const q = Object.keys(params).length
? '?' + new URLSearchParams(params).toString() : '';
const r = await fetch('/api/' + path + q);
if (!r.ok) throw new Error(r.status);
return r.json();
},
img(lcuPath) {
if (!lcuPath) return '';
return '/api/img?path=' + encodeURIComponent(lcuPath);
},
};
// ── sessionStorage 缓存 ───────────────────────
const Cache = {
get(k) { try { return JSON.parse(sessionStorage.getItem(k)); } catch{ return null; } },
set(k, v) { try { sessionStorage.setItem(k, JSON.stringify(v)); } catch{} },
};
// ── Toast ─────────────────────────────────────
function showToast(msg, type = '') {
const container = document.getElementById('toast-container');
if (!container) return;
const icon = type === 'err' ? 'i' : '✓';
const el = document.createElement('div');
el.className = 'toast' + (type ? ' ' + type : '');
el.innerHTML = `
<div class="toast-icon">${icon}</div>
<div class="toast-body">${msg}</div>
<div class="toast-bar"></div>`;
container.appendChild(el);
setTimeout(() => {
el.classList.add('leaving');
setTimeout(() => { el.remove(); }, 300);
}, 2600);
}
// ── 加载中 Toast(旋转动画,手动关闭)────────
let _loadingToastEl = null;
function showLoadingToast(msg) {
const container = document.getElementById('toast-container');
if (!container) return;
if (_loadingToastEl) _loadingToastEl.remove();
const el = document.createElement('div');
el.className = 'toast toast-loading';
el.innerHTML = `<div class="toast-spinner"></div><div class="toast-body">${msg}</div>`;
container.appendChild(el);
_loadingToastEl = el;
}
function hideLoadingToast() {
if (_loadingToastEl) {
_loadingToastEl.classList.add('leaving');
setTimeout(() => { if(_loadingToastEl){_loadingToastEl.remove(); _loadingToastEl = null;} }, 300);
}
}
// ── HTML 转义 ─────────────────────────────────
function esc(s) {
return String(s ?? '').replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
// ── 页面路由 ──────────────────────────────────
function navigate(page) {
document.querySelectorAll('.page').forEach(p => {
p.classList.remove('active');
// 离开皮肤库时清空内容,避免布局残留
if (p.id === 'page-skins' && page !== 'skins') {
p.innerHTML = '';
}
// 离开美化库时清空内容
if (p.id === 'page-beauty' && page !== 'beauty') {
p.innerHTML = '';
}
});
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
document.getElementById('page-' + page).classList.add('active');
document.querySelector(`.nav-item[data-page="${page}"]`).classList.add('active');
if (page === 'skins') renderSkinLib();
if (page === 'beauty') renderBeauty();
if (page === 'tools') renderTools();
if (page === 'settings') renderSettings();
}
function renderTools() {
// 由 tools.js 实现
}
// ── 美化库 ────────────────────────────────────
const BEAUTY_CATS = [
{ key: 'xt', label: '人物血条', icon: `<svg viewBox="0 0 24 24" width="18" height="18"><path fill="#ff4d4f" d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>`, color: '#ff4d4f', bg: '#fff1f0', mod: 'XongMao_xt' },
{ key: 'zz', label: '鼠标指针', icon: `<svg viewBox="0 0 24 24" width="18" height="18"><path fill="#1677ff" d="M4 0l16 12.279-6.951 1.17 4.325 8.817-3.596 1.734-4.35-8.879-5.428 4.702z"/></svg>`, color: '#1677ff', bg: '#e6f4ff', mod: 'XongMao_zz' },
{ key: 'jm', label: '操作界面', icon: `<svg viewBox="0 0 24 24" width="18" height="18"><path fill="#722ed1" d="M20 3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H4V5h16v14zM6 7h2v2H6zm0 4h2v2H6zm0 4h2v2H6zm4-8h8v2h-8zm0 4h8v2h-8zm0 4h8v2h-8z"/></svg>`, color: '#722ed1', bg: '#f9f0ff', mod: 'XongMao_jm' },
{ key: 'yy', label: '人物语音', icon: `<svg viewBox="0 0 24 24" width="18" height="18"><path fill="#fa8c16" d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg>`, color: '#fa8c16', bg: '#fff7e6', mod: 'XongMao_yy' },
{ key: 'zt', label: '游戏字体', icon: `<svg viewBox="0 0 24 24" width="18" height="18"><path fill="#13c2c2" d="M9 4v3h5v12h3V7h5V4H9zm-6 8h3v7h3v-7h3V9H3v3z"/></svg>`, color: '#13c2c2', bg: '#e6fffb', mod: 'XongMao_zt' },
{ key: 'dt', label: '游戏地图', icon: `<svg viewBox="0 0 24 24" width="18" height="18"><path fill="#52c41a" d="M20.5 3l-.16.03L15 5.1 9 3 3.36 4.9c-.21.07-.36.25-.36.48V20.5c0 .28.22.5.5.5l.16-.03L9 18.9l6 2.1 5.64-1.9c.21-.07.36-.25.36-.48V3.5c0-.28-.22-.5-.5-.5zM15 19l-6-2.11V5l6 2.11V19z"/></svg>`, color: '#52c41a', bg: '#f6ffed', mod: 'XongMao_dt' },
];
// 每个分类资源,name为显示昵称,img为图片文件名
const BEAUTY_ITEMS = {
dt: [
{ id:'dt_1', name:'夜晚地图', img:'dt_1.jpg' },
{ id:'dt_2', name:'白天地图', img:'dt_2.jpg' },
{ id:'dt_3', name:'沙滩地图', img:'dt_3.jpg' },
{ id:'dt_4', name:'冬季地图', img:'dt_4.jpg' },
{ id:'dt_5', name:'诅咒地图', img:'dt_5.jpg' },
],
jm: [
{ id:'jm_1', name:'最强王者', img:'jm_1.jpg' },
{ id:'jm_2', name:'少女人体', img:'jm_2.jpg' },
{ id:'jm_3', name:'猫娘乐园', img:'jm_3.jpg' },
{ id:'jm_4', name:'洛菲女孩', img:'jm_4.jpg' },
{ id:'jm_5', name:'雷姆UI', img:'jm_5.jpg' },
{ id:'jm_6', name:'雷电将军', img:'jm_6.jpg' },
{ id:'jm_7', name:'科技崩坏', img:'jm_7.jpg' },
{ id:'jm_8', name:'古拉小鲨鱼', img:'jm_8.jpg' },
{ id:'jm_9', name:'博雷阿苏', img:'jm_9.jpg' },
{ id:'jm_10', name:'新年春节', img:'jm_10.jpg' },
],
xt: [
{ id:'xt_1', name:'可爱粉色血条', img:'xt_1.jpg' },
{ id:'xt_2', name:'冰蓝条纹血条', img:'xt_2.jpg' },
{ id:'xt_3', name:'黄色条纹血条', img:'xt_3.jpg' },
{ id:'xt_4', name:'蓝色条纹血条', img:'xt_4.jpg' },
{ id:'xt_5', name:'绿色条纹血条', img:'xt_5.jpg' },
],
yy: [
{ id:'yy_1', name:'俄美 语音', img:'yy_1.jpg' },
{ id:'yy_2', name:'韩国 语音', img:'yy_2.jpg' },
{ id:'yy_3', name:'美国 语音', img:'yy_3.jpg' },
{ id:'yy_4', name:'日本 语音', img:'yy_4.jpg' },
],
zt: [
{ id:'zt_1', name:'方正粗圆简体', img:'zt_1.jpg' },
{ id:'zt_2', name:'方正剪纸简体', img:'zt_2.jpg' },
{ id:'zt_3', name:'方正卡通简体', img:'zt_3.jpg' },
{ id:'zt_4', name:'方正瞄呜体', img:'zt_4.jpg' },
{ id:'zt_5', name:'方正稚艺字体', img:'zt_5.jpg' },
{ id:'zt_6', name:'华康少女文字体', img:'zt_6.jpg' },
],
zz: [
{ id:'zz_1', name:'彩色指针', img:'zz_1.jpg' },
{ id:'zz_2', name:'奥特曼', img:'zz_2.jpg' },
{ id:'zz_3', name:'哆啦A梦', img:'zz_3.jpg' },
{ id:'zz_4', name:'多啦美', img:'zz_4.jpg' },
{ id:'zz_5', name:'小呆龙', img:'zz_5.jpg' },
{ id:'zz_6', name:'刀光剑神', img:'zz_6.jpg' },
{ id:'zz_7', name:'彩色星星', img:'zz_7.jpg' },
{ id:'zz_8', name:'红色手指', img:'zz_8.jpg' },
],
};
let _beautyCurCat = 'xt';
let _beautyResReady = false; // 图片资源是否已下载
async function renderBeauty() {
const page = document.getElementById('page-beauty');
// 先检查资源是否存在
if (!_beautyResReady) {
try {
const res = await API.get('checkBeautyRes');
if (res.exists) {
_beautyResReady = true;
}
} catch(e) { /* 检查失败当作不存在 */ }
}
// 资源不存在:显示下载提示页
if (!_beautyResReady) {
page.innerHTML = `
<div class="beauty-wrap" style="align-items:center;justify-content:center;">
<div style="text-align:center;padding:40px 20px;">
<div style="font-size:48px;margin-bottom:16px;">📦</div>
<div style="font-size:16px;font-weight:600;color:var(--text);margin-bottom:8px;">美化资源未下载</div>
<div style="font-size:13px;color:var(--text-muted);margin-bottom:24px;">需要先下载美化图片资源包才能使用</div>
<button onclick="downloadBeautyRes()" style="
height:44px;padding:0 32px;
background:linear-gradient(135deg,#1677ff,#0bc4e3);
color:#fff;border:none;border-radius:10px;
font-size:15px;font-weight:700;cursor:pointer;
box-shadow:0 4px 16px rgba(22,119,255,0.4);">
下载美化资源包
</button>
<div id="beautyResProgress" style="margin-top:16px;font-size:13px;color:var(--text-muted);"></div>
</div>
</div>`;
return;
}
// 资源存在:正常显示
page.innerHTML = `
<div class="beauty-wrap">
<!-- 顶部分类Tab -->
<div class="beauty-tabs">
<div style="display:flex;gap:10px;flex:1;flex-wrap:nowrap;">
${BEAUTY_CATS.map(c => `
<div class="beauty-tab ${c.key === _beautyCurCat ? 'active' : ''}"
data-cat="${c.key}"
style="${c.key === _beautyCurCat ? `background:#fff;border-color:${c.color};color:${c.color};` : ''}"
onclick="switchBeautyCat('${c.key}')">
<span style="display:inline-flex;align-items:center;line-height:1;">${c.icon}</span>
<span>${c.label}</span>
</div>`).join('')}
</div>
<div class="beauty-total">共有 <span class="beauty-total-num" id="beautyTotalNum">${BEAUTY_ITEMS[_beautyCurCat]?.length ?? 0}</span> 个资源</div>
</div>
<!-- 图片网格 -->
<div class="beauty-grid-wrap">
<div class="beauty-grid" id="beautyGrid"></div>
</div>
</div>`;
renderBeautyGrid(_beautyCurCat);
}
// 下载美化资源包
async function downloadBeautyRes() {
const btn = document.querySelector('[onclick="downloadBeautyRes()"]');
const tip = document.getElementById('beautyResProgress');
if (btn) btn.disabled = true;
if (tip) tip.textContent = '正在获取下载地址...';
try {
const res = await API.get('downloadBeautyRes');
if (!res.ok) {
if (tip) tip.textContent = '获取下载地址失败,请检查网络';
if (btn) btn.disabled = false;
return;
}
if (tip) tip.textContent = '开始下载...';
// 轮询下载进度
await pollBeautyResProgress();
// 下载完成,重新渲染
_beautyResReady = true;
renderBeauty();
} catch(e) {
if (tip) tip.textContent = '下载失败,请重试';
if (btn) btn.disabled = false;
}
}
// 轮询资源包下载进度
async function pollBeautyResProgress() {
return new Promise((resolve) => {
const timer = setInterval(async () => {
try {
const p = await API.get('downloadProgress');
const status = parseInt(p.status ?? 0);
const tip = document.getElementById('beautyResProgress');
if (status === 3) {
clearInterval(timer);
if (tip) tip.textContent = '下载完成,正在解压...';
// 等待后端解压完成
await new Promise(r => setTimeout(r, 3000));
resolve();
} else if (status === 1 || status === 2) {
const pct = parseFloat(p.progress || 0).toFixed(0);
const spd = p.speed || '';
if (tip) tip.textContent = `正在下载 ${pct}% ${spd}`;
}
} catch(e) {
clearInterval(timer);
resolve();
}
}, 600);
});
}
function switchBeautyCat(key) {
_beautyCurCat = key;
document.querySelectorAll('.beauty-tab').forEach(el => {
const k = el.dataset.cat;
const c = BEAUTY_CATS_MAP.get(k);
if (k === key) {
el.classList.add('active');
el.style.background = c.bg;
el.style.borderColor = c.color;
el.style.color = c.color;
} else {
el.classList.remove('active');
el.style.background = el.style.borderColor = el.style.color = '';
}
});
renderBeautyGrid(key);
}
function renderBeautyGrid(key) {
const grid = document.getElementById('beautyGrid');
if (!grid) return;
const items = BEAUTY_ITEMS[key] || [];
const numEl = document.getElementById('beautyTotalNum');
if (numEl) numEl.textContent = items.length;
const isWide = key === 'jm';
grid.className = 'beauty-grid' + (isWide ? ' beauty-grid-wide' : '');
grid.innerHTML = items.map((item, i) => `
<div class="beauty-card"
data-cat="${key}" data-idx="${i+1}" data-name="${esc(item.name)}">
<div class="beauty-card-img-wrap ${isWide ? 'beauty-img-wide' : ''}">
<img src="/api/beautyImg?name=${item.img}" alt="${esc(item.name)}"
onerror="this.onerror=null;this.style.display='none';this.parentElement.classList.add('img-missing')">
<div class="beauty-img-missing-tip">📦 双击下载</div>
</div>
<div class="beauty-card-name">${esc(item.name)}</div>
</div>`).join('');
// 用 addEventListener 绑定双击,避免 ondblclick 内联失效
grid.querySelectorAll('.beauty-card').forEach(card => {
card.addEventListener('dblclick', () => {
downloadBeautyItem(card.dataset.cat, card.dataset.idx, card.dataset.name);
});
});
}
async function applyBeautyItem() {} // 已废弃 - 保留空函数避免调用报错
// ── 美化库分类 Map(O(1)查找)────────────────
const BEAUTY_CATS_MAP = new Map(BEAUTY_CATS.map(c => [c.key, c]));
// ── 美化圆形进度弹窗 ─────────────────────────
const RING_LEN = 452.39; // 2 * Math.PI * 72
function showBeautyProgress(imgSrc, label) {
const modal = document.getElementById('beauty-progress-modal');
const img = document.getElementById('beauty-progress-img');
const lbl = document.getElementById('beauty-progress-label');
const pct = document.getElementById('beauty-progress-pct');
const bar = document.getElementById('beauty-progress-bar');
const spd = document.getElementById('beauty-progress-speed');
if (!modal) return;
if (imgSrc) img.src = imgSrc;
lbl.textContent = label;
pct.textContent = '0%';
if (spd) spd.textContent = '';
bar.style.strokeDashoffset = RING_LEN;
modal.classList.remove('hidden');
}
function updateBeautyProgress(percent, label, speed) {
const lbl = document.getElementById('beauty-progress-label');
const pct = document.getElementById('beauty-progress-pct');
const bar = document.getElementById('beauty-progress-bar');
const spd = document.getElementById('beauty-progress-speed');
if (!bar) return;
const p = Math.min(Math.max(parseFloat(percent) || 0, 0), 100);
bar.style.strokeDashoffset = RING_LEN * (1 - p / 100);
pct.textContent = p.toFixed(0) + '%';
if (label) lbl.textContent = label;
if (spd && speed !== undefined) spd.textContent = speed;
}
function hideBeautyProgress() {
const modal = document.getElementById('beauty-progress-modal');
if (modal) modal.classList.add('hidden');
}
async function downloadBeautyItem(catKey, idx, name) {
idx = parseInt(idx);
const card = document.querySelector(`.beauty-card[data-idx="${idx}"]`);
const imgSrc = card?.querySelector('img')?.src || '';
showBeautyProgress(imgSrc, `正在处理:${name}`);
try {
const res = await API.get('downloadBeauty', { catKey, idx });
if (!res.ok) {
hideBeautyProgress();
showToast('操作失败', 'err');
return;
}
if (res.cached) {
// 先用Promise封装假进度动画,跑完再挂载
await new Promise(resolve => {
let p = 0;
function step() {
p += 3 + Math.random() * 5;
if (p >= 95) { p = 95; updateBeautyProgress(95, `正在处理:${name}`, ''); resolve(); return; }
updateBeautyProgress(p, `正在处理:${name}`, '');
setTimeout(step, 60);
}
step();
});
updateBeautyProgress(98, `正在挂载:${name}`, '');
try {
await API.get('applyUI', { catKey });
updateBeautyProgress(100, `已应用:${name}`, '');
await new Promise(r => setTimeout(r, 500));
hideBeautyProgress();
showToast(`✓ 已应用:${name}`, 'ok');
} catch(e) {
hideBeautyProgress();
showToast('挂载失败', 'err');
}
return;
}
await new Promise(r => setTimeout(r, 400));
await pollDownloadProgress(name, catKey);
} catch(e) {
hideBeautyProgress();
showToast('操作失败,请检查网络', 'err');
}
}
async function pollDownloadProgress(name, catKey) {
return new Promise((resolve) => {
let waitCount = 0;
let started = false;
let fakeTimer = null;
let fakePct = 0;
// 假进度动画:在真实进度来之前先跑动画
function startFakeProgress() {
fakePct = 0;
fakeTimer = setInterval(() => {
if (fakePct < 85) {
fakePct += Math.random() * 8 + 2;
if (fakePct > 85) fakePct = 85;
updateBeautyProgress(fakePct, `正在下载:${name}`, '');
}
}, 200);
}
function stopFakeProgress() {
if (fakeTimer) { clearInterval(fakeTimer); fakeTimer = null; }
}
const timer = setInterval(async () => {
try {
const p = await API.get('downloadProgress');
const status = parseInt(p.status ?? 0);
const pct = parseFloat(p.progress || 0); // 后端已经是0~100
const speed = p.speed || '';
if (status === 1 || status === 2) {
started = true; waitCount = 0;
stopFakeProgress();
// 后端已经返回0~100的百分比,不需要再×100
const realPct = parseFloat(p.progress || 0);
if (realPct > fakePct) {
updateBeautyProgress(realPct, `正在下载:${name}`, speed);
}
return;
}
if (status === 3) {
clearInterval(timer);
stopFakeProgress();
updateBeautyProgress(100, `正在挂载:${name}`, '');
try {
await API.get('applyUI', { catKey });
hideBeautyProgress();
showToast(`✓ 已应用:${name}`, 'ok');
} catch(e) {
hideBeautyProgress();
showToast('挂载失败', 'err');
}
resolve(); return;
}
// status=0 等待开始,启动假进度
if (!started) {
waitCount++;
if (waitCount === 1) startFakeProgress();
if (waitCount > 30) {
clearInterval(timer);
stopFakeProgress();
hideBeautyProgress();
showToast('下载超时,请重试', 'err');
resolve();
}
}
} catch(e) {
clearInterval(timer);
stopFakeProgress();
hideBeautyProgress();
showToast('获取进度失败', 'err');
resolve();
}
}, 500);
});
}
// 以下函数已废弃,保留空函数避免潜在调用报错
async function restoreBeauty() {}
async function clearAllBeauty() {}
async function applyUIOnly() {}
// ── 懒加载图片(IntersectionObserver)────────
let _lazyObserver = null;
function initLazyObserver() {
if (_lazyObserver) return;
_lazyObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (!entry.isIntersecting) return;
const img = entry.target;
_lazyObserver.unobserve(img);
// 普通 data-src 懒加载
if (img.dataset.src) {
img.src = img.dataset.src;
img.removeAttribute('data-src');
}
// 英雄头像异步加载
if (typeof img._loadIcon === 'function') {
const fn = img._loadIcon;
img._loadIcon = null;
fn(img);
}
});
}, { rootMargin: '400px' });
}
// 重置 observer(切换页面时调用)
function resetLazyObserver() {
if (_lazyObserver) { _lazyObserver.disconnect(); _lazyObserver = null; }
}
// 把懒加载图片注册到 Observer,并立刻触发已在视口内的图片
function observeLazy(container) {
initLazyObserver();
container.querySelectorAll('img[data-src]').forEach(img => _lazyObserver.observe(img));
requestAnimationFrame(() => {
container.querySelectorAll('img[data-src]').forEach(img => {
const rect = img.getBoundingClientRect();
const inView = rect.top < window.innerHeight + 400 && rect.bottom > -400;
if (inView) {
_lazyObserver.unobserve(img);
img.src = img.dataset.src;
img.removeAttribute('data-src');
}
});
});
}
// ── 初始化 ────────────────────────────────────
async function initApp() {
try {
const s = await API.get('summoner');
APP.summonerId = s.summonerId;
APP.summonerName = s.displayName || s.gameName || s.internalName || '--';
APP.puuid = s.puuid;
APP.iconId = s.profileIconId ?? 29;
APP.level = s.summonerLevel ?? '--';
const avatarEl = document.getElementById('sidebarAvatar');
const nameEl = document.getElementById('sidebarName');
const iconUrl = API.img(`/lol-game-data/assets/v1/profile-icons/${APP.iconId}.jpg`);
if (avatarEl) avatarEl.src = iconUrl;
if (nameEl) nameEl.textContent = APP.summonerName;
navigate('skins');
// LOL 客户端连接成功后,启动英雄选择自动跳转监听
ChampSelectWatcher.start();
} catch(e) {
// 未连接时停止监听,等待重连
ChampSelectWatcher.stop();
document.getElementById('page-skins').innerHTML =
'<div class="placeholder-page"><p>⏳ 正在等待 LOL 客户端启动...</p></div>';
setTimeout(initApp, 3000);
}
}
// initApp 由登录成功后的 enterMainApp() 调用,不自动执行
// initApp();
// ── 用户信息弹出卡片 ──────────────────────────
function toggleUserCard() {
const card = document.getElementById('userCard');
if (!card) return;
const isHidden = card.classList.contains('hidden');
if (isHidden) {
const avatarEl = document.getElementById('userCardAvatar');
const nameEl = document.getElementById('userCardName');
const levelEl = document.getElementById('userCardLevel');
const accountEl = document.getElementById('userCardAccount');
const expireEl = document.getElementById('userCardExpire');
if (avatarEl) avatarEl.src = API.img(`/lol-game-data/assets/v1/profile-icons/${APP.iconId || 29}.jpg`);
if (nameEl) nameEl.textContent = APP.summonerName || '--';
if (levelEl) levelEl.textContent = 'Lv ' + (APP.level || '--');
if (accountEl) accountEl.textContent = APP.authAccount || '--';
if (expireEl) expireEl.textContent = APP.authExpire || '--';
const trigger = document.getElementById('sidebarUser');
if (trigger) {
const rect = trigger.getBoundingClientRect();
card.style.left = rect.left + 'px';
card.style.width = rect.width + 'px';
card.style.bottom = (window.innerHeight - rect.top + 8) + 'px';
card.style.top = 'auto';
}
card.classList.remove('hidden');
setTimeout(() => document.addEventListener('click', closeUserCard, { once: true }), 0);
} else {
card.classList.add('hidden');
}
}
function closeUserCard() {
document.getElementById('userCard')?.classList.add('hidden');
}
// ── 主题切换 ──────────────────────────────────
function toggleTheme() {
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
const newTheme = isDark ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
const btn = document.getElementById('themeToggleBtn');
if (btn) btn.textContent = newTheme === 'dark' ? '🌙' : '☀️';
}
// ── 英雄选择自动跳转 ──────────────────────────
// 易语言后端定时轮询 LCU,检测到英雄变化后通过
// WebMessage 推送 {"命令":"跳转皮肤","英雄ID":xxx}
// 前端只监听消息,不做任何 HTTP 轮询
// ─────────────────────────────────────────────
const ChampSelectWatcher = (function () {
let _lastChampId = 0;
let _active = false;
/* 收到后端 WebMessage 推送后处理跳转 */
async function _onMsg(data) {
if (!_active) return;
const cid = parseInt(data['英雄ID'] || 0);
// 0 = 英雄选择结束,重置
if (cid === 0) { _lastChampId = 0; return; }
// 同一英雄不重复跳转(防止后端反复推送同一ID)
if (cid === _lastChampId) return;
_lastChampId = cid;
// 确保切换到皮肤库页
const skinsPage = document.getElementById('page-skins');
if (!skinsPage) return;
if (!skinsPage.classList.contains('active')) {
navigate('skins');
// 等待 navigate 执行完(包括 renderSkinLib)
await new Promise(r => setTimeout(r, 100));
}
// 从本地缓存找英雄名
let champName = '', champAlias = '';
const champs = Cache.get('champions_lcu') || [];
const found = champs.find(c => parseInt(c.id || c.heroId) === cid);
if (found) { champName = found.name || ''; champAlias = found.alias || ''; }
await openChampSkins(cid, champName, champAlias);
}
/* 开启监听 */
function start() {
if (_active) return;
_active = true;
// 直接监听 WebView2 原始消息事件(兼容后端推送)
if (window.chrome && window.chrome.webview) {
window.chrome.webview.addEventListener('message', _rawHandler);
}
// 同时通过 Bridge 监听(双保险)
if (window.Bridge) {
Bridge.on('跳转皮肤', _onMsg);
}
// 前端自己轮询 session 接口(后端线程 LCU 可能有连接问题)
_startFrontendPoller();
}
/* 前端轮询:直接请求 LCU session,自己解析 championPickIntent */
let _pollerTimer = null;
let _pollerLastId = 0;
function _startFrontendPoller() {
if (_pollerTimer) return;
_pollerTimer = setInterval(async () => {
if (!_active) return;
try {
const r = await fetch('/api/champsession');
if (!r.ok) { _pollerLastId = 0; return; }
const session = await r.json();
// 从 myTeam 里找 championPickIntent(悬停)或 championId(锁定)
let cid = 0;
const myTeam = session.myTeam || [];
for (const p of myTeam) {
if (p.championPickIntent && p.championPickIntent > 0) {
cid = p.championPickIntent; break;
}
if (p.championId && p.championId > 0) {
cid = p.championId; break;
}
}
console.log('[Poller] myTeam长度:', myTeam.length, '找到cid:', cid);
if (cid > 0 && cid !== _pollerLastId) {
_pollerLastId = cid;
_onMsg({ '命令': '跳转皮肤', '英雄ID': cid });
} else if (cid === 0) {
_pollerLastId = 0;
}
} catch(e) { /* 静默失败 */ }
}, 300);
}
/* 原始消息处理(直接挂在 webview 上,不经过 Bridge 过滤)*/
function _rawHandler(e) {
try {
const raw = e.data;
const data = typeof raw === 'string' ? JSON.parse(raw) : raw;
if (data && data['命令'] === '跳转皮肤') _onMsg(data);
} catch(ex) {
// 可能是其他命令的消息,忽略
}
}
/* 停止监听 */
function stop() {
if (!_active) return;
_active = false;
_lastChampId = 0;
}
function reset() { _lastChampId = 0; }
function isActive() { return _active; }
return { start, stop, reset, isActive };
})();