新增聊天室发送图片功能

This commit is contained in:
2026-04-12 14:04:18 +08:00
parent d2f08eb2dd
commit 00b9396dea
10 changed files with 547 additions and 42 deletions
+178 -7
View File
@@ -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)), '操作失败',