新增聊天室发送图片功能
This commit is contained in:
@@ -115,7 +115,8 @@
|
||||
claimEnvelopeUrl: (id, ceremonyId) => `/wedding/${id}/claim`,
|
||||
envelopeStatusUrl: (id) => `/wedding/${id}/envelope-status`,
|
||||
},
|
||||
earnRewardUrl: "{{ route('earn.video_reward') }}"
|
||||
earnRewardUrl: "{{ route('earn.video_reward') }}",
|
||||
chatImageRetentionDays: 3
|
||||
};
|
||||
</script>
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js', 'resources/js/chat.js'])
|
||||
@@ -174,6 +175,23 @@
|
||||
@include('chat.partials.global-dialog')
|
||||
{{-- Toast 轻提示 --}}
|
||||
@include('chat.partials.toast-notification')
|
||||
{{-- 聊天图片大图预览层 --}}
|
||||
<div id="chat-image-lightbox"
|
||||
style="display:none; position:fixed; inset:0; z-index:10020; background:rgba(15,23,42,.86); backdrop-filter:blur(4px);"
|
||||
onclick="closeChatImageLightbox(event)">
|
||||
<div
|
||||
style="position:absolute; inset:0; display:flex; align-items:center; justify-content:center; padding:32px;"
|
||||
onclick="closeChatImageLightbox(event)">
|
||||
<img id="chat-image-lightbox-img" src="" alt="聊天图片预览"
|
||||
onclick="event.stopPropagation()"
|
||||
style="max-width:92vw; max-height:86vh; border-radius:12px; box-shadow:0 18px 50px rgba(0,0,0,.45);">
|
||||
</div>
|
||||
<button type="button" onclick="closeChatImageLightbox(event)"
|
||||
style="position:absolute; top:20px; right:24px; z-index:10021; border:none; background:transparent; color:#fff; font-size:34px; cursor:pointer;">×</button>
|
||||
<div id="chat-image-lightbox-name"
|
||||
style="position:absolute; left:50%; bottom:24px; transform:translateX(-50%); z-index:10021; max-width:88vw; color:#e2e8f0; font-size:12px; text-align:center; word-break:break-all;">
|
||||
</div>
|
||||
</div>
|
||||
{{-- 大卡片通知(任命公告、好友通知、礼包选择等) --}}
|
||||
@include('chat.partials.chat-banner')
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
--}}
|
||||
|
||||
<div class="input-bar">
|
||||
<form id="chat-form" onsubmit="sendMessage(event)">
|
||||
<form id="chat-form" onsubmit="sendMessage(event)" enctype="multipart/form-data">
|
||||
{{-- 第一行:工具选项 --}}
|
||||
<div class="input-row">
|
||||
<label>对
|
||||
@@ -81,18 +81,18 @@
|
||||
</button>
|
||||
<div id="welcome-menu" class="welcome-menu" style="display:none;">
|
||||
@php
|
||||
$welcomeMessages = [
|
||||
'欢迎【{name}】来到我们的聊天室,请遵守规则,文明聊天!',
|
||||
'【{name}】,你好!欢迎来访,有什么问题随时告诉我们!',
|
||||
'热烈欢迎【{name}】加入,愿您在这里度过愉快的时光!',
|
||||
'欢迎新朋友【{name}】!请先阅读公告,了解聊天室规则哦~',
|
||||
'【{name}】来了!欢迎欢迎,希望你在这里玩得开心!',
|
||||
'亲爱的【{name}】,欢迎光临本聊天室,请保持文明礼貌!',
|
||||
'欢迎【{name}】入驻!有问题请联系管理员,我们随时为您服务!',
|
||||
'【{name}】,初来乍到,欢迎多多关照,我们是一家人!',
|
||||
'大家欢迎新成员【{name}】!请遵守群规,共建和谐聊天环境!',
|
||||
'欢迎【{name}】莅临指导!希望你常来,让我们一起聊天!',
|
||||
];
|
||||
$welcomeMessages = [
|
||||
'欢迎【{name}】来到我们的聊天室,请遵守规则,文明聊天!',
|
||||
'【{name}】,你好!欢迎来访,有什么问题随时告诉我们!',
|
||||
'热烈欢迎【{name}】加入,愿您在这里度过愉快的时光!',
|
||||
'欢迎新朋友【{name}】!请先阅读公告,了解聊天室规则哦~',
|
||||
'【{name}】来了!欢迎欢迎,希望你在这里玩得开心!',
|
||||
'亲爱的【{name}】,欢迎光临本聊天室,请保持文明礼貌!',
|
||||
'欢迎【{name}】入驻!有问题请联系管理员,我们随时为您服务!',
|
||||
'【{name}】,初来乍到,欢迎多多关照,我们是一家人!',
|
||||
'大家欢迎新成员【{name}】!请遵守群规,共建和谐聊天环境!',
|
||||
'欢迎【{name}】莅临指导!希望你常来,让我们一起聊天!',
|
||||
];
|
||||
@endphp
|
||||
@foreach ($welcomeMessages as $msg)
|
||||
<div class="welcome-menu-item" onclick="sendWelcomeTpl({{ json_encode($msg) }})">
|
||||
@@ -102,8 +102,9 @@
|
||||
</div>
|
||||
|
||||
@if (
|
||||
$user->user_level >= (int) \App\Models\Sysparam::getValue('level_announcement', '10') ||
|
||||
$room->master == $user->username)
|
||||
$user->user_level >= (int) \App\Models\Sysparam::getValue('level_announcement', '10') ||
|
||||
$room->master == $user->username
|
||||
)
|
||||
<button type="button" onclick="promptAnnouncement()"
|
||||
style="font-size: 11px; padding: 1px 6px; background: #4a9; color: #fff; border: none; border-radius: 2px; cursor: pointer;">设公告</button>
|
||||
@endif
|
||||
@@ -112,6 +113,13 @@
|
||||
style="font-size: 11px; padding: 1px 6px; background: #2563eb; color: #fff; border: none; border-radius: 2px; cursor: pointer;">🎣
|
||||
钓鱼</button>
|
||||
|
||||
<input type="file" id="chat_image" name="image" accept="image/jpeg,image/png,image/gif,image/webp" style="display:none;"
|
||||
onchange="handleChatImageSelected(this)">
|
||||
<button type="button" onclick="document.getElementById('chat_image')?.click()"
|
||||
style="font-size: 11px; padding: 3px 8px; background: #0f766e; color: #fff; border: none; border-radius: 3px; cursor: pointer;">
|
||||
📷 图片
|
||||
</button>
|
||||
|
||||
@if ($user->user_level >= (int) \App\Models\Sysparam::getValue('superlevel', '100'))
|
||||
<button type="button" onclick="promptAnnounceMessage()"
|
||||
style="font-size: 11px; padding: 1px 6px; background: #7c3aed; color: #fff; border: none; border-radius: 2px; cursor: pointer;">📢
|
||||
@@ -147,8 +155,10 @@
|
||||
|
||||
{{-- 第二行:输入框 + 发送 --}}
|
||||
<div class="input-row">
|
||||
|
||||
|
||||
<input type="text" id="content" name="content" class="say-input"
|
||||
placeholder="在这里输入聊天内容,按 Enter 发送..." autocomplete="off">
|
||||
placeholder="在这里输入聊天内容或发送图片,按 Enter 发送..." autocomplete="off">
|
||||
<button type="submit" id="send-btn" class="send-btn">发送</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -151,6 +151,80 @@
|
||||
|
||||
window.showVipPresenceBanner = showVipPresenceBanner;
|
||||
|
||||
/**
|
||||
* 判断图片消息是否已经超过前端允许展示的保留期。
|
||||
*/
|
||||
function isExpiredChatImageMessage(msg) {
|
||||
if (!msg) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (msg.message_type === 'expired_image') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (msg.message_type !== 'image') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!msg.image_url || !msg.image_thumb_url) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const retentionDays = parseInt(window.chatContext?.chatImageRetentionDays || 3, 10);
|
||||
const sentAtText = String(msg.sent_at || '').replace(' ', 'T');
|
||||
const sentAt = sentAtText ? new Date(sentAtText) : null;
|
||||
|
||||
if (!sentAt || Number.isNaN(sentAt.getTime())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Date.now() >= sentAt.getTime() + retentionDays * 24 * 60 * 60 * 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建普通聊天消息的正文区域,支持缩略图与过期占位渲染。
|
||||
*/
|
||||
function buildChatMessageContent(msg, fontColor) {
|
||||
const rawContent = msg.content || '';
|
||||
|
||||
if (msg.message_type === 'image' && !isExpiredChatImageMessage(msg)) {
|
||||
const fullUrl = escapeHtml(msg.image_url || '');
|
||||
const thumbUrl = escapeHtml(msg.image_thumb_url || '');
|
||||
const imageName = escapeHtml(msg.image_original_name || '聊天图片');
|
||||
const captionHtml = rawContent ?
|
||||
`<span style="display:inline-block; max-width:220px; color:${fontColor}; line-height:1.55;">${rawContent}</span>` :
|
||||
'';
|
||||
|
||||
return `
|
||||
<span style="display:inline-flex; align-items:flex-start; gap:6px; vertical-align:middle;">
|
||||
<a href="${fullUrl}" data-full="${fullUrl}" data-alt="${imageName}"
|
||||
onclick="openChatImageLightbox(this.dataset.full, this.dataset.alt); return false;"
|
||||
style="display:inline-block; border:1px solid rgba(15,23,42,.14); border-radius:10px; overflow:hidden; background:#f8fafc; box-shadow:0 2px 10px rgba(15,23,42,.10);">
|
||||
<img src="${thumbUrl}" alt="${imageName}"
|
||||
style="display:block; max-width:96px; max-height:96px; object-fit:cover; cursor:zoom-in;">
|
||||
</a>
|
||||
${captionHtml}
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
if (msg.message_type === 'expired_image' || isExpiredChatImageMessage(msg)) {
|
||||
const captionHtml = rawContent ?
|
||||
`<span style="display:inline-block; color:${fontColor}; line-height:1.55;">${rawContent}</span>` :
|
||||
'';
|
||||
|
||||
return `
|
||||
<span style="display:inline-flex; align-items:center; gap:6px; vertical-align:middle;">
|
||||
<span style="display:inline-flex; align-items:center; padding:4px 8px; border:1px dashed #94a3b8; border-radius:999px; background:#f8fafc; color:#64748b; font-size:12px;">🖼️ 图片已过期</span>
|
||||
${captionHtml}
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
return rawContent;
|
||||
}
|
||||
|
||||
// ── Tab 切换 ──────────────────────────────────────
|
||||
let _roomsRefreshTimer = null;
|
||||
|
||||
@@ -606,6 +680,7 @@
|
||||
}
|
||||
const headImg =
|
||||
`<img src="${headImgSrc}" style="display:inline;width:16px;height:16px;vertical-align:middle;margin-right:2px;mix-blend-mode: multiply;" onerror="this.src='/images/headface/1.gif'">`;
|
||||
const messageBodyHtml = buildChatMessageContent(msg, fontColor);
|
||||
|
||||
let html = '';
|
||||
|
||||
@@ -726,7 +801,7 @@
|
||||
buildActionStr(msg.action, fromHtml, toHtml, '悄悄说') :
|
||||
`${fromHtml}对${toHtml}悄悄说:`;
|
||||
html =
|
||||
`${headImg}<span class="msg-secret">${verbStr}</span><span class="msg-content" style="color: ${fontColor}; font-style: italic;">${msg.content}</span>`;
|
||||
`${headImg}<span class="msg-secret">${verbStr}</span><span class="msg-content" style="color: ${fontColor}; font-style: italic;">${messageBodyHtml}</span>`;
|
||||
}
|
||||
} else if (msg.to_user && msg.to_user !== '大家') {
|
||||
// 对特定对象说话
|
||||
@@ -735,14 +810,14 @@
|
||||
const verbStr = msg.action ?
|
||||
buildActionStr(msg.action, fromHtml, toHtml) :
|
||||
`${fromHtml}对${toHtml}说:`;
|
||||
html = `${headImg}${verbStr}<span class="msg-content" style="color: ${fontColor}">${msg.content}</span>`;
|
||||
html = `${headImg}${verbStr}<span class="msg-content" style="color: ${fontColor}">${messageBodyHtml}</span>`;
|
||||
} else {
|
||||
// 对大家说话
|
||||
const fromHtml = clickableUser(msg.from_user, '#000099');
|
||||
const verbStr = msg.action ?
|
||||
buildActionStr(msg.action, fromHtml, '大家') :
|
||||
`${fromHtml}对大家说:`;
|
||||
html = `${headImg}${verbStr}<span class="msg-content" style="color: ${fontColor}">${msg.content}</span>`;
|
||||
html = `${headImg}${verbStr}<span class="msg-content" style="color: ${fontColor}">${messageBodyHtml}</span>`;
|
||||
}
|
||||
|
||||
if (!timeStrOverride) {
|
||||
@@ -1251,6 +1326,99 @@
|
||||
let _imeComposing = false;
|
||||
const _contentInput = document.getElementById('content');
|
||||
|
||||
/**
|
||||
* 更新底部图片选择状态提示。
|
||||
*/
|
||||
function updateChatImageSelectionLabel(filename = '') {
|
||||
const nameEl = document.getElementById('chat-image-name');
|
||||
if (!nameEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
nameEl.textContent = filename || '未选择图片';
|
||||
nameEl.style.color = filename ? '#0f766e' : '#64748b';
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理聊天图片选择后的前端状态展示。
|
||||
*/
|
||||
function handleChatImageSelected(input) {
|
||||
const file = input?.files?.[0] ?? null;
|
||||
if (!file) {
|
||||
updateChatImageSelectionLabel('');
|
||||
return;
|
||||
}
|
||||
|
||||
updateChatImageSelectionLabel(file.name);
|
||||
|
||||
// 用户选择图片后,立即触发自动发送
|
||||
sendMessage(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理当前选中的聊天图片。
|
||||
*/
|
||||
function clearSelectedChatImage(resetInput = false) {
|
||||
const imageInput = document.getElementById('chat_image');
|
||||
|
||||
if (resetInput && imageInput) {
|
||||
imageInput.value = '';
|
||||
}
|
||||
|
||||
updateChatImageSelectionLabel('');
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开聊天图片大图预览层。
|
||||
*/
|
||||
function openChatImageLightbox(imageUrl, imageName = '聊天图片') {
|
||||
const lightbox = document.getElementById('chat-image-lightbox');
|
||||
const imageEl = document.getElementById('chat-image-lightbox-img');
|
||||
const nameEl = document.getElementById('chat-image-lightbox-name');
|
||||
|
||||
if (!lightbox || !imageEl || !imageUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
imageEl.src = imageUrl;
|
||||
imageEl.alt = imageName;
|
||||
|
||||
if (nameEl) {
|
||||
nameEl.textContent = imageName;
|
||||
}
|
||||
|
||||
lightbox.style.display = 'block';
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭聊天图片大图预览层。
|
||||
*/
|
||||
function closeChatImageLightbox(event = null) {
|
||||
// 如果是点击事件,且点击的目标不是背景或关闭按钮(比如点击了图片本身且没有阻止冒泡),则不关闭
|
||||
// 已经在 HTML 中对 img 做了 stopPropagation,此处 event.target !== event.currentTarget 仍是安全的
|
||||
if (event && event.target !== event.currentTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lightbox = document.getElementById('chat-image-lightbox');
|
||||
if (!lightbox) return;
|
||||
|
||||
lightbox.style.display = 'none';
|
||||
|
||||
const imageEl = document.getElementById('chat-image-lightbox-img');
|
||||
if (imageEl) {
|
||||
imageEl.src = '';
|
||||
}
|
||||
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
window.handleChatImageSelected = handleChatImageSelected;
|
||||
window.openChatImageLightbox = openChatImageLightbox;
|
||||
window.closeChatImageLightbox = closeChatImageLightbox;
|
||||
updateChatImageSelectionLabel();
|
||||
|
||||
// 中文/日文等 IME 组词开始
|
||||
_contentInput.addEventListener('compositionstart', () => {
|
||||
_imeComposing = true;
|
||||
@@ -1302,9 +1470,11 @@
|
||||
const formData = new FormData(form);
|
||||
const contentInput = document.getElementById('content');
|
||||
const submitBtn = document.getElementById('send-btn');
|
||||
const imageInput = document.getElementById('chat_image');
|
||||
const selectedImage = imageInput?.files?.[0] ?? null;
|
||||
|
||||
const content = formData.get('content').trim();
|
||||
if (!content) {
|
||||
const content = String(formData.get('content') || '').trim();
|
||||
if (!content && !selectedImage) {
|
||||
contentInput.focus();
|
||||
_isSending = false;
|
||||
return;
|
||||
@@ -1312,7 +1482,7 @@
|
||||
|
||||
// 如果发言对象是 AI 小助手,也发送一份给专用机器人 API,不打断正常的发消息流程
|
||||
const toUser = formData.get('to_user');
|
||||
if (toUser === 'AI小班长') {
|
||||
if (toUser === 'AI小班长' && content) {
|
||||
sendToChatBot(content); // 异步调用,不阻塞全局发送
|
||||
}
|
||||
|
||||
@@ -1320,7 +1490,7 @@
|
||||
// 当用户输入内容符合暗号格式(4-8位大写字母/数字)时,主动尝试领取
|
||||
// 不依赖轮询标志,服务端负责校验是否真有活跃箱子及暗号是否匹配
|
||||
const passcodePattern = /^[A-Z0-9]{4,8}$/;
|
||||
if (passcodePattern.test(content.trim())) {
|
||||
if (!selectedImage && passcodePattern.test(content.trim())) {
|
||||
_isSending = false;
|
||||
|
||||
try {
|
||||
@@ -1385,6 +1555,7 @@
|
||||
const data = await response.json();
|
||||
if (response.ok && data.status === 'success') {
|
||||
contentInput.value = '';
|
||||
clearSelectedChatImage(true);
|
||||
contentInput.focus();
|
||||
} else {
|
||||
window.chatDialog.alert('发送失败: ' + (data.message || JSON.stringify(data.errors)), '操作失败',
|
||||
|
||||
Reference in New Issue
Block a user