2026-03-09 11:30:11 +08:00
|
|
|
|
{{--
|
|
|
|
|
|
文件功能:全局大卡片通知组件(chatBanner)
|
|
|
|
|
|
|
|
|
|
|
|
提供全局 JS API:
|
|
|
|
|
|
window.chatBanner.show(opts) → 显示居中大卡片
|
|
|
|
|
|
window.chatBanner.close(id?) → 关闭指定卡片
|
|
|
|
|
|
|
|
|
|
|
|
opts 参数:
|
|
|
|
|
|
id: string (可选) 同 ID 会替换旧弹窗,防止重叠
|
|
|
|
|
|
icon: string Emoji 图标(如 '🎉💚🎉')
|
|
|
|
|
|
title: string 小标题(如 '好友通知')
|
|
|
|
|
|
name: string 大名字行(可留空)
|
|
|
|
|
|
body: string 主内容(支持 HTML)
|
|
|
|
|
|
sub: string 副内容(小字)
|
|
|
|
|
|
gradient: string[] 渐变颜色数组(3个颜色)
|
|
|
|
|
|
titleColor: string 小标题颜色
|
|
|
|
|
|
autoClose: number 自动关闭 ms,0=不关闭,默认 5000
|
|
|
|
|
|
buttons: Array<{
|
|
|
|
|
|
label: string
|
|
|
|
|
|
color: string
|
|
|
|
|
|
onClick(btn, close): Function
|
|
|
|
|
|
}>
|
|
|
|
|
|
|
|
|
|
|
|
从 scripts.blade.php 拆分,确保页面全局组件独立维护。
|
|
|
|
|
|
|
|
|
|
|
|
@author ChatRoom Laravel
|
|
|
|
|
|
@version 1.0.0
|
|
|
|
|
|
--}}
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 全局大卡片通知公共组件。
|
|
|
|
|
|
* 对标 window.chatDialog 和 window.chatToast 的设计风格,
|
|
|
|
|
|
* 用于展示任命公告、好友通知、红包选择等需要居中展示的大卡片。
|
|
|
|
|
|
*/
|
|
|
|
|
|
window.chatBanner = (function() {
|
2026-04-19 14:42:42 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 将任意文本转为 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>');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 11:30:11 +08:00
|
|
|
|
/** 注入入场/退场动画(全局只注入一次) */
|
|
|
|
|
|
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);">
|
2026-04-19 14:42:42 +08:00
|
|
|
|
${escapeBannerText(btn.label || '确定')}
|
2026-03-09 11:30:11 +08:00
|
|
|
|
</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;">
|
2026-04-19 14:42:42 +08:00
|
|
|
|
${opts.icon ? `<div style="font-size:40px; margin-bottom:8px;">${escapeBannerText(opts.icon)}</div>` : ''}
|
2026-03-09 11:30:11 +08:00
|
|
|
|
${opts.title ? `<div style="color:${titleColor}; font-size:13px; font-weight:bold; letter-spacing:3px; margin-bottom:12px;">
|
2026-04-19 14:42:42 +08:00
|
|
|
|
══ ${escapeBannerText(opts.title)} ══
|
2026-03-09 11:30:11 +08:00
|
|
|
|
</div>` : ''}
|
|
|
|
|
|
${opts.name ? `<div style="color:white; font-size:22px; font-weight:900; text-shadow:0 2px 8px rgba(0,0,0,0.3);">
|
2026-04-19 14:42:42 +08:00
|
|
|
|
${escapeBannerText(opts.name)}
|
2026-03-09 11:30:11 +08:00
|
|
|
|
</div>` : ''}
|
2026-04-19 14:42:42 +08:00
|
|
|
|
${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>` : ''}
|
2026-03-09 11:30:11 +08:00
|
|
|
|
${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>
|