This repository has been archived on 2026-06-21. You can view files and clone it, but cannot push or open issues or pull requests.
Files
MetaLab/templates/shared/static/js/utils.js
Victor_Jay c73b131b83 feat: prompt/confirm 替换为自定义弹窗
- shared/utils.js 新增 showConfirm()/showPrompt() 通用函数(inline CSS,无需额外样式)
- admin/common.js 移除重复 showConfirm(),统一使用 utils.js
- admin/posts.js: 3 处 confirm/prompt 替换(审核/退回/锁定)
- admin/comments.js: 1 处 confirm 替换
- settings/index.html: 4 处 confirm + 1 处 prompt 替换
- studio/posts.html + drafts.html: 3 处 confirm 替换
- post-view.js + login.js + post-favorite.js: 3 处 confirm 替换
2026-06-02 20:11:48 +08:00

188 lines
8.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ============================================================
MetaLab 共享工具函数 — 前后台公用,消除 DRY 重复
============================================================ */
'use strict';
/**
* 获取 CSRF 令牌 — 优先从 cookie 读取与服务端验证源一致token 刷新后保持最新),
* meta 标签作为后备(部分页面可能未设置 cookie
* @returns {string} CSRF 令牌,未找到时返回空字符串
*/
function getCSRFToken() {
var match = document.cookie.match(/(?:^|;\s*)mlb_csrf=([^;]*)/);
if (match && match[1]) return match[1];
var meta = document.querySelector('meta[name="csrf-token"]');
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(); });
}
/**
* 确认弹窗 — 替代原生 confirm(),统一全站风格
* @param {string} title — 弹窗标题
* @param {string} message — 提示消息(支持 \n 换行)
* @returns {Promise<boolean>} 用户确认返回 true取消返回 false
*/
function showConfirm(title, message) {
return new Promise(function(resolve) {
var safeTitle = escapeHtml(title);
var safeMsg = escapeHtml(message).replace(/\n/g, '<br>');
var overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.4);z-index:9998;display:flex;align-items:center;justify-content:center;';
overlay.innerHTML = '<div style="background:#fff;border-radius:8px;padding:28px 32px;min-width:320px;max-width:440px;box-shadow:0 8px 30px rgba(0,0,0,0.15);">'
+ '<h3 style="margin:0 0 12px 0;font-size:17px;color:#2d3436;">' + safeTitle + '</h3>'
+ '<p style="margin:0 0 20px 0;font-size:14px;line-height:1.6;color:#636e72;">' + safeMsg + '</p>'
+ '<div style="display:flex;gap:10px;justify-content:flex-end;">'
+ '<button id="mlbModalCancel" style="padding:8px 20px;border:1px solid #d1d5db;border-radius:6px;background:#fff;color:#374151;font-size:14px;cursor:pointer;">取消</button>'
+ '<button id="mlbModalConfirm" style="padding:8px 20px;border:none;border-radius:6px;background:#dc2626;color:#fff;font-size:14px;cursor:pointer;">确认</button>'
+ '</div></div>';
document.body.appendChild(overlay);
function close(val) {
overlay.remove();
resolve(val);
}
overlay.querySelector('#mlbModalCancel').onclick = function() { close(false); };
overlay.querySelector('#mlbModalConfirm').onclick = function() { close(true); };
overlay.addEventListener('click', function(e) {
if (e.target === overlay) close(false);
});
// Enter 键确认
var onKey = function(e) {
if (e.key === 'Enter') { document.removeEventListener('keydown', onKey); close(true); }
if (e.key === 'Escape') { document.removeEventListener('keydown', onKey); close(false); }
};
document.addEventListener('keydown', onKey);
});
}
/**
* 输入弹窗 — 替代原生 prompt(),统一全站风格
* @param {string} message — 提示文本
* @param {string} [defaultValue=''] — 输入框默认值
* @returns {Promise<string|null>} 用户输入的值,取消返回 null
*/
function showPrompt(message, defaultValue) {
return new Promise(function(resolve) {
var safeMsg = escapeHtml(message);
var overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.4);z-index:9998;display:flex;align-items:center;justify-content:center;';
overlay.innerHTML = '<div style="background:#fff;border-radius:8px;padding:28px 32px;min-width:320px;max-width:440px;box-shadow:0 8px 30px rgba(0,0,0,0.15);">'
+ '<p style="margin:0 0 16px 0;font-size:14px;line-height:1.6;color:#2d3436;">' + safeMsg + '</p>'
+ '<input id="mlbPromptInput" type="text" style="width:100%;padding:10px 12px;border:1px solid #d1d5db;border-radius:6px;font-size:14px;color:#2d3436;outline:none;box-sizing:border-box;" value="' + escapeHtml(defaultValue || '') + '" placeholder="">'
+ '<div style="display:flex;gap:10px;justify-content:flex-end;margin-top:16px;">'
+ '<button id="mlbPromptCancel" style="padding:8px 20px;border:1px solid #d1d5db;border-radius:6px;background:#fff;color:#374151;font-size:14px;cursor:pointer;">取消</button>'
+ '<button id="mlbPromptConfirm" style="padding:8px 20px;border:none;border-radius:6px;background:#0984e3;color:#fff;font-size:14px;cursor:pointer;">确认</button>'
+ '</div></div>';
document.body.appendChild(overlay);
var input = overlay.querySelector('#mlbPromptInput');
function close(val) {
overlay.remove();
resolve(val);
}
overlay.querySelector('#mlbPromptCancel').onclick = function() { close(null); };
overlay.querySelector('#mlbPromptConfirm').onclick = function() { close(input.value); };
overlay.addEventListener('click', function(e) {
if (e.target === overlay) close(null);
});
// Enter 确认Escape 取消
var onKey = function(e) {
if (e.key === 'Enter') { document.removeEventListener('keydown', onKey); close(input.value); }
if (e.key === 'Escape') { document.removeEventListener('keydown', onKey); close(null); }
};
document.addEventListener('keydown', onKey);
// 自动聚焦输入框
setTimeout(function() { input.focus(); input.select(); }, 50);
});
}
// 全局头像图片加载失败回退(替代 onerror 内联事件,满足 CSP 无 unsafe-inline
document.addEventListener('error', function(e) {
var img = e.target;
if (img && img.tagName === 'IMG' && (img.classList.contains('nav-avatar') || img.classList.contains('user-avatar-img'))) {
img.style.display = 'none';
var next = img.nextElementSibling;
if (next) next.style.display = 'flex';
}
}, true);