refactor: 提取公共工具函数到 shared/utils.js,消除 DRY 重复
- escapeHtml 8 处定义合并为 shared/utils.js 1 处:删除 shortcode.js、posts/show.html、settings/index.html、comments.js、energy.js、common.js 中的重复定义 - formatTime 6 处定义合并为 1 处:统一 ISO 时间格式化逻辑 - api() 5 处定义收敛:前台页面使用 shared/utils.js 统一 api(method,url,data),admin 使用 common.js 的 api 对象 - showToast 3 处定义合并为 1 处:自包含内联样式,前后台通用 - admin/posts.js api 调用迁移为 api.post()
This commit is contained in:
@ -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 + '"]') ||
|
||||
|
||||
@ -999,11 +999,11 @@
|
||||
var sign = item.amount > 0 ? '+' : '';
|
||||
html += '<div class="energy-log-item ' + cls + '">'
|
||||
+ '<div class="energy-log-meta">'
|
||||
+ '<span class="energy-log-type">' + escapeHTML(item.type_name) + '</span>'
|
||||
+ '<span class="energy-log-type">' + escapeHtml(item.type_name) + '</span>'
|
||||
+ '<span class="energy-log-time">' + formatTime(item.created_at) + '</span>'
|
||||
+ '</div>'
|
||||
+ '<div class="energy-log-info">'
|
||||
+ '<span class="energy-log-desc">' + escapeHTML(item.description) + '</span>'
|
||||
+ '<span class="energy-log-desc">' + escapeHtml(item.description) + '</span>'
|
||||
+ '<span class="energy-log-amount">' + sign + item.amount_display + ' 域能</span>'
|
||||
+ '</div>'
|
||||
+ '</div>';
|
||||
@ -1014,16 +1014,7 @@
|
||||
logListEl.innerHTML = '<div class="energy-log-empty">加载失败</div>';
|
||||
});
|
||||
|
||||
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 = '<div class="energy-log-empty">加载中...</div>';
|
||||
api('GET', '/api/folders')
|
||||
.then(function (d) {
|
||||
if (!d.success) {
|
||||
listEl.innerHTML = '<div class="energy-log-empty">' + esc(d.message || '加载失败') + '</div>';
|
||||
listEl.innerHTML = '<div class="energy-log-empty">' + escapeHtml(d.message || '加载失败') + '</div>';
|
||||
return;
|
||||
}
|
||||
if (!d.data || !d.data.folders || d.data.folders.length === 0) {
|
||||
@ -1064,7 +1043,7 @@
|
||||
var pub = f.is_public ? '公开' : '私密';
|
||||
html += '<div class="fav-folder-card">'
|
||||
+ '<div class="fav-folder-info">'
|
||||
+ '<span class="fav-folder-name">' + esc(f.name) + isDefault + '</span>'
|
||||
+ '<span class="fav-folder-name">' + escapeHtml(f.name) + isDefault + '</span>'
|
||||
+ '<span class="fav-folder-meta">' + (f.item_count || 0) + ' 篇文章 · ' + pub + '</span>'
|
||||
+ '</div>'
|
||||
+ '<div class="fav-folder-actions">'
|
||||
|
||||
@ -92,15 +92,9 @@
|
||||
};
|
||||
|
||||
// =========================================================================
|
||||
// 工具函数
|
||||
// 工具函数 — escapeHtml 由 shared/utils.js 统一提供
|
||||
// =========================================================================
|
||||
|
||||
function escapeHtml(str) {
|
||||
var div = document.createElement('div');
|
||||
div.appendChild(document.createTextNode(str));
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 主渲染逻辑
|
||||
// =========================================================================
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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, '"').replace(/'/g, ''');
|
||||
}
|
||||
// escapeHtml / formatTime 由 shared/utils.js 统一提供
|
||||
|
||||
function showMsg(msgEl, text, type) {
|
||||
if (!msgEl) return;
|
||||
|
||||
@ -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('操作失败'); });
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
Reference in New Issue
Block a user