diff --git a/templates/MetaLab-2026/html/posts/show.html b/templates/MetaLab-2026/html/posts/show.html
index 0a739e7..8686b21 100644
--- a/templates/MetaLab-2026/html/posts/show.html
+++ b/templates/MetaLab-2026/html/posts/show.html
@@ -376,17 +376,6 @@
var currentFolderId = null;
var loading = false;
- function api(method, url, data) {
- var opts = { method: method, headers: { 'Content-Type': 'application/json' } };
- var token = getCSRFToken();
- if (token) opts.headers['X-CSRF-Token'] = token;
- if (data) opts.body = JSON.stringify(data);
- return fetch(url, opts).then(function(r) {
- if (r.status === 401) { window.location.href = '/auth/login?redirect=/posts/' + postId; throw new Error('unauthorized'); }
- return r.json();
- });
- }
-
// 更新按钮状态
function updateBtn(favorited) {
isFavorited = favorited;
@@ -624,28 +613,7 @@
var currentPage = 1;
var totalPages = 1;
- // ====== 工具函数 ======
- function api(method, url, data) {
- var opts = { method: method, headers: { 'Content-Type': 'application/json' } };
- var token = getCSRFToken();
- if (token) opts.headers['X-CSRF-Token'] = token;
- if (data) opts.body = JSON.stringify(data);
- return fetch(url, opts).then(function(r) { return r.json(); });
- }
-
- function escapeHtml(text) {
- var d = document.createElement('div');
- d.textContent = text;
- return d.innerHTML;
- }
-
- function formatTime(iso) {
- var d = new Date(iso);
- var pad = function(n) { return n < 10 ? '0'+n : n; };
- return d.getFullYear() + '-' + pad(d.getMonth()+1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes());
- }
-
- // ====== 渲染评论 HTML ======
+ // escapeHtml / formatTime / showToast / api 由 shared/utils.js 统一提供
function buildCommentCard(c) {
var time = c.created_at ? formatTime(c.created_at) : '';
var author = c.author_name ? escapeHtml(c.author_name) : '用户已注销';
@@ -1057,19 +1025,8 @@
});
}
- // ====== 评论高亮 + 已删除评论Toast ======
- function showToast(msg) {
- var toast = document.createElement('div');
- toast.className = 'toast';
- toast.textContent = msg;
- toast.style.cssText = 'position:fixed;top:20px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.85);color:#fff;padding:10px 24px;border-radius:6px;font-size:14px;z-index:9999;animation:fadeIn 0.3s ease';
- document.body.appendChild(toast);
- setTimeout(function() {
- toast.style.opacity = '0';
- toast.style.transition = 'opacity 0.3s';
- setTimeout(function() { toast.remove(); }, 300);
- }, 2500);
- }
+ // ====== 评论高亮 ======
+ // showToast 由 shared/utils.js 统一提供
function highlightComment(commentId) {
var card = commentsList.querySelector('.comment-card[data-id="' + commentId + '"]') ||
diff --git a/templates/MetaLab-2026/html/settings/index.html b/templates/MetaLab-2026/html/settings/index.html
index eefac56..bfdcdc6 100644
--- a/templates/MetaLab-2026/html/settings/index.html
+++ b/templates/MetaLab-2026/html/settings/index.html
@@ -999,11 +999,11 @@
var sign = item.amount > 0 ? '+' : '';
html += '
加载中...
';
api('GET', '/api/folders')
.then(function (d) {
if (!d.success) {
- listEl.innerHTML = ''
+ '
'
- + '' + esc(f.name) + isDefault + ''
+ + '' + escapeHtml(f.name) + isDefault + ''
+ '' + (f.item_count || 0) + ' 篇文章 · ' + pub + ''
+ '
'
+ '
'
diff --git a/templates/MetaLab-2026/static/js/shortcode.js b/templates/MetaLab-2026/static/js/shortcode.js
index 09e614c..8196018 100644
--- a/templates/MetaLab-2026/static/js/shortcode.js
+++ b/templates/MetaLab-2026/static/js/shortcode.js
@@ -92,15 +92,9 @@
};
// =========================================================================
- // 工具函数
+ // 工具函数 — escapeHtml 由 shared/utils.js 统一提供
// =========================================================================
- function escapeHtml(str) {
- var div = document.createElement('div');
- div.appendChild(document.createTextNode(str));
- return div.innerHTML;
- }
-
// =========================================================================
// 主渲染逻辑
// =========================================================================
diff --git a/templates/admin/static/js/comments.js b/templates/admin/static/js/comments.js
index 4bbc047..7ac7b1c 100644
--- a/templates/admin/static/js/comments.js
+++ b/templates/admin/static/js/comments.js
@@ -11,30 +11,14 @@
var pagination = document.getElementById('commentPagination');
var loading = document.getElementById('commentLoading');
- function api(url, opts) {
- opts = opts || {};
- return fetch(url, opts).then(function(r) { return r.json(); });
- }
-
- function escapeHtml(text) {
- if (!text) return '';
- var d = document.createElement('div');
- d.textContent = text;
- return d.innerHTML;
- }
-
- function formatTime(iso) {
- var d = new Date(iso);
- var pad = function(n) { return n < 10 ? '0' + n : n; };
- return d.getFullYear() + '-' + pad(d.getMonth()+1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes());
- }
+ // escapeHtml / formatTime / api 由 shared/utils.js 统一提供
function loadComments(page) {
loading.style.display = 'block';
var url = '/api/admin/comments?page=' + page + '&page_size=20';
if (currentKeyword) url += '&keyword=' + encodeURIComponent(currentKeyword);
- api(url).then(function(d) {
+ api('GET', url).then(function(d) {
loading.style.display = 'none';
if (!d.success) return;
totalPages = d.data.total_pages || 1;
diff --git a/templates/admin/static/js/common.js b/templates/admin/static/js/common.js
index 08e8565..e88e622 100644
--- a/templates/admin/static/js/common.js
+++ b/templates/admin/static/js/common.js
@@ -4,8 +4,8 @@
不包含任何页面特有逻辑
===================================================== */
-// --- AJAX 封装 ---
-// 注意:getCSRFToken() 由共享 utils.js 提供
+// --- AJAX 封装 (管理后台专用对象式 API) ---
+// 注意:escapeHtml / formatTime / showToast / getCSRFToken 由 shared/utils.js 统一提供
const api = {
async get(url) {
const res = await fetch(url, {
@@ -37,27 +37,6 @@ const api = {
}
};
-// --- Toast 通知 ---
-function showToast(message, type) {
- const container = document.querySelector('.toast-container') || createToastContainer();
- const toast = document.createElement('div');
- toast.className = `toast toast-${type}`;
- toast.textContent = message;
- container.appendChild(toast);
- setTimeout(() => {
- toast.style.opacity = '0';
- toast.style.transition = 'opacity 0.3s';
- setTimeout(() => toast.remove(), 300);
- }, 3000);
-}
-
-function createToastContainer() {
- const el = document.createElement('div');
- el.className = 'toast-container';
- document.body.appendChild(el);
- return el;
-}
-
// --- 确认弹窗 ---
function showConfirm(title, message) {
return new Promise((resolve) => {
@@ -88,23 +67,6 @@ function showConfirm(title, message) {
});
}
-function escapeHtml(text) {
- const div = document.createElement('div');
- div.textContent = text;
- return div.innerHTML;
-}
-
-// --- 时间格式化 ---
-function formatTime(isoStr) {
- if (!isoStr) return '—';
- const d = new Date(isoStr);
- return d.getFullYear() + '-' +
- String(d.getMonth() + 1).padStart(2, '0') + '-' +
- String(d.getDate()).padStart(2, '0') + ' ' +
- String(d.getHours()).padStart(2, '0') + ':' +
- String(d.getMinutes()).padStart(2, '0');
-}
-
// --- 角色标签 ---
// 数据源由服务端 model.RoleDisplayNames 注入为 window.__ROLE_NAMES
function roleLabel(role) {
diff --git a/templates/admin/static/js/energy.js b/templates/admin/static/js/energy.js
index b80e04f..fcbe047 100644
--- a/templates/admin/static/js/energy.js
+++ b/templates/admin/static/js/energy.js
@@ -185,17 +185,7 @@
}
}
- function formatTime(isoStr) {
- if (!isoStr) return '';
- var d = new Date(isoStr);
- var pad = function (n) { return n < 10 ? '0' + n : '' + n; };
- return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes());
- }
-
- function escapeHtml(text) {
- if (!text) return '';
- return text.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''');
- }
+ // escapeHtml / formatTime 由 shared/utils.js 统一提供
function showMsg(msgEl, text, type) {
if (!msgEl) return;
diff --git a/templates/admin/static/js/posts.js b/templates/admin/static/js/posts.js
index 0a43de3..a1a0707 100644
--- a/templates/admin/static/js/posts.js
+++ b/templates/admin/static/js/posts.js
@@ -1,16 +1,6 @@
(function() {
// CSRF token 由 shared/utils.js 的 getCSRFToken() 统一提供
-
- function api(url, method, body) {
- return fetch(url, {
- method: method,
- headers: {
- 'Content-Type': 'application/json',
- 'X-CSRF-Token': getCSRFToken()
- },
- body: body ? JSON.stringify(body) : undefined
- }).then(function(r) { return r.json(); });
- }
+ // api 对象 (api.get/api.post/api.put) 由 admin/common.js 统一提供
function refreshPage() { window.location.reload(); }
@@ -28,7 +18,7 @@
// 审核通过
handleClick('.post-approve', function(id) {
- api('/api/admin/posts/' + id + '/approve', 'POST')
+ api.post('/api/admin/posts/' + id + '/approve')
.then(function(d) { if (d.success) refreshPage(); else alert(d.message); })
.catch(function() { alert('操作失败'); });
});
@@ -37,7 +27,7 @@
handleClick('.post-reject', function(id) {
var reason = prompt('请输入退回理由(必填):');
if (!reason || !reason.trim()) return;
- api('/api/admin/posts/' + id + '/reject', 'POST', { reason: reason.trim() })
+ api.post('/api/admin/posts/' + id + '/reject', { reason: reason.trim() })
.then(function(d) { if (d.success) refreshPage(); else alert(d.message); })
.catch(function() { alert('操作失败'); });
});
@@ -46,14 +36,14 @@
handleClick('.post-lock', function(id) {
var reason = prompt('请输入锁定理由(必填):');
if (!reason || !reason.trim()) return;
- api('/api/admin/posts/' + id + '/lock', 'POST', { reason: reason.trim() })
+ api.post('/api/admin/posts/' + id + '/lock', { reason: reason.trim() })
.then(function(d) { if (d.success) refreshPage(); else alert(d.message); })
.catch(function() { alert('操作失败'); });
});
// 解锁
handleClick('.post-unlock', function(id) {
- api('/api/admin/posts/' + id + '/unlock', 'POST')
+ api.post('/api/admin/posts/' + id + '/unlock')
.then(function(d) { if (d.success) refreshPage(); else alert(d.message); })
.catch(function() { alert('操作失败'); });
});
diff --git a/templates/shared/static/js/utils.js b/templates/shared/static/js/utils.js
index 77353a8..b4b1eb4 100644
--- a/templates/shared/static/js/utils.js
+++ b/templates/shared/static/js/utils.js
@@ -15,6 +15,82 @@ function getCSRFToken() {
return meta ? meta.getAttribute('content') : '';
}
+/**
+ * HTML 转义 — 使用 DOM API 安全转义,避免 XSS
+ * @param {string} text — 原始文本
+ * @returns {string} 转义后的 HTML 安全字符串
+ */
+function escapeHtml(text) {
+ if (!text) return '';
+ var div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+}
+
+/**
+ * 时间格式化 — ISO 字符串转为 YYYY-MM-DD HH:mm
+ * @param {string} isoStr — ISO 8601 时间字符串
+ * @returns {string} 格式化后的时间,无效时返回 '—'
+ */
+function formatTime(isoStr) {
+ if (!isoStr) return '—';
+ var d = new Date(isoStr);
+ if (isNaN(d.getTime())) return isoStr;
+ return d.getFullYear() + '-' +
+ String(d.getMonth() + 1).padStart(2, '0') + '-' +
+ String(d.getDate()).padStart(2, '0') + ' ' +
+ String(d.getHours()).padStart(2, '0') + ':' +
+ String(d.getMinutes()).padStart(2, '0');
+}
+
+/**
+ * Toast 通知 — 创建临时提示,3 秒后自动淡出移除
+ * @param {string} message — 提示文本
+ * @param {string} [type='info'] — 类型:success / error / info / warning
+ */
+function showToast(message, type) {
+ type = type || 'info';
+ var colors = { success: '#16a34a', error: '#dc2626', info: 'rgba(0,0,0,0.85)', warning: '#d97706' };
+ var bg = colors[type] || colors.info;
+
+ var container = document.querySelector('.toast-container');
+ if (!container) {
+ container = document.createElement('div');
+ container.className = 'toast-container';
+ container.style.cssText = 'position:fixed;top:20px;right:20px;z-index:9999;display:flex;flex-direction:column;gap:8px;pointer-events:none';
+ document.body.appendChild(container);
+ }
+ var toast = document.createElement('div');
+ toast.className = 'toast toast-' + type;
+ toast.textContent = message;
+ toast.style.cssText = 'background:' + bg + ';color:#fff;padding:10px 24px;border-radius:6px;font-size:14px;pointer-events:auto';
+ container.appendChild(toast);
+ setTimeout(function () {
+ toast.style.opacity = '0';
+ toast.style.transition = 'opacity 0.3s';
+ setTimeout(function () { toast.remove(); }, 300);
+ }, 3000);
+}
+
+/**
+ * 统一 API 请求封装 — 自动注入 CSRF Token
+ * @param {string} method — HTTP 方法 (GET/POST/PUT/DELETE)
+ * @param {string} url — 请求 URL
+ * @param {object} [data] — 请求体数据,GET 请求忽略
+ * @returns {Promise} fetch 的 response JSON
+ */
+function api(method, url, data) {
+ var opts = {
+ method: method,
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRF-Token': getCSRFToken()
+ }
+ };
+ if (data) opts.body = JSON.stringify(data);
+ return fetch(url, opts).then(function (r) { return r.json(); });
+}
+
// 全局头像图片加载失败回退(替代 onerror 内联事件,满足 CSP 无 unsafe-inline)
document.addEventListener('error', function(e) {
var img = e.target;