Files

83 lines
3.0 KiB
JavaScript
Raw Permalink 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.
/* =====================================================================
* 通用工具(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 };
}