feat: 新增多项功能并添加新工具

- 私人剪贴板新增限时直通URL生成功能
- doc_cloud_keeper新增标题复制按钮
- 新增贪蛇出动游戏工具
- 新增OSS中转直链工具
- 添加批量抽帧测试脚本
This commit is contained in:
yangxiangyuan
2026-08-11 10:22:31 +08:00
parent 91148fe064
commit 6dc79a2c12
28 changed files with 3169 additions and 6 deletions
+213
View File
@@ -0,0 +1,213 @@
/* 贪蛇出动 - 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 => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;' }[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;
})();