Files

83 lines
3.0 KiB
JavaScript
Raw Permalink Normal View History

2026-08-22 22:28:41 +08:00
/* =====================================================================
* 通用工具(utils.js
* DOM 查询 / HTML 转义 / URL 白名单 / 日期格式化 / Toast / 自定义弹窗
* ===================================================================== */
export function $(selector, root = document) {
return root.querySelector(selector);
}
export function escapeHtml(value) {
return String(value ?? "").replace(/[&<>"']/g, (c) => ({
"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;",
}[c]));
}
// URL 协议白名单:仅允许 http/https、站内相对路径与锚点,
// 禁止 javascript: / data: / vbscript: 等危险协议,防止 XSS。
export function safeUrl(url, type = "link") {
const value = String(url ?? "").trim();
if (!value) return "";
if (/^(https?:)?\/\//i.test(value)) return value;
if (value.startsWith("/") || value.startsWith("#")) return value;
if (type === "link" && value.startsWith("mailto:")) return value;
return "";
}
export function parseDate(value) {
if (!value) return null;
let text = String(value).replace(" ", "T");
// 后端时间为无时区 UTC,补 Z 后按 UTC 解析并转为本地时区显示
if (!/[zZ]|[+-]\d{2}:\d{2}$/.test(text)) text += "Z";
const d = new Date(text);
return Number.isNaN(d.getTime()) ? null : d;
}
export function formatDate(value) {
const d = parseDate(value);
if (!d) return "";
const p = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
}
/* ---------------- 通知(自定义 Snackbar,不使用 alert ---------------- */
export function showToast(message, type = "info") {
const el = document.createElement("div");
el.className = `toast toast-${type}`;
el.textContent = message;
$("#toast-root").appendChild(el);
requestAnimationFrame(() => el.classList.add("show"));
setTimeout(() => {
el.classList.remove("show");
setTimeout(() => el.remove(), 300);
}, 3200);
}
/* ---------------- 自定义弹窗 ---------------- */
export function openModal({ title, content }) {
const overlay = document.createElement("div");
overlay.className = "modal-overlay";
overlay.innerHTML = `
<div class="modal" role="dialog" aria-modal="true">
<div class="modal-header">
<h3></h3>
<button class="modal-close" aria-label="关闭">✕</button>
</div>
<div class="modal-body"></div>
</div>`;
$(".modal h3", overlay).textContent = title;
const bodyEl = $(".modal-body", overlay);
if (typeof content === "string") bodyEl.innerHTML = content;
else bodyEl.appendChild(content);
const close = () => {
overlay.remove();
document.removeEventListener("keydown", onKey);
};
const onKey = (e) => { if (e.key === "Escape") close(); };
document.addEventListener("keydown", onKey);
$(".modal-close", overlay).addEventListener("click", close);
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
$("#modal-root").appendChild(overlay);
return { overlay, close };
}