refactor(web): unify toast — remove duplicate, reuse api.js showToast
- Deleted redundant toast.js (was duplicating api.js's toast with conflicting CSS)
- Extended api.js showToast: returns {dismiss}, supports shake option, guards
against double-dismiss
- Removed duplicate #toastContainer CSS block from styles.css; added shake
animation to existing toast CSS
- logs.js: import showToast from api.js; WS disconnect shows persistent error
toast with shake, reconnect shows success toast
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
69ae0b05b7
commit
b7d55fed81
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>PLC Control</title>
|
||||
<link rel="stylesheet" href="/ui/styles.css?v=20260325e" />
|
||||
<link rel="stylesheet" href="/ui/styles.css?v=20260325f" />
|
||||
</head>
|
||||
<body>
|
||||
<div data-partial="/ui/html/topbar.html"></div>
|
||||
|
|
@ -19,10 +19,9 @@
|
|||
<div data-partial="/ui/html/chart-panel.html"></div>
|
||||
</main>
|
||||
|
||||
<div id="toastContainer"></div>
|
||||
<div data-partial="/ui/html/modals.html"></div>
|
||||
<div data-partial="/ui/html/api-doc-drawer.html"></div>
|
||||
|
||||
<script type="module" src="/ui/js/index.js?v=20260325e"></script>
|
||||
<script type="module" src="/ui/js/index.js?v=20260325f"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -25,12 +25,14 @@ const ICONS = { error: "✕", warning: "!", success: "✓", info: "i" };
|
|||
* @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 } = {}) {
|
||||
export function showToast(title, { message, level = "error", duration = 4000, shake = false } = {}) {
|
||||
const container = getContainer();
|
||||
|
||||
const el = document.createElement("div");
|
||||
el.className = `toast ${level}`;
|
||||
el.className = `toast ${level}${shake ? " shake" : ""}`;
|
||||
el.innerHTML = `
|
||||
<span class="toast-icon">${ICONS[level] ?? "i"}</span>
|
||||
<div class="toast-body">
|
||||
|
|
@ -39,6 +41,7 @@ export function showToast(title, { message, level = "error", duration = 4000 } =
|
|||
</div>`;
|
||||
|
||||
const dismiss = () => {
|
||||
if (!el.parentNode) return;
|
||||
el.classList.add("hiding");
|
||||
el.addEventListener("animationend", () => el.remove(), { once: true });
|
||||
};
|
||||
|
|
@ -47,7 +50,7 @@ export function showToast(title, { message, level = "error", duration = 4000 } =
|
|||
container.appendChild(el);
|
||||
|
||||
if (duration > 0) setTimeout(dismiss, duration);
|
||||
return dismiss;
|
||||
return { dismiss };
|
||||
}
|
||||
|
||||
// ── apiFetch ──────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { prependEvent } from "./events.js";
|
|||
import { formatValue } from "./points.js";
|
||||
import { state } from "./state.js";
|
||||
import { renderUnits } from "./units.js";
|
||||
import { showToast } from "./toast.js";
|
||||
import { showToast } from "./api.js";
|
||||
|
||||
function escapeHtml(text) {
|
||||
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
||||
|
|
@ -66,11 +66,16 @@ function setWsStatus(connected) {
|
|||
dom.wsLabel.textContent = connected ? "已连接" : "连接断开,重连中…";
|
||||
}
|
||||
if (!connected && !_disconnectToast) {
|
||||
_disconnectToast = showToast("后端连接断开,正在重连…", "error", 0);
|
||||
_disconnectToast = showToast("后端连接断开", {
|
||||
message: "正在重连,请稍候…",
|
||||
level: "error",
|
||||
duration: 0,
|
||||
shake: true,
|
||||
});
|
||||
} else if (connected && _disconnectToast) {
|
||||
_disconnectToast.dismiss();
|
||||
_disconnectToast = null;
|
||||
showToast("连接已恢复", "success", 3000);
|
||||
showToast("连接已恢复", { level: "success", duration: 3000 });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
const container = document.getElementById("toastContainer");
|
||||
|
||||
/**
|
||||
* Show a toast notification.
|
||||
* @param {string} message
|
||||
* @param {"error"|"warning"|"success"|"info"} type
|
||||
* @param {number} duration ms before auto-dismiss (0 = persistent until manually dismissed)
|
||||
* @returns {{ dismiss: () => void }} call dismiss() to remove early
|
||||
*/
|
||||
export function showToast(message, type = "info", duration = 4000) {
|
||||
const el = document.createElement("div");
|
||||
el.className = `toast ${type} shake`;
|
||||
el.textContent = message;
|
||||
container.appendChild(el);
|
||||
|
||||
function dismiss() {
|
||||
if (!el.parentNode) return;
|
||||
el.classList.add("leaving");
|
||||
el.addEventListener("animationend", () => el.remove(), { once: true });
|
||||
}
|
||||
|
||||
let timer = duration > 0 ? setTimeout(dismiss, duration) : null;
|
||||
|
||||
el.addEventListener("click", () => {
|
||||
if (timer) clearTimeout(timer);
|
||||
dismiss();
|
||||
});
|
||||
|
||||
return { dismiss };
|
||||
}
|
||||
|
|
@ -1270,6 +1270,7 @@ button.danger:hover { background: var(--danger-hover); }
|
|||
.toast-message { color: var(--text-2); font-size: 12px; }
|
||||
|
||||
.toast.hiding { animation: toast-out 0.15s ease forwards; }
|
||||
.toast.shake { animation: toast-shake 0.4s ease; }
|
||||
|
||||
@keyframes toast-in {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
|
|
@ -1279,6 +1280,13 @@ button.danger:hover { background: var(--danger-hover); }
|
|||
from { opacity: 1; transform: translateY(0); }
|
||||
to { opacity: 0; transform: translateY(8px); }
|
||||
}
|
||||
@keyframes toast-shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
20% { transform: translateX(-5px); }
|
||||
40% { transform: translateX(5px); }
|
||||
60% { transform: translateX(-4px); }
|
||||
80% { transform: translateX(4px); }
|
||||
}
|
||||
|
||||
/* ── Scrollbar ────────────────────────────────────── */
|
||||
|
||||
|
|
@ -1311,54 +1319,4 @@ button.danger:hover { background: var(--danger-hover); }
|
|||
.doc-card { border-left: none; border-right: none; }
|
||||
}
|
||||
|
||||
/* ── Toast notifications ─────────────────────────── */
|
||||
|
||||
#toastContainer {
|
||||
position: fixed;
|
||||
top: calc(var(--topbar-h) + 8px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.toast {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
animation: toast-in 0.2s ease forwards;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.toast.error { background: var(--danger); }
|
||||
.toast.warning { background: var(--warning); }
|
||||
.toast.success { background: var(--success); }
|
||||
.toast.info { background: var(--accent); }
|
||||
.toast.shake { animation: toast-in 0.2s ease forwards, toast-shake 0.4s ease 0.2s; }
|
||||
.toast.leaving { animation: toast-out 0.2s ease forwards; }
|
||||
|
||||
@keyframes toast-in {
|
||||
from { opacity: 0; transform: translateY(-8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes toast-out {
|
||||
from { opacity: 1; transform: translateY(0); }
|
||||
to { opacity: 0; transform: translateY(-8px); }
|
||||
}
|
||||
@keyframes toast-shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
20% { transform: translateX(-5px); }
|
||||
40% { transform: translateX(5px); }
|
||||
60% { transform: translateX(-4px); }
|
||||
80% { transform: translateX(4px); }
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue