// ============================================================ // 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 = `${icon}${escapeHtml(msg)}`; 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 = `