264 行
10 KiB
JavaScript
264 行
10 KiB
JavaScript
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 ───
|
||
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 = '<span class="fnw-toast-icon">' +
|
||
(type === 'ok' ? '✓' : type === 'err' ? '✗' : 'ⓘ') +
|
||
'</span><span class="fnw-toast-msg">' + msg + '</span>';
|
||
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 = {
|
||
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);
|
||
}
|
||
}
|
||
})();
|