// 聊天室居中大卡片通知组件,提供 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(); }