服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
114 lines
3.8 KiB
JavaScript
114 lines
3.8 KiB
JavaScript
// ============================================================
|
||
// DNF90 GM 工具 — API 客户端与通用工具
|
||
// ============================================================
|
||
|
||
const API = {
|
||
async request(method, path, body) {
|
||
const opts = { method, headers: {} };
|
||
if (body !== undefined) {
|
||
opts.headers['Content-Type'] = 'application/json';
|
||
opts.body = JSON.stringify(body);
|
||
}
|
||
const res = await fetch(path, opts);
|
||
let data;
|
||
try { data = await res.json(); }
|
||
catch (e) { data = { ok: false, error: '响应解析失败' }; }
|
||
if (!res.ok || data.ok === false) {
|
||
const err = new Error(data.error || `请求失败 (${res.status})`);
|
||
err.data = data;
|
||
throw err;
|
||
}
|
||
return data.data;
|
||
},
|
||
get(path) { return this.request('GET', path); },
|
||
post(path, body) { return this.request('POST', path, body ?? {}); },
|
||
};
|
||
|
||
// ---------- Toast ----------
|
||
function toast(msg, type = 'info', ms = 3200) {
|
||
let wrap = document.querySelector('.toast-wrap');
|
||
if (!wrap) {
|
||
wrap = document.createElement('div');
|
||
wrap.className = 'toast-wrap';
|
||
document.body.appendChild(wrap);
|
||
}
|
||
const el = document.createElement('div');
|
||
el.className = 'toast ' + type;
|
||
const icon = type === 'ok' ? '✓' : type === 'err' ? '✕' : 'ℹ';
|
||
el.innerHTML = `<span style="font-weight:700">${icon}</span><span>${escapeHtml(msg)}</span>`;
|
||
wrap.appendChild(el);
|
||
setTimeout(() => {
|
||
el.style.transition = 'opacity 0.3s, transform 0.3s';
|
||
el.style.opacity = '0';
|
||
el.style.transform = 'translateX(30px)';
|
||
setTimeout(() => el.remove(), 300);
|
||
}, ms);
|
||
}
|
||
|
||
function escapeHtml(s) {
|
||
if (s === null || s === undefined) return '';
|
||
return String(s).replace(/[&<>"']/g, c => ({
|
||
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
||
}[c]));
|
||
}
|
||
|
||
function fmt(n) {
|
||
if (n === null || n === undefined || n === '') return '—';
|
||
const num = Number(n);
|
||
if (isNaN(num)) return String(n);
|
||
return num.toLocaleString('zh-CN');
|
||
}
|
||
|
||
function rarityColor(r) { return `rarity-${r}`; }
|
||
function rarityName(r) {
|
||
const names = ['普通', '高级', '稀有', '神器', '传说', '史诗', '神话'];
|
||
return names[r] || '未知';
|
||
}
|
||
|
||
// ---------- Modal ----------
|
||
function openModal({ title, body, foot, wide = false }) {
|
||
const mask = document.createElement('div');
|
||
mask.className = 'modal-mask';
|
||
mask.innerHTML = `
|
||
<div class="modal ${wide ? 'wide' : ''}">
|
||
<div class="modal-head">
|
||
<span>${escapeHtml(title)}</span>
|
||
<button class="close" data-close>×</button>
|
||
</div>
|
||
<div class="modal-body"></div>
|
||
<div class="modal-foot"></div>
|
||
</div>`;
|
||
const modal = mask.querySelector('.modal');
|
||
const bodyEl = mask.querySelector('.modal-body');
|
||
const footEl = mask.querySelector('.modal-foot');
|
||
if (typeof body === 'string') bodyEl.innerHTML = body;
|
||
else if (body) bodyEl.appendChild(body);
|
||
if (typeof foot === 'string') footEl.innerHTML = foot;
|
||
else if (foot) footEl.appendChild(foot);
|
||
|
||
const close = () => mask.remove();
|
||
mask.addEventListener('click', e => { if (e.target === mask) close(); });
|
||
mask.querySelector('[data-close]').addEventListener('click', close);
|
||
document.body.appendChild(mask);
|
||
return { mask, modal, bodyEl, footEl, close };
|
||
}
|
||
|
||
// ---------- Loading ----------
|
||
function loadingHtml(text = '加载中…') {
|
||
return `<div class="loading"><div class="spinner"></div><div>${escapeHtml(text)}</div></div>`;
|
||
}
|
||
|
||
// 连接状态
|
||
let CONN = { connected: false, base_url: '' };
|
||
|
||
function updateConnDot() {
|
||
const dots = document.querySelectorAll('.conn-dot');
|
||
dots.forEach(d => {
|
||
d.className = 'conn-dot ' + (CONN.connected ? 'on' : 'off');
|
||
});
|
||
const txt = document.querySelector('.conn-text');
|
||
if (txt) txt.textContent = CONN.connected
|
||
? `已连接 ${CONN.base_url || ''}`
|
||
: '未连接服务端';
|
||
}
|