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 charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>PLC Control</title>
|
<title>PLC Control</title>
|
||||||
<link rel="stylesheet" href="/ui/styles.css?v=20260325e" />
|
<link rel="stylesheet" href="/ui/styles.css?v=20260325f" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div data-partial="/ui/html/topbar.html"></div>
|
<div data-partial="/ui/html/topbar.html"></div>
|
||||||
|
|
@ -19,10 +19,9 @@
|
||||||
<div data-partial="/ui/html/chart-panel.html"></div>
|
<div data-partial="/ui/html/chart-panel.html"></div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<div id="toastContainer"></div>
|
|
||||||
<div data-partial="/ui/html/modals.html"></div>
|
<div data-partial="/ui/html/modals.html"></div>
|
||||||
<div data-partial="/ui/html/api-doc-drawer.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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -25,12 +25,14 @@ const ICONS = { error: "✕", warning: "!", success: "✓", info: "i" };
|
||||||
* @param {string} [opts.message] 次要说明文字
|
* @param {string} [opts.message] 次要说明文字
|
||||||
* @param {"error"|"warning"|"success"|"info"} [opts.level="error"]
|
* @param {"error"|"warning"|"success"|"info"} [opts.level="error"]
|
||||||
* @param {number} [opts.duration=4000] 自动关闭毫秒数,0 表示不自动关闭
|
* @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 container = getContainer();
|
||||||
|
|
||||||
const el = document.createElement("div");
|
const el = document.createElement("div");
|
||||||
el.className = `toast ${level}`;
|
el.className = `toast ${level}${shake ? " shake" : ""}`;
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<span class="toast-icon">${ICONS[level] ?? "i"}</span>
|
<span class="toast-icon">${ICONS[level] ?? "i"}</span>
|
||||||
<div class="toast-body">
|
<div class="toast-body">
|
||||||
|
|
@ -39,6 +41,7 @@ export function showToast(title, { message, level = "error", duration = 4000 } =
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
const dismiss = () => {
|
const dismiss = () => {
|
||||||
|
if (!el.parentNode) return;
|
||||||
el.classList.add("hiding");
|
el.classList.add("hiding");
|
||||||
el.addEventListener("animationend", () => el.remove(), { once: true });
|
el.addEventListener("animationend", () => el.remove(), { once: true });
|
||||||
};
|
};
|
||||||
|
|
@ -47,7 +50,7 @@ export function showToast(title, { message, level = "error", duration = 4000 } =
|
||||||
container.appendChild(el);
|
container.appendChild(el);
|
||||||
|
|
||||||
if (duration > 0) setTimeout(dismiss, duration);
|
if (duration > 0) setTimeout(dismiss, duration);
|
||||||
return dismiss;
|
return { dismiss };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── apiFetch ──────────────────────────────────────
|
// ── apiFetch ──────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { prependEvent } from "./events.js";
|
||||||
import { formatValue } from "./points.js";
|
import { formatValue } from "./points.js";
|
||||||
import { state } from "./state.js";
|
import { state } from "./state.js";
|
||||||
import { renderUnits } from "./units.js";
|
import { renderUnits } from "./units.js";
|
||||||
import { showToast } from "./toast.js";
|
import { showToast } from "./api.js";
|
||||||
|
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
||||||
|
|
@ -66,11 +66,16 @@ function setWsStatus(connected) {
|
||||||
dom.wsLabel.textContent = connected ? "已连接" : "连接断开,重连中…";
|
dom.wsLabel.textContent = connected ? "已连接" : "连接断开,重连中…";
|
||||||
}
|
}
|
||||||
if (!connected && !_disconnectToast) {
|
if (!connected && !_disconnectToast) {
|
||||||
_disconnectToast = showToast("后端连接断开,正在重连…", "error", 0);
|
_disconnectToast = showToast("后端连接断开", {
|
||||||
|
message: "正在重连,请稍候…",
|
||||||
|
level: "error",
|
||||||
|
duration: 0,
|
||||||
|
shake: true,
|
||||||
|
});
|
||||||
} else if (connected && _disconnectToast) {
|
} else if (connected && _disconnectToast) {
|
||||||
_disconnectToast.dismiss();
|
_disconnectToast.dismiss();
|
||||||
_disconnectToast = null;
|
_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-message { color: var(--text-2); font-size: 12px; }
|
||||||
|
|
||||||
.toast.hiding { animation: toast-out 0.15s ease forwards; }
|
.toast.hiding { animation: toast-out 0.15s ease forwards; }
|
||||||
|
.toast.shake { animation: toast-shake 0.4s ease; }
|
||||||
|
|
||||||
@keyframes toast-in {
|
@keyframes toast-in {
|
||||||
from { opacity: 0; transform: translateY(8px); }
|
from { opacity: 0; transform: translateY(8px); }
|
||||||
|
|
@ -1279,6 +1280,13 @@ button.danger:hover { background: var(--danger-hover); }
|
||||||
from { opacity: 1; transform: translateY(0); }
|
from { opacity: 1; transform: translateY(0); }
|
||||||
to { opacity: 0; transform: translateY(8px); }
|
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 ────────────────────────────────────── */
|
/* ── Scrollbar ────────────────────────────────────── */
|
||||||
|
|
||||||
|
|
@ -1311,54 +1319,4 @@ button.danger:hover { background: var(--danger-hover); }
|
||||||
.doc-card { border-left: none; border-right: none; }
|
.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