6dc79a2c12
- 私人剪贴板新增限时直通URL生成功能 - doc_cloud_keeper新增标题复制按钮 - 新增贪蛇出动游戏工具 - 新增OSS中转直链工具 - 添加批量抽帧测试脚本
214 lines
9.2 KiB
JavaScript
214 lines
9.2 KiB
JavaScript
/* 贪蛇出动 - UI:HUD 刷新 / 道具转盘 / 复活 / 结算 / Toast 与交互详情弹窗(双模式) */
|
||
(function () {
|
||
'use strict';
|
||
window.GS = window.GS || {};
|
||
const U = GS.U;
|
||
|
||
/* ---------- 道具图标:程序化 SVG ---------- */
|
||
function itemSvg(key) {
|
||
switch (key) {
|
||
case 'helmet':
|
||
return '<svg viewBox="0 0 64 64"><path d="M10 38a22 22 0 0 1 44 0v6H36l-4-8-4 8H10z" fill="#e04a3a"/><path d="M14 38a18 18 0 0 1 36 0" fill="none" stroke="#fff" stroke-width="3" opacity=".55"/><rect x="8" y="42" width="48" height="6" rx="3" fill="#b83224"/></svg>';
|
||
case 'rocket':
|
||
return '<svg viewBox="0 0 64 64"><path d="M32 4c8 8 10 20 10 28l-10 10-10-10c0-8 2-20 10-28z" fill="#e8ecf5"/><circle cx="32" cy="24" r="5" fill="#5a8dd0"/><path d="M22 32l-8 12 10-2zM42 32l8 12-10-2z" fill="#e04a3a"/><path d="M28 44l4 12 4-12z" fill="#ffb62e"/></svg>';
|
||
case 'magnet':
|
||
return '<svg viewBox="0 0 64 64"><path d="M18 12v20a14 14 0 0 0 28 0V12H36v20a4 4 0 0 1-8 0V12z" fill="#e04a3a"/><rect x="18" y="12" width="10" height="10" fill="#e8ecf5"/><rect x="36" y="12" width="10" height="10" fill="#e8ecf5"/></svg>';
|
||
case 'expcard':
|
||
return '<svg viewBox="0 0 64 64"><rect x="8" y="16" width="48" height="32" rx="5" fill="#ffd23e"/><rect x="8" y="16" width="48" height="32" rx="5" fill="none" stroke="#c98f00" stroke-width="2"/><circle cx="24" cy="32" r="8" fill="#fff3c4"/><path d="M38 26h12M38 32h12M38 38h8" stroke="#c98f00" stroke-width="3" stroke-linecap="round"/></svg>';
|
||
default: return '';
|
||
}
|
||
}
|
||
|
||
class UI {
|
||
constructor() {
|
||
this.el = id => document.getElementById(id);
|
||
this.statsEl = this.el('gs-hud-stats');
|
||
this.coinsEl = this.el('gs-stat-coins');
|
||
this.luckyEl = this.el('gs-stat-lucky');
|
||
this.killsEl = this.el('gs-stat-kills');
|
||
this.progEl = this.el('gs-hud-progress');
|
||
this.progFill = this.el('gs-progress-fill');
|
||
this.progText = this.el('gs-progress-text');
|
||
this.boardEl = this.el('gs-hud-board');
|
||
this.boardList = this.el('gs-board-list');
|
||
this.bestName = this.el('gs-best-name');
|
||
this.bestScore = this.el('gs-best-score');
|
||
this.rouletteEl = this.el('gs-roulette');
|
||
this.reviveEl = this.el('gs-revive');
|
||
this.resultEl = this.el('gs-result');
|
||
this.toastEl = this.el('gs-toast');
|
||
this.dlgEl = this.el('gs-dialog');
|
||
this._bulbs();
|
||
this._toastTimer = null;
|
||
this._dlgTimer = null;
|
||
}
|
||
|
||
/* 转盘灯泡装饰 */
|
||
_bulbs() {
|
||
document.querySelectorAll('.gs-bulbs').forEach(box => {
|
||
const vert = box.classList.contains('gs-bulbs-left') || box.classList.contains('gs-bulbs-right');
|
||
const n = vert ? 7 : 12;
|
||
for (let i = 0; i < n; i++) box.appendChild(document.createElement('i'));
|
||
});
|
||
setInterval(() => {
|
||
document.querySelectorAll('.gs-bulbs i').forEach((b, i) => b.classList.toggle('off', (i + Math.floor(Date.now() / 400)) % 2 === 0));
|
||
}, 400);
|
||
}
|
||
|
||
setUiMode(mode) { GS.CFG.uiMode = mode; }
|
||
|
||
/* ---------- HUD ---------- */
|
||
showHud(v) {
|
||
this.statsEl.classList.toggle('gs-hidden', !v);
|
||
this.boardEl.classList.toggle('gs-hidden', !v);
|
||
}
|
||
|
||
updateStats(totalCoins, lucky, kills) {
|
||
this.coinsEl.textContent = U.fmt(totalCoins);
|
||
this.luckyEl.textContent = U.fmt(lucky);
|
||
this.killsEl.textContent = U.fmt(kills);
|
||
}
|
||
|
||
updateProgress(score, target, visible) {
|
||
this.progEl.classList.toggle('gs-hidden', !visible);
|
||
if (!visible) return;
|
||
this.progFill.style.width = U.clamp(score / target * 100, 0, 100) + '%';
|
||
this.progText.textContent = Math.floor(Math.min(score, target)) + '/' + target;
|
||
}
|
||
|
||
updateBoard(rows, me, best) {
|
||
if (best) {
|
||
this.bestName.textContent = best.name || '-';
|
||
this.bestScore.textContent = U.fmt(best.score || 0);
|
||
}
|
||
let html = '';
|
||
rows.forEach((r, i) => {
|
||
const rank = i + 1;
|
||
const medal = rank === 1 ? '👑' : (rank <= 3 ? ['①', '②', '③'][rank - 1] : rank);
|
||
html += `<div class="gs-board-row${r.me ? ' me' : ''}">
|
||
<span class="gs-board-rank r${rank}">${medal}</span>
|
||
<span class="gs-board-name">${this._esc(r.name)}</span>
|
||
<span class="gs-board-score">${U.fmt(r.score)}</span></div>`;
|
||
});
|
||
this.boardList.innerHTML = html;
|
||
}
|
||
|
||
_esc(s) { return String(s).replace(/[<>&"]/g, c => ({ '<': '<', '>': '>', '&': '&', '"': '"' }[c])); }
|
||
|
||
/* ---------- 转盘 ---------- */
|
||
showRoulette(gems) {
|
||
this.rouletteEl.classList.remove('gs-hidden');
|
||
this.el('gs-reroll-text').textContent = GS.CFG.rerollCost + ' 换一换';
|
||
this.updateGems(gems);
|
||
}
|
||
hideRoulette() { this.rouletteEl.classList.add('gs-hidden'); }
|
||
updateGems(gems) {
|
||
const btn = this.el('gs-reroll');
|
||
btn.disabled = gems < GS.CFG.rerollCost;
|
||
}
|
||
setReels(keys, selected) {
|
||
document.querySelectorAll('.gs-reel').forEach((reel, i) => {
|
||
reel.classList.toggle('selected', selected === i);
|
||
reel.querySelector('.gs-reel-icon').innerHTML = itemSvg(keys[i]);
|
||
reel.title = GS.ITEMS[keys[i]].name + ':' + GS.ITEMS[keys[i]].desc;
|
||
});
|
||
}
|
||
spinReels(keys) {
|
||
// 滚动动画:快速切换图标后落定
|
||
const reels = document.querySelectorAll('.gs-reel');
|
||
reels.forEach((reel, i) => {
|
||
const icon = reel.querySelector('.gs-reel-icon');
|
||
let n = 0;
|
||
const timer = setInterval(() => {
|
||
icon.innerHTML = itemSvg(U.pick(GS.ITEM_KEYS));
|
||
if (++n > 8 + i * 4) { clearInterval(timer); icon.innerHTML = itemSvg(keys[i]); }
|
||
}, 70);
|
||
});
|
||
}
|
||
|
||
/* ---------- 复活 ---------- */
|
||
showRevive(killerName, usedFree) {
|
||
this.reviveEl.classList.remove('gs-hidden');
|
||
this.el('gs-revive-killer').textContent = killerName || '对手';
|
||
const btn = this.el('gs-revive-btn');
|
||
btn.disabled = usedFree;
|
||
btn.textContent = usedFree ? '已复活过' : '▶ 免费复活';
|
||
}
|
||
hideRevive() { this.reviveEl.classList.add('gs-hidden'); }
|
||
updateReviveCount(n, pct) {
|
||
this.el('gs-revive-count').textContent = n;
|
||
this.el('gs-revive-count-ring').style.setProperty('--p', (pct * 100) + '%');
|
||
}
|
||
|
||
/* ---------- 结算 ---------- */
|
||
showResult(data) {
|
||
this.resultEl.classList.remove('gs-hidden');
|
||
this.el('gs-result-score').textContent = U.fmt(data.score);
|
||
this.el('gs-result-coins').textContent = U.fmt(data.coins);
|
||
this.el('gs-result-kills').textContent = U.fmt(data.kills);
|
||
this.el('gs-result-lucky').textContent = U.fmt(data.lucky);
|
||
this.el('gs-result-best-num').textContent = U.fmt(data.best);
|
||
}
|
||
hideResult() { this.resultEl.classList.add('gs-hidden'); }
|
||
|
||
/* ---------- Toast(normal 模式 / 强提示) ---------- */
|
||
toast(msg, type) {
|
||
const el = this.toastEl;
|
||
el.textContent = msg;
|
||
el.className = type || '';
|
||
el.classList.remove('gs-hidden');
|
||
clearTimeout(this._toastTimer);
|
||
this._toastTimer = setTimeout(() => el.classList.add('gs-hidden'), 2000);
|
||
}
|
||
|
||
/* ---------- 交互详情弹窗(debug 模式) ---------- */
|
||
showDialog(info, mode) {
|
||
// mode: view=查看类(normal 降级 toast) / error=异常(双模式保留)
|
||
const isDebug = GS.CFG.uiMode === 'debug';
|
||
if (!isDebug && mode === 'view') { this.toast(info.action + ' 成功', 'success'); return; }
|
||
if (!isDebug && mode !== 'error') { this.toast(info.action + (info.ok ? ' 成功' : ' 失败'), info.ok ? 'success' : 'error'); return; }
|
||
const body = this.el('gs-dialog-body');
|
||
body.textContent =
|
||
'触发动作:' + info.action + '\n' +
|
||
'时间:' + U.nowStr() + '\n' +
|
||
'接口地址:' + (info.url || '-') + '\n' +
|
||
'HTTP 方法:' + (info.method || '-') + '\n' +
|
||
'请求参数:' + (info.req || '-') + '\n' +
|
||
'返回结果:' + (info.res || '-') + '\n' +
|
||
'错误信息:' + (info.err || '-') + '\n' +
|
||
'耗时:' + (info.ms != null ? info.ms + 'ms' : '-') + '\n' +
|
||
'当前状态:' + (info.ok ? '成功' : '失败');
|
||
this.dlgEl.classList.remove('gs-hidden');
|
||
this._dlgInfo = info;
|
||
clearTimeout(this._dlgTimer);
|
||
if (mode !== 'error') this._dlgTimer = setTimeout(() => this.hideDialog(), 3000);
|
||
}
|
||
hideDialog() { this.dlgEl.classList.add('gs-hidden'); }
|
||
|
||
/* 带交互记录的 fetch 包装(双模式) */
|
||
async trackFetch(action, url, opts) {
|
||
const t0 = performance.now();
|
||
let info = { action, url, method: (opts && opts.method) || 'GET', req: opts && opts.body ? opts.body : '-' };
|
||
try {
|
||
const res = await fetch(url, opts);
|
||
const text = await res.text();
|
||
info.ms = Math.round(performance.now() - t0);
|
||
info.res = text.slice(0, 400);
|
||
info.ok = res.ok;
|
||
if (!res.ok) info.err = 'HTTP ' + res.status;
|
||
this.showDialog(info, res.ok ? 'view' : 'error');
|
||
return res.ok ? JSON.parse(text) : null;
|
||
} catch (e) {
|
||
info.ms = Math.round(performance.now() - t0);
|
||
info.ok = false;
|
||
info.err = String(e && e.message || e);
|
||
this.showDialog(info, 'error');
|
||
return null;
|
||
}
|
||
}
|
||
}
|
||
|
||
GS.UI = UI;
|
||
GS.itemSvg = itemSvg;
|
||
})();
|