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 += '
' + '
' - + '' + escapeHTML(item.type_name) + '' + + '' + escapeHtml(item.type_name) + '' + '' + formatTime(item.created_at) + '' + '
' + '
' - + '' + escapeHTML(item.description) + '' + + '' + escapeHtml(item.description) + '' + '' + sign + item.amount_display + ' 域能' + '
' + '
'; @@ -1014,16 +1014,7 @@ logListEl.innerHTML = '
加载失败
'; }); - function escapeHTML(str) { - var div = document.createElement('div'); - div.appendChild(document.createTextNode(str)); - return div.innerHTML; - } - - function formatTime(isoStr) { - if (!isoStr) return ''; - return isoStr.replace('T', ' ').replace(/\+.*/, ''); - } + // escapeHtml / formatTime 由 shared/utils.js 统一提供 })(); // ===== 收藏夹 Tab ===== @@ -1032,26 +1023,14 @@ var createBtn = document.getElementById('createFolderBtn'); if (!listEl) return; - function esc(s) { - var d = document.createElement('div'); - d.textContent = s || ''; - return d.innerHTML; - } - - 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(); }); - } + // escapeHtml / api 由 shared/utils.js 统一提供 function loadFolders() { listEl.innerHTML = '
加载中...
'; api('GET', '/api/folders') .then(function (d) { if (!d.success) { - listEl.innerHTML = '
' + esc(d.message || '加载失败') + '
'; + listEl.innerHTML = '
' + escapeHtml(d.message || '加载失败') + '
'; return; } if (!d.data || !d.data.folders || d.data.folders.length === 0) { @@ -1064,7 +1043,7 @@ var pub = f.is_public ? '公开' : '私密'; html += '
' + '
' - + '' + 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;