feat: 错误监控面板、生成通知、社区搜索、任务队列优化

- AdminMonitor: admin用户可见的客户端错误实时监控面板,右下角浮窗
- generationNotifier: 生成完成浏览器通知 + 站内Toast
- CommunityPage: 新增搜索框,标题/描述/标签模糊匹配,防抖300ms
- App.tsx: 全局unhandled error/rejection监听上报
- WorkbenchPage: 任务并发提示改为显示当前任务数
- serverConnection: 后端client-errors路由注册
- WelcomeSplash: 欢迎按钮全程显示

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
2026-06-03 02:01:21 +08:00
parent 468d1d27dd
commit 4ed02aaad5
9 changed files with 411 additions and 6 deletions
+50
View File
@@ -0,0 +1,50 @@
/**
* Browser notification + in-app toast for generation task completions.
* Falls back gracefully when Notification API is unavailable.
*/
let permissionGranted = false;
async function requestPermission(): Promise<boolean> {
if (permissionGranted) return true;
if (typeof Notification === "undefined") return false;
if (Notification.permission === "granted") { permissionGranted = true; return true; }
if (Notification.permission === "denied") return false;
try {
const result = await Notification.requestPermission();
permissionGranted = result === "granted";
return permissionGranted;
} catch {
return false;
}
}
export function notifyTaskCompleted(label: string, mode: "image" | "video" = "image") {
const emoji = mode === "video" ? "🎬" : "🖼️";
const title = `${emoji} ${label}生成完成`;
const body = "点击返回查看生成结果";
// Browser notification (background tab)
if (typeof Notification !== "undefined" && Notification.permission === "granted") {
try { new Notification(title, { body, icon: "/favicon.ico", tag: "gen-complete" }); } catch { /* */ }
}
// In-app toast
dispatchGenToast(title);
}
// Use the existing toast system for in-app notifications
function dispatchGenToast(msg: string) {
try {
import("../components/toast/toastStore").then((m) => m.toast(msg, "success"));
} catch { /* toast system not loaded */ }
}
/** Call once on app init to pre-warm permission. */
export async function initNotificationPermission() {
if (typeof Notification === "undefined") return;
if (Notification.permission === "default") {
// Don't prompt immediately — wait for first user interaction
document.addEventListener("click", () => requestPermission(), { once: true });
}
}