// ===== lib-api.jsx ===== // 极简 API 客户端,挂在 window.API。所有方法返回 Promise。 // 失败时 reject 一个 ApiError,含 status / code / message / detail。 class ApiError extends Error { constructor({ status, code, message, detail }) { super(message || 'API 请求失败'); this.status = status; this.code = code || 'unknown'; this.detail = detail || {}; } } const BASE = ''; // 同源,走 Caddy 反代 async function _req(method, path, { body, headers, formData } = {}) { const opts = { method, headers: { ...(headers || {}) } }; if (formData) { opts.body = formData; } else if (body !== undefined) { opts.body = JSON.stringify(body); opts.headers['Content-Type'] = 'application/json'; } const resp = await fetch(BASE + path, opts); let payload = null; const text = await resp.text(); try { payload = text ? JSON.parse(text) : null; } catch (_) { payload = { raw: text }; } if (!resp.ok) { throw new ApiError({ status: resp.status, code: (payload && payload.code) || `http_${resp.status}`, message: (payload && payload.message) || `请求失败 (${resp.status})`, detail: (payload && payload.detail) || payload, }); } return payload; } const TOKEN_PREFIX = 'mojian:token:'; window.API = { ApiError, // ----- 凭证管理(localStorage:跨 tab 跨刷新跨重启都保留) ----- saveToken(orderId, token) { if (!orderId || !token) return; try { localStorage.setItem(TOKEN_PREFIX + orderId, token); } catch (e) { // 隐私模式下 localStorage 可能不可写,降级用 sessionStorage try { sessionStorage.setItem(TOKEN_PREFIX + orderId, token); } catch (_) {} } }, getToken(orderId) { return ( localStorage.getItem(TOKEN_PREFIX + orderId) || sessionStorage.getItem(TOKEN_PREFIX + orderId) || '' ); }, clearToken(orderId) { try { localStorage.removeItem(TOKEN_PREFIX + orderId); } catch (_) {} try { sessionStorage.removeItem(TOKEN_PREFIX + orderId); } catch (_) {} }, // ----- 公开 API ----- async uploadOrder(file) { const fd = new FormData(); fd.append('file', file); return _req('POST', '/api/orders', { formData: fd }); }, async getOrder(orderId) { return _req('GET', `/api/orders/${encodeURIComponent(orderId)}`); }, async getReport(orderId, token) { const t = token || this.getToken(orderId); return _req('GET', `/api/orders/${encodeURIComponent(orderId)}/report?token=${encodeURIComponent(t)}`); }, getReportPdfUrl(orderId, token) { const t = token || this.getToken(orderId); return `/api/orders/${encodeURIComponent(orderId)}/report/pdf?token=${encodeURIComponent(t)}`; }, async trial(text) { const fd = new FormData(); fd.append('text', text); return _req('POST', '/api/orders/trial', { formData: fd }); }, async joinWaitlist(payload) { return _req('POST', '/api/waitlist', { body: payload }); }, // ----- 管理员 API(Basic Auth) ----- ADMIN_KEY: 'mojian:admin:basic', saveAdmin(username, password) { sessionStorage.setItem(this.ADMIN_KEY, btoa(`${username}:${password}`)); }, clearAdmin() { sessionStorage.removeItem(this.ADMIN_KEY); }, isAdminLoggedIn() { return !!sessionStorage.getItem(this.ADMIN_KEY); }, _adminHeaders() { const b64 = sessionStorage.getItem(this.ADMIN_KEY); if (!b64) throw new ApiError({ status: 401, code: 'not_logged_in', message: '未登录' }); return { Authorization: 'Basic ' + b64 }; }, async adminListOrders({ status, amount_suffix, q, limit = 50, offset = 0 } = {}) { const params = new URLSearchParams(); if (status) params.set('status', status); if (amount_suffix !== undefined && amount_suffix !== null && amount_suffix !== '') { params.set('amount_suffix', String(amount_suffix)); } if (q) params.set('q', q); params.set('limit', String(limit)); params.set('offset', String(offset)); return _req('GET', '/api/admin/orders?' + params.toString(), { headers: this._adminHeaders() }); }, async adminConfirm(orderId, note = '') { return _req('POST', `/api/admin/orders/${encodeURIComponent(orderId)}/confirm`, { body: { note }, headers: this._adminHeaders() }); }, async adminRefund(orderId, reason = '') { return _req('POST', `/api/admin/orders/${encodeURIComponent(orderId)}/refund`, { body: { reason }, headers: this._adminHeaders() }); }, async adminReprocess(orderId) { return _req('POST', `/api/admin/orders/${encodeURIComponent(orderId)}/reprocess`, { body: {}, headers: this._adminHeaders() }); }, async adminStats() { return _req('GET', '/api/admin/stats', { headers: this._adminHeaders() }); }, async adminWaitlist({ tier, limit = 100 } = {}) { const params = new URLSearchParams(); if (tier) params.set('tier', tier); params.set('limit', String(limit)); return _req('GET', '/api/admin/waitlist?' + params.toString(), { headers: this._adminHeaders() }); }, };