迁移聊天室大卡片通知脚本
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
/**
|
||||
* 将多行纯文本转为带换行的安全 HTML。
|
||||
*
|
||||
* @param {unknown} text
|
||||
* @returns {string}
|
||||
*/
|
||||
function renderMultilineText(text) {
|
||||
return escapeBannerText(text).replace(/\n/g, "<br>");
|
||||
}
|
||||
|
||||
/**
|
||||
* 注入 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<object>} buttons
|
||||
* @returns {string}
|
||||
*/
|
||||
function renderButtons(buttons) {
|
||||
if (buttons.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const buttonItems = buttons.map((button, index) => `
|
||||
<button data-banner-btn="${index}"
|
||||
style="background:${button.color || "#10b981"}; color:#fff; border:none; border-radius:8px;
|
||||
padding:8px 20px; font-size:13px; font-weight:bold; cursor:pointer;
|
||||
box-shadow:0 4px 12px rgba(0,0,0,0.25);">
|
||||
${escapeBannerText(button.label || "确定")}
|
||||
</button>
|
||||
`).join("");
|
||||
|
||||
return `<div style="margin-top:18px; display:flex; gap:10px; justify-content:center;">${buttonItems}</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建全局大卡片通知 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 = `
|
||||
<div style="background: linear-gradient(135deg, ${gradient});
|
||||
border-radius: 20px; padding: 28px 44px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
|
||||
border: 2px solid rgba(255,255,255,0.25); backdrop-filter: blur(8px);
|
||||
min-width: 260px;">
|
||||
${options.icon ? `<div style="font-size:40px; margin-bottom:8px;">${escapeBannerText(options.icon)}</div>` : ""}
|
||||
${options.title ? `<div style="color:${titleColor}; font-size:13px; font-weight:bold; letter-spacing:3px; margin-bottom:12px;">
|
||||
══ ${escapeBannerText(options.title)} ══
|
||||
</div>` : ""}
|
||||
${options.name ? `<div style="color:white; font-size:22px; font-weight:900; text-shadow:0 2px 8px rgba(0,0,0,0.3);">
|
||||
${escapeBannerText(options.name)}
|
||||
</div>` : ""}
|
||||
${options.body ? `<div style="color:rgba(255,255,255,0.9); font-size:14px; margin-top:10px;">${renderMultilineText(options.body)}</div>` : ""}
|
||||
${options.sub ? `<div style="color:rgba(255,255,255,0.6); font-size:12px; margin-top:6px;">${renderMultilineText(options.sub)}</div>` : ""}
|
||||
${renderButtons(buttons)}
|
||||
<div style="color:rgba(255,255,255,0.35); font-size:11px; margin-top:14px;">
|
||||
${new Date().toLocaleTimeString("zh-CN")}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -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
|
||||
--}}
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 全局大卡片通知公共组件。
|
||||
* 对标 window.chatDialog 和 window.chatToast 的设计风格,
|
||||
* 用于展示任命公告、好友通知、红包选择等需要居中展示的大卡片。
|
||||
*/
|
||||
window.chatBanner = (function() {
|
||||
/**
|
||||
* 将任意文本转为 HTML 安全文本。
|
||||
*/
|
||||
function escapeBannerText(text) {
|
||||
return String(text ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
/**
|
||||
* 将多行纯文本转为带 <br> 的安全 HTML。
|
||||
*/
|
||||
function renderMultilineText(text) {
|
||||
return escapeBannerText(text).replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
/** 注入入场/退场动画(全局只注入一次) */
|
||||
function ensureKeyframes() {
|
||||
if (document.getElementById('appoint-keyframes')) {
|
||||
return;
|
||||
}
|
||||
const style = document.createElement('style');
|
||||
style.id = 'appoint-keyframes';
|
||||
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 {Object} opts 选项(见上方注释)
|
||||
*/
|
||||
function show(opts = {}) {
|
||||
ensureKeyframes();
|
||||
|
||||
// 大卡片弹出时播放叮咚通知音
|
||||
if (window.chatSound) {
|
||||
window.chatSound.ding();
|
||||
}
|
||||
|
||||
const id = opts.id || 'chat-banner-default';
|
||||
const gradient = (opts.gradient || ['#4f46e5', '#7c3aed', '#db2777']).join(', ');
|
||||
const titleColor = opts.titleColor || '#fde68a';
|
||||
const autoClose = opts.autoClose ?? 5000;
|
||||
|
||||
// 移除同 ID 的旧弹窗,避免重叠
|
||||
const old = document.getElementById(id);
|
||||
if (old) {
|
||||
old.remove();
|
||||
}
|
||||
|
||||
// 构建按钮 HTML
|
||||
const hasButtons = opts.buttons && opts.buttons.length > 0;
|
||||
let buttonsHtml = '';
|
||||
if (hasButtons) {
|
||||
buttonsHtml = '<div style="margin-top:18px; display:flex; gap:10px; justify-content:center;">';
|
||||
opts.buttons.forEach((btn, idx) => {
|
||||
buttonsHtml += `<button data-banner-btn="${idx}"
|
||||
style="background:${btn.color || '#10b981'}; color:#fff; border:none; border-radius:8px;
|
||||
padding:8px 20px; font-size:13px; font-weight:bold; cursor:pointer;
|
||||
box-shadow:0 4px 12px rgba(0,0,0,0.25);">
|
||||
${escapeBannerText(btn.label || '确定')}
|
||||
</button>`;
|
||||
});
|
||||
buttonsHtml += '</div>';
|
||||
}
|
||||
|
||||
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 = `
|
||||
<div style="background: linear-gradient(135deg, ${gradient});
|
||||
border-radius: 20px; padding: 28px 44px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
|
||||
border: 2px solid rgba(255,255,255,0.25); backdrop-filter: blur(8px);
|
||||
min-width: 260px;">
|
||||
${opts.icon ? `<div style="font-size:40px; margin-bottom:8px;">${escapeBannerText(opts.icon)}</div>` : ''}
|
||||
${opts.title ? `<div style="color:${titleColor}; font-size:13px; font-weight:bold; letter-spacing:3px; margin-bottom:12px;">
|
||||
══ ${escapeBannerText(opts.title)} ══
|
||||
</div>` : ''}
|
||||
${opts.name ? `<div style="color:white; font-size:22px; font-weight:900; text-shadow:0 2px 8px rgba(0,0,0,0.3);">
|
||||
${escapeBannerText(opts.name)}
|
||||
</div>` : ''}
|
||||
${opts.body ? `<div style="color:rgba(255,255,255,0.9); font-size:14px; margin-top:10px;">${renderMultilineText(opts.body)}</div>` : ''}
|
||||
${opts.sub ? `<div style="color:rgba(255,255,255,0.6); font-size:12px; margin-top:6px;">${renderMultilineText(opts.sub)}</div>` : ''}
|
||||
${buttonsHtml}
|
||||
<div style="color:rgba(255,255,255,0.35); font-size:11px; margin-top:14px;">
|
||||
${new Date().toLocaleTimeString('zh-CN')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(banner);
|
||||
|
||||
// 绑定按钮点击事件
|
||||
if (hasButtons) {
|
||||
const closeFn = () => {
|
||||
banner.style.animation = 'appoint-fade-out 0.5s ease forwards';
|
||||
setTimeout(() => banner.remove(), 500);
|
||||
};
|
||||
opts.buttons.forEach((btn, idx) => {
|
||||
const el = banner.querySelector(`[data-banner-btn="${idx}"]`);
|
||||
if (el && btn.onClick) {
|
||||
el.addEventListener('click', () => btn.onClick(el, closeFn));
|
||||
} else if (el) {
|
||||
el.addEventListener('click', closeFn);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 自动关闭
|
||||
if (autoClose > 0) {
|
||||
setTimeout(() => {
|
||||
if (!document.getElementById(id)) {
|
||||
return;
|
||||
}
|
||||
banner.style.animation = 'appoint-fade-out 0.5s ease forwards';
|
||||
setTimeout(() => banner.remove(), 500);
|
||||
}, autoClose);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭指定 ID 的 banner。
|
||||
*
|
||||
* @param {string} id banner 的 DOM ID
|
||||
*/
|
||||
function close(id) {
|
||||
const el = document.getElementById(id || 'chat-banner-default');
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
el.style.animation = 'appoint-fade-out 0.5s ease forwards';
|
||||
setTimeout(() => el.remove(), 500);
|
||||
}
|
||||
|
||||
return {
|
||||
show,
|
||||
close
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user