From a5c2022422e602c410c81bf157ea21fad262f881 Mon Sep 17 00:00:00 2001 From: lkddi Date: Sat, 25 Apr 2026 13:54:00 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BF=81=E7=A7=BB=E8=81=8A=E5=A4=A9=E5=AE=A4?= =?UTF-8?q?=E5=A4=A7=E5=8D=A1=E7=89=87=E9=80=9A=E7=9F=A5=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resources/js/chat-room.js | 4 + resources/js/chat-room/banner.js | 214 ++++++++++++++++++ .../views/chat/partials/chat-banner.blade.php | 171 +------------- 3 files changed, 219 insertions(+), 170 deletions(-) create mode 100644 resources/js/chat-room/banner.js diff --git a/resources/js/chat-room.js b/resources/js/chat-room.js index cb075ad..d195cc5 100644 --- a/resources/js/chat-room.js +++ b/resources/js/chat-room.js @@ -2,6 +2,7 @@ // 统一转发各子模块导出,方便测试或后续模块继续复用同一组工具。 export { escapeHtml, escapeHtmlWithLineBreaks } from "./chat-room/html.js"; +export { bindChatBanner } from "./chat-room/banner.js"; export { bindGlobalDialogControls } from "./chat-room/dialog.js"; export { bindDailySignInControls } from "./chat-room/daily-sign-in.js"; export { applyFontSize, bindChatFontSizeControl, CHAT_FONT_SIZE_STORAGE_KEY, restoreChatFontSize } from "./chat-room/font-size.js"; @@ -78,6 +79,7 @@ export { export { createMessageQueue } from "./chat-room/message-queue.js"; import { escapeHtml, escapeHtmlWithLineBreaks } from "./chat-room/html.js"; +import { bindChatBanner } from "./chat-room/banner.js"; import { bindGlobalDialogControls } from "./chat-room/dialog.js"; import { bindDailySignInControls } from "./chat-room/daily-sign-in.js"; import { applyFontSize, bindChatFontSizeControl, CHAT_FONT_SIZE_STORAGE_KEY, restoreChatFontSize } from "./chat-room/font-size.js"; @@ -158,6 +160,7 @@ if (typeof window !== "undefined") { window.ChatRoomTools = { escapeHtml, escapeHtmlWithLineBreaks, + bindChatBanner, bindGlobalDialogControls, bindDailySignInControls, applyFontSize, @@ -269,6 +272,7 @@ if (typeof window !== "undefined") { window.applyFontSize = applyFontSize; // 页面加载后立即注册事件委托,具体业务逻辑仍由各子模块负责。 + bindChatBanner(); bindGlobalDialogControls(); bindDailySignInControls(); bindChatFontSizeControl(); diff --git a/resources/js/chat-room/banner.js b/resources/js/chat-room/banner.js new file mode 100644 index 0000000..ac78969 --- /dev/null +++ b/resources/js/chat-room/banner.js @@ -0,0 +1,214 @@ +// 聊天室居中大卡片通知组件,提供 window.chatBanner.show/close 兼容入口。 + +const DEFAULT_BANNER_ID = "chat-banner-default"; +const BANNER_KEYFRAMES_ID = "appoint-keyframes"; + +/** + * 将任意文本转为 HTML 安全文本。 + * + * @param {unknown} text + * @returns {string} + */ +function escapeBannerText(text) { + return String(text ?? "") + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +/** + * 将多行纯文本转为带换行的安全 HTML。 + * + * @param {unknown} text + * @returns {string} + */ +function renderMultilineText(text) { + return escapeBannerText(text).replace(/\n/g, "
"); +} + +/** + * 注入 Banner 入场与退场动画,避免重复创建 style 节点。 + * + * @returns {void} + */ +function ensureBannerKeyframes() { + if (document.getElementById(BANNER_KEYFRAMES_ID)) { + return; + } + + const style = document.createElement("style"); + style.id = BANNER_KEYFRAMES_ID; + style.textContent = ` + @keyframes appoint-pop { + 0% { opacity: 0; transform: translate(-50%,-50%) scale(0.5); } + 70% { transform: translate(-50%,-50%) scale(1.05); } + 100% { opacity: 1; transform: translate(-50%,-50%) scale(1); } + } + @keyframes appoint-fade-out { + from { opacity: 1; } + to { opacity: 0; transform: translate(-50%,-50%) scale(0.9); } + } + `; + document.head.appendChild(style); +} + +/** + * 播放大卡片退场动画,并在动画后移除节点。 + * + * @param {HTMLElement} banner + * @returns {void} + */ +function closeBannerElement(banner) { + banner.style.animation = "appoint-fade-out 0.5s ease forwards"; + window.setTimeout(() => banner.remove(), 500); +} + +/** + * 生成大卡片按钮 HTML。 + * + * @param {Array} buttons + * @returns {string} + */ +function renderButtons(buttons) { + if (buttons.length === 0) { + return ""; + } + + const buttonItems = buttons.map((button, index) => ` + + `).join(""); + + return `
${buttonItems}
`; +} + +/** + * 创建全局大卡片通知 API。 + * + * @returns {{show: Function, close: Function}} + */ +function createChatBanner() { + return { + /** + * 显示居中大卡片通知。 + * + * @param {object} options + * @param {string} [options.id] 同 ID 会替换旧弹窗 + * @param {string} [options.icon] Emoji 图标 + * @param {string} [options.title] 小标题 + * @param {string} [options.name] 大名字行 + * @param {string} [options.body] 主内容纯文本 + * @param {string} [options.sub] 副内容纯文本 + * @param {string[]} [options.gradient] 渐变颜色 + * @param {string} [options.titleColor] 小标题颜色 + * @param {number} [options.autoClose] 自动关闭毫秒,0 表示不自动关闭 + * @param {Array<{label: string, color?: string, onClick?: Function}>} [options.buttons] 操作按钮 + * @returns {void} + */ + show(options = {}) { + ensureBannerKeyframes(); + window.chatSound?.ding?.(); + + const id = options.id || DEFAULT_BANNER_ID; + const gradient = (options.gradient || ["#4f46e5", "#7c3aed", "#db2777"]).join(", "); + const titleColor = options.titleColor || "#fde68a"; + const autoClose = options.autoClose ?? 5000; + const buttons = Array.isArray(options.buttons) ? options.buttons : []; + const hasButtons = buttons.length > 0; + + // 同 ID 弹窗只保留最新一条,避免任命、红包等通知堆叠遮挡。 + document.getElementById(id)?.remove(); + + const banner = document.createElement("div"); + banner.id = id; + banner.style.cssText = ` + position: fixed; top: 50%; left: 50%; transform: translate(-50%,-50%); + z-index: 99999; text-align: center; + animation: appoint-pop 0.5s cubic-bezier(0.175,0.885,0.32,1.275); + ${hasButtons ? "pointer-events: auto;" : "pointer-events: none;"} + `; + + banner.innerHTML = ` +
+ ${options.icon ? `
${escapeBannerText(options.icon)}
` : ""} + ${options.title ? `
+ ══ ${escapeBannerText(options.title)} ══ +
` : ""} + ${options.name ? `
+ ${escapeBannerText(options.name)} +
` : ""} + ${options.body ? `
${renderMultilineText(options.body)}
` : ""} + ${options.sub ? `
${renderMultilineText(options.sub)}
` : ""} + ${renderButtons(buttons)} +
+ ${new Date().toLocaleTimeString("zh-CN")} +
+
+ `; + + document.body.appendChild(banner); + + buttons.forEach((button, index) => { + const element = banner.querySelector(`[data-banner-btn="${index}"]`); + if (!element) { + return; + } + + element.addEventListener("click", () => { + if (typeof button.onClick === "function") { + button.onClick(element, () => closeBannerElement(banner)); + return; + } + + closeBannerElement(banner); + }); + }); + + if (autoClose > 0) { + window.setTimeout(() => { + if (document.getElementById(id)) { + closeBannerElement(banner); + } + }, autoClose); + } + }, + + /** + * 关闭指定 ID 的大卡片通知。 + * + * @param {string} [id] + * @returns {void} + */ + close(id) { + const banner = document.getElementById(id || DEFAULT_BANNER_ID); + if (!banner) { + return; + } + + closeBannerElement(banner); + }, + }; +} + +/** + * 绑定全局大卡片通知 API。 + * + * @returns {void} + */ +export function bindChatBanner() { + if (typeof document === "undefined" || window.chatBanner) { + return; + } + + window.chatBanner = createChatBanner(); +} diff --git a/resources/views/chat/partials/chat-banner.blade.php b/resources/views/chat/partials/chat-banner.blade.php index fb32712..9153563 100644 --- a/resources/views/chat/partials/chat-banner.blade.php +++ b/resources/views/chat/partials/chat-banner.blade.php @@ -21,177 +21,8 @@ onClick(btn, close): Function }> - 从 scripts.blade.php 拆分,确保页面全局组件独立维护。 + 实际实现已迁移到 resources/js/chat-room/banner.js,由 resources/js/chat-room.js 统一通过 Vite 加载。 @author ChatRoom Laravel @version 1.0.0 --}} - -