Initial commit: MyBlog full stack blog
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
/* =====================================================================
|
||||
* 通用工具(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) => ({
|
||||
"&": "&", "<": "<", ">": ">", '"': """, "'": "'",
|
||||
}[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 };
|
||||
}
|
||||
Reference in New Issue
Block a user