88 lines
2.6 KiB
JavaScript
88 lines
2.6 KiB
JavaScript
import { dom } from "./dom.js";
|
||
|
||
export function setStatus(text) {
|
||
dom.statusText.textContent = text;
|
||
}
|
||
|
||
// ── Toast ─────────────────────────────────────────
|
||
|
||
function getContainer() {
|
||
let el = document.getElementById("toast-container");
|
||
if (!el) {
|
||
el = document.createElement("div");
|
||
el.id = "toast-container";
|
||
document.body.appendChild(el);
|
||
}
|
||
return el;
|
||
}
|
||
|
||
const ICONS = { error: "✕", warning: "!", success: "✓", info: "i" };
|
||
|
||
/**
|
||
* 显示 toast 通知。
|
||
* @param {string} title 主要文字
|
||
* @param {object} [opts]
|
||
* @param {string} [opts.message] 次要说明文字
|
||
* @param {"error"|"warning"|"success"|"info"} [opts.level="error"]
|
||
* @param {number} [opts.duration=4000] 自动关闭毫秒数,0 表示不自动关闭
|
||
* @param {boolean} [opts.shake=false] 出现时加抖动动画
|
||
* @returns {{ dismiss: () => void }}
|
||
*/
|
||
export function showToast(title, { message, level = "error", duration = 4000, shake = false } = {}) {
|
||
const container = getContainer();
|
||
|
||
const el = document.createElement("div");
|
||
el.className = `toast ${level}${shake ? " shake" : ""}`;
|
||
el.innerHTML = `
|
||
<span class="toast-icon">${ICONS[level] ?? "i"}</span>
|
||
<div class="toast-body">
|
||
<div class="toast-title">${title}</div>
|
||
${message ? `<div class="toast-message">${message}</div>` : ""}
|
||
</div>`;
|
||
|
||
const dismiss = () => {
|
||
if (!el.parentNode) return;
|
||
el.classList.remove("shake");
|
||
el.classList.add("hiding");
|
||
el.addEventListener("animationend", () => el.remove(), { once: true });
|
||
};
|
||
|
||
el.addEventListener("click", dismiss);
|
||
container.appendChild(el);
|
||
|
||
if (duration > 0) setTimeout(dismiss, duration);
|
||
return { dismiss };
|
||
}
|
||
|
||
// ── apiFetch ──────────────────────────────────────
|
||
|
||
export async function apiFetch(url, options = {}) {
|
||
const response = await fetch(url, {
|
||
headers: { "Content-Type": "application/json" },
|
||
...options,
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const text = (await response.text()) || response.statusText;
|
||
showToast(`请求失败 ${response.status}`, { message: text });
|
||
throw new Error(text);
|
||
}
|
||
|
||
if (response.status === 204) {
|
||
return null;
|
||
}
|
||
|
||
const contentType = response.headers.get("content-type") || "";
|
||
if (contentType.includes("application/json")) {
|
||
return response.json();
|
||
}
|
||
|
||
return response.text();
|
||
}
|
||
|
||
export function withStatus(task) {
|
||
return task.catch((error) => {
|
||
setStatus(error.message || "请求失败");
|
||
});
|
||
}
|