728 lines
28 KiB
JavaScript
728 lines
28 KiB
JavaScript
/* =============================================
|
||
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,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>');
|
||
}
|
||
|
||
// ── 页面路由 ──────────────────────────────────
|
||
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 };
|
||
})();
|