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:
@ -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