/** * 商城前台 AJAX 封装 * * 提供统一的 XHR 请求方法、购物车操作、签到、工单提交等。 * 依赖:页面中需有 或全局变量 FNW_CSRF */ var Fnw = (function () { 'use strict'; // ─── CSRF Token ─── function getCsrfToken() { var m = document.querySelector('meta[name="csrf"]'); return m ? m.getAttribute('content') : (window.FNW_CSRF || ''); } // ─── 通用 XHR ─── /** * 发送请求 * @param {Object} opts * @param {string} opts.action - API action 名称 * @param {string} [opts.method='GET'] - HTTP 方法 * @param {Object} [opts.data] - POST 数据 * @param {function} [opts.success] - 成功回调 (res) * @param {function} [opts.error] - 失败回调 (res) * @param {function} [opts.complete] - 完成回调 */ function request(opts) { var method = (opts.method || 'GET').toUpperCase(); var url = 'api.php?action=' + encodeURIComponent(opts.action); var xhr = new XMLHttpRequest(); xhr.open(method, url, true); xhr.setRequestHeader('Accept', 'application/json'); xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); if (method === 'POST') { xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); } // CSRF header for POST if (method === 'POST') { xhr.setRequestHeader('X-CSRF-Token', getCsrfToken()); } xhr.onreadystatechange = function () { if (xhr.readyState !== 4) return; if (opts.complete) opts.complete(); if (xhr.status === 0) { if (opts.error) opts.error({ ok: false, msg: '网络错误,请检查连接' }); return; } var res; try { res = JSON.parse(xhr.responseText); } catch (e) { res = { ok: false, msg: '服务器响应异常' }; } if (res.ok) { if (opts.success) opts.success(res); } else { if (opts.error) opts.error(res); } }; if (method === 'POST' && opts.data) { var params = []; // Always include csrf_token in POST body as fallback opts.data.csrf_token = getCsrfToken(); for (var k in opts.data) { if (opts.data.hasOwnProperty(k)) { params.push(encodeURIComponent(k) + '=' + encodeURIComponent(opts.data[k])); } } xhr.send(params.join('&')); } else { // Append GET params if (opts.data && method === 'GET') { var qs = []; for (var k in opts.data) { if (opts.data.hasOwnProperty(k)) { qs.push(encodeURIComponent(k) + '=' + encodeURIComponent(opts.data[k])); } } if (qs.length) url += '&' + qs.join('&'); } xhr.send(); } } // ─── Toast 提示 ─── var toastTimer = null; function showToast(msg, type) { type = type || 'ok'; // 'ok' | 'err' | 'info' var existing = document.getElementById('fnwToast'); if (existing) existing.remove(); var el = document.createElement('div'); el.id = 'fnwToast'; el.className = 'fnw-toast fnw-toast-' + type; el.innerHTML = '' + (type === 'ok' ? '✓' : type === 'err' ? '✗' : 'ⓘ') + '' + msg + ''; Object.assign(el.style, { position: 'fixed', top: '-60px', left: '50%', transform: 'translateX(-50%)', zIndex: '9999', padding: '12px 24px', borderRadius: '10px', fontSize: '.92rem', fontWeight: '500', display: 'flex', alignItems: 'center', gap: '8px', boxShadow: '0 8px 24px rgba(0,0,0,.15)', transition: 'top .3s ease', maxWidth: '90vw' }); var colors = { ok: { bg: '#f0fdf4', border: '#86efac', color: '#166534', icon: '#22c55e' }, err: { bg: '#fef2f2', border: '#fca5a5', color: '#991b1b', icon: '#ef4444' }, info: { bg: '#eff6ff', border: '#93c5fd', color: '#1e40af', icon: '#3b82f6' } }; var c = colors[type] || colors.info; el.style.background = c.bg; el.style.border = '1px solid ' + c.border; el.style.color = c.color; el.querySelector('.fnw-toast-icon').style.color = c.icon; document.body.appendChild(el); // Animate in requestAnimationFrame(function () { el.style.top = '20px'; }); clearTimeout(toastTimer); toastTimer = setTimeout(function () { el.style.top = '-60px'; setTimeout(function () { el.remove(); }, 320); }, 2800); return el; } // ─── 购物车 ─── var cart = { add: function (productId, qty, callback) { request({ action: 'cart_add', method: 'POST', data: { product_id: productId, qty: qty || 1 }, success: function (res) { showToast(res.msg || '已加入购物车', 'ok'); cart.updateBadge(res.data.cart_count); if (callback) callback(null, res.data); }, error: function (res) { showToast(res.msg || '加入购物车失败', 'err'); if (callback) callback(new Error(res.msg)); } }); }, update: function (productId, qty, callback) { request({ action: 'cart_update', method: 'POST', data: { product_id: productId, qty: qty }, success: function (res) { cart.updateBadge(res.data.cart_count); if (callback) callback(null, res.data); }, error: function (res) { showToast(res.msg || '更新失败', 'err'); if (callback) callback(new Error(res.msg)); } }); }, remove: function (productId, callback) { request({ action: 'cart_remove', method: 'POST', data: { product_id: productId }, success: function (res) { showToast('已移除', 'ok'); cart.updateBadge(res.data.cart_count); if (callback) callback(null, res.data); }, error: function (res) { showToast(res.msg || '移除失败', 'err'); if (callback) callback(new Error(res.msg)); } }); }, clear: function (callback) { request({ action: 'cart_clear', method: 'POST', success: function (res) { showToast('购物车已清空', 'ok'); cart.updateBadge(0); if (callback) callback(null); }, error: function (res) { showToast(res.msg || '清空失败', 'err'); if (callback) callback(new Error(res.msg)); } }); }, updateBadge: function (count) { var badges = document.querySelectorAll('.badge.cart-badge'); badges.forEach(function (b) { b.textContent = count; b.style.display = count > 0 ? 'inline' : 'none'; }); // Also try nav-link badge var navBadge = document.querySelector('a[href="cart"] .badge'); if (navBadge) { navBadge.textContent = count; navBadge.style.display = count > 0 ? 'inline' : 'none'; } } }; // ─── 签到 ─── var sign = { do: function (callback) { request({ action: 'sign_in', method: 'POST', success: function (res) { showToast(res.msg || '签到成功!', 'ok'); if (callback) callback(null, res.data); }, error: function (res) { showToast(res.msg || '签到失败', 'err'); if (callback) callback(new Error(res.msg)); } }); } }; // ─── 工单 ─── var ticket = { submit: function (data, callback) { request({ action: 'ticket_submit', method: 'POST', data: data, success: function (res) { showToast(res.msg || '工单提交成功!', 'ok'); if (callback) callback(null, res.data); }, error: function (res) { showToast(res.msg || '提交失败', 'err'); if (callback) callback(new Error(res.msg)); } }); } }; // ─── 搜索(防抖)───────────────────────────────────────────── var searchTimer = null; var search = { /** * 带防抖的搜索 * @param {string} q 关键词 * @param {number} [delay=400] 防抖毫秒数 * @param {function} callback 回调 (err, results) */ query: function (q, delay, callback) { if (typeof delay === 'function') { callback = delay; delay = 400; } clearTimeout(searchTimer); if (!q || q.length < 1) { if (callback) callback(null, []); return; } searchTimer = setTimeout(function () { request({ action: 'search', data: { q: q }, success: function (res) { if (callback) callback(null, res.data); }, error: function (res) { if (callback) callback(new Error(res.msg)); } }); }, delay); } }; // ─── 公开 API ─── return { request: request, toast: showToast, cart: cart, sign: sign, ticket: ticket, search: search, csrf: getCsrfToken }; })(); // ─── 页面加载后初始化:注入 CSRF meta ─── (function () { // 如果页面还没有 csrf meta tag,尝试从表单中获取并注入 if (!document.querySelector('meta[name="csrf"]')) { var csrfInput = document.querySelector('input[name="csrf"]'); if (csrfInput) { var meta = document.createElement('meta'); meta.name = 'csrf'; meta.content = csrfInput.value; document.head.appendChild(meta); } } })();