添加图片预览和粘贴功能,支持多图和文字混合发送
This commit is contained in:
1 parent
b0ffe3a198
commit
62f5b3d3d9
5 files changed
+584
-70
No files matched your search
@@ -1665,3 +1665,129 @@ body {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
/* Image preview in input */
|
||||
/* 输入框图片预览样式 */
|
||||
.input-image-preview {
|
||||
position: relative;
|
||||
display: inline-block; /* 改为 inline-block 以便并排显示 */
|
||||
margin: 4px; /* 统一 margin */
|
||||
border-radius: 8px;
|
||||
overflow: visible; /* 允许移除按钮稍微超出 */
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef; /* 边框调整 */
|
||||
transition: all 0.2s ease;
|
||||
max-width: 80px; /* 限制预览容器的最大宽度 */
|
||||
}
|
||||
|
||||
.input-image-preview:hover {
|
||||
border-color: #007bff;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 123, 255, 0.1); /* 调整阴影 */
|
||||
}
|
||||
|
||||
.input-image-preview-img {
|
||||
display: block;
|
||||
width: 100%; /* 图片宽度充满容器 */
|
||||
height: 60px; /* 固定高度 */
|
||||
object-fit: cover;
|
||||
border-radius: 6px; /* 轻微圆角 */
|
||||
}
|
||||
|
||||
.input-image-remove-btn {
|
||||
position: absolute;
|
||||
top: -10px; /* 调整位置使其更易点击 */
|
||||
right: -10px; /* 调整位置使其更易点击 */
|
||||
width: 22px; /* 增大点击区域 */
|
||||
height: 22px; /* 增大点击区域 */
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.input-image-remove-btn:hover {
|
||||
background: #c82333;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.input-image-remove-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* Image message in chat bubble */
|
||||
/* 聊天气泡中的图片消息样式 */
|
||||
.bubble-img {
|
||||
max-width: 300px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 200px;
|
||||
object-fit: contain;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: block;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.bubble-img:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.bubble-content .bubble-img:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.bubble-content .bubble-img:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Mixed content (text + image) */
|
||||
/* 混合内容(文字 + 图片)样式 */
|
||||
.bubble-content.mixed-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bubble-content.mixed-content .message-text {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.bubble-content.mixed-content .bubble-img {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.input-image-preview-img {
|
||||
width: 60px;
|
||||
height: 45px;
|
||||
}
|
||||
|
||||
.bubble-img {
|
||||
max-width: 250px;
|
||||
max-height: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.input-image-preview-img {
|
||||
width: 50px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.bubble-img {
|
||||
max-width: 200px;
|
||||
max-height: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
+85
-8
@@ -16,6 +16,7 @@ import {
|
||||
$id,
|
||||
createElement,
|
||||
on,
|
||||
off,
|
||||
addClass,
|
||||
removeClass
|
||||
} from './util.dom.js';
|
||||
@@ -56,10 +57,48 @@ export function addMsg(text, isHistory = false, msgType = 'text', timestamp = nu
|
||||
if (!chatArea) return;
|
||||
let className = 'bubble me' + (msgType.includes('_private') ? ' private-message' : '');
|
||||
const date = new Date(ts);
|
||||
const time = date.getHours().toString().padStart(2, '0') + ':' + date.getMinutes().toString().padStart(2, '0'); let contentHtml = '';
|
||||
if (msgType === 'image' || msgType === 'image_private') {
|
||||
const safeImgSrc = escapeHTML(text).replace(/javascript:/gi, '');
|
||||
contentHtml = `<img src="${safeImgSrc}"alt="image"class="bubble-img">`
|
||||
const time = date.getHours().toString().padStart(2, '0') + ':' + date.getMinutes().toString().padStart(2, '0'); let contentHtml = ''; if (msgType === 'image' || msgType === 'image_private') {
|
||||
// Handle image messages (can contain both text and images)
|
||||
if (typeof text === 'object' && text.images && Array.isArray(text.images)) {
|
||||
// New multi-image format: {text: "", images: ["data:image...", "data:image..."]}
|
||||
const messageText = text.text ? textToHTML(text.text) : '';
|
||||
const imageElements = text.images.map(imgData => {
|
||||
const safeImgSrc = escapeHTML(imgData).replace(/javascript:/gi, '');
|
||||
return `<img src="${safeImgSrc}" alt="image" class="bubble-img">`;
|
||||
}).join('');
|
||||
if (messageText && imageElements) {
|
||||
// Mixed content: text + images
|
||||
contentHtml = `<div class="mixed-content">
|
||||
<div class="message-text">${messageText}</div>
|
||||
${imageElements}
|
||||
</div>`;
|
||||
} else if (imageElements) {
|
||||
// Images only
|
||||
contentHtml = imageElements;
|
||||
} else {
|
||||
// Fallback to text only
|
||||
contentHtml = messageText;
|
||||
}
|
||||
} else if (typeof text === 'object' && text.image) {
|
||||
// Legacy single image format: {text: "", image: "data:image..."}
|
||||
const safeImgSrc = escapeHTML(text.image).replace(/javascript:/gi, '');
|
||||
const messageText = text.text ? textToHTML(text.text) : '';
|
||||
|
||||
if (messageText) {
|
||||
// Mixed content: text + image
|
||||
contentHtml = `<div class="mixed-content">
|
||||
<div class="message-text">${messageText}</div>
|
||||
<img src="${safeImgSrc}" alt="image" class="bubble-img">
|
||||
</div>`;
|
||||
} else {
|
||||
// Image only
|
||||
contentHtml = `<img src="${safeImgSrc}" alt="image" class="bubble-img">`;
|
||||
}
|
||||
} else {
|
||||
// Legacy format: plain image data URL
|
||||
const safeImgSrc = escapeHTML(text).replace(/javascript:/gi, '');
|
||||
contentHtml = `<img src="${safeImgSrc}" alt="image" class="bubble-img">`;
|
||||
}
|
||||
} else if (msgType === 'file' || msgType === 'file_private') {
|
||||
// Handle file messages
|
||||
contentHtml = renderFileMessage(text, true);
|
||||
@@ -101,10 +140,48 @@ export function addOtherMsg(msg, userName = '', avatar = '', isHistory = false,
|
||||
if (!chatArea) return;
|
||||
const bubbleWrap = createElement('div', {
|
||||
class: 'bubble-other-wrap'
|
||||
}); let contentHtml = '';
|
||||
if (msgType === 'image' || msgType === 'image_private') {
|
||||
const safeImgSrc = escapeHTML(msg).replace(/javascript:/gi, '');
|
||||
contentHtml = `<img src="${safeImgSrc}"alt="image"class="bubble-img">`
|
||||
}); let contentHtml = ''; if (msgType === 'image' || msgType === 'image_private') {
|
||||
// Handle image messages (can contain both text and images)
|
||||
if (typeof msg === 'object' && msg.images && Array.isArray(msg.images)) {
|
||||
// New multi-image format: {text: "", images: ["data:image...", "data:image..."]}
|
||||
const messageText = msg.text ? textToHTML(msg.text) : '';
|
||||
const imageElements = msg.images.map(imgData => {
|
||||
const safeImgSrc = escapeHTML(imgData).replace(/javascript:/gi, '');
|
||||
return `<img src="${safeImgSrc}" alt="image" class="bubble-img">`;
|
||||
}).join('');
|
||||
if (messageText && imageElements) {
|
||||
// Mixed content: text + images
|
||||
contentHtml = `<div class="mixed-content">
|
||||
<div class="message-text">${messageText}</div>
|
||||
${imageElements}
|
||||
</div>`;
|
||||
} else if (imageElements) {
|
||||
// Images only
|
||||
contentHtml = imageElements;
|
||||
} else {
|
||||
// Fallback to text only
|
||||
contentHtml = messageText;
|
||||
}
|
||||
} else if (typeof msg === 'object' && msg.image) {
|
||||
// Legacy single image format: {text: "", image: "data:image..."}
|
||||
const safeImgSrc = escapeHTML(msg.image).replace(/javascript:/gi, '');
|
||||
const messageText = msg.text ? textToHTML(msg.text) : '';
|
||||
|
||||
if (messageText) {
|
||||
// Mixed content: text + image
|
||||
contentHtml = `<div class="mixed-content">
|
||||
<div class="message-text">${messageText}</div>
|
||||
<img src="${safeImgSrc}" alt="image" class="bubble-img">
|
||||
</div>`;
|
||||
} else {
|
||||
// Image only
|
||||
contentHtml = `<img src="${safeImgSrc}" alt="image" class="bubble-img">`;
|
||||
}
|
||||
} else {
|
||||
// Legacy format: plain image data URL
|
||||
const safeImgSrc = escapeHTML(msg).replace(/javascript:/gi, '');
|
||||
contentHtml = `<img src="${safeImgSrc}" alt="image" class="bubble-img">`;
|
||||
}
|
||||
} else if (msgType === 'file' || msgType === 'file_private') {
|
||||
// Handle file messages
|
||||
contentHtml = renderFileMessage(msg, false); } else {
|
||||
|
||||
+116
-40
@@ -10,6 +10,12 @@ import {
|
||||
downloadFile
|
||||
} from './util.file.js';
|
||||
|
||||
// 从 util.image.js 中导入图片处理功能
|
||||
// Import image processing functions from util.image.js
|
||||
import {
|
||||
setupImagePaste
|
||||
} from './util.image.js';
|
||||
|
||||
// 从 util.emoji.js 中导入设置表情选择器的函数
|
||||
// Import setupEmojiPicker function from util.emoji.js
|
||||
import {
|
||||
@@ -51,9 +57,11 @@ import {
|
||||
// Import chat-related functions from chat.js
|
||||
import {
|
||||
addMsg, // 添加普通消息到聊天窗口 / Add a normal message to chat
|
||||
addOtherMsg, // 添加其他用户消息 / Add message from other users
|
||||
addSystemMsg, // 添加系统消息 / Add a system message
|
||||
setupImagePreview, // 设置图片预览功能 / Setup image preview
|
||||
setupInputPlaceholder // 设置输入框的占位提示 / Setup placeholder for input box
|
||||
setupInputPlaceholder, // 设置输入框的占位提示 / Setup placeholder for input box
|
||||
autoGrowInput // 自动调整输入框高度 / Auto adjust input height
|
||||
} from './chat.js';
|
||||
|
||||
// 从 ui.js 中导入 UI 界面相关的功能
|
||||
@@ -81,6 +89,7 @@ window.config = {
|
||||
// 把一些函数挂载到 window 对象上供其他模块使用
|
||||
// Expose functions to the global window object for accessibility
|
||||
window.addSystemMsg = addSystemMsg;
|
||||
window.addOtherMsg = addOtherMsg;
|
||||
window.joinRoom = joinRoom;
|
||||
window.notifyMessage = notifyMessage;
|
||||
window.setupEmojiPicker = setupEmojiPicker;
|
||||
@@ -137,8 +146,12 @@ window.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
// 点击其他地方时关闭设置面板 (已移除,因为现在使用侧边栏形式)
|
||||
// Close settings panel when clicking outside (removed since we now use sidebar format)
|
||||
|
||||
const input = document.querySelector('.input-message-input'); // 消息输入框 / Message input box
|
||||
|
||||
// 设置图片粘贴功能
|
||||
// Setup image paste functionality
|
||||
const imagePasteHandler = setupImagePaste('.input-message-input');
|
||||
|
||||
if (input) {
|
||||
input.focus(); // 自动聚焦 / Auto focus
|
||||
input.addEventListener('keydown', (e) => {
|
||||
@@ -146,45 +159,108 @@ window.addEventListener('DOMContentLoaded', () => {
|
||||
// Pressing Enter (without Shift) sends the message
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
const text = input.innerText.trim(); // 获取输入的文本 / Get input text
|
||||
const rd = roomsData[activeRoomIndex]; // 当前房间数据 / Current room data
|
||||
if (text && rd && rd.chat) {
|
||||
if (rd.privateChatTargetId) {
|
||||
// 私聊消息加密并发送
|
||||
// Encrypt and send private message
|
||||
const targetClient = rd.chat.channel[rd.privateChatTargetId];
|
||||
if (targetClient && targetClient.shared) {
|
||||
const clientMessagePayload = {
|
||||
a: 'm',
|
||||
t: 'text_private',
|
||||
d: text
|
||||
};
|
||||
const encryptedClientMessage = rd.chat.encryptClientMessage(clientMessagePayload, targetClient.shared);
|
||||
const serverRelayPayload = {
|
||||
a: 'c',
|
||||
p: encryptedClientMessage,
|
||||
c: rd.privateChatTargetId
|
||||
};
|
||||
const encryptedMessageForServer = rd.chat.encryptServerMessage(serverRelayPayload, rd.chat.serverShared);
|
||||
rd.chat.sendMessage(encryptedMessageForServer);
|
||||
addMsg(text, false, 'text_private');
|
||||
} else {
|
||||
addSystemMsg(`Cannot send private message to ${rd.privateChatTargetName}.User might not be fully connected.`)
|
||||
}
|
||||
} else {
|
||||
// 公共频道消息发送
|
||||
// Send public message
|
||||
rd.chat.sendChannelMessage('text', text);
|
||||
addMsg(text);
|
||||
}
|
||||
// 清空输入框并触发 input 事件
|
||||
// Clear input and trigger input event
|
||||
input.innerText = '';
|
||||
input.dispatchEvent(new Event('input'));
|
||||
}
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 发送消息的统一函数
|
||||
// Unified function to send messages
|
||||
function sendMessage() {
|
||||
const text = input.innerText.trim(); // 获取输入的文本 / Get input text
|
||||
const images = imagePasteHandler ? imagePasteHandler.getCurrentImages() : []; // 获取所有图片
|
||||
|
||||
if (!text && images.length === 0) return; // 如果没有文本且没有图片,则不发送
|
||||
const rd = roomsData[activeRoomIndex]; // 当前房间数据 / Current room data
|
||||
|
||||
if (rd && rd.chat) {
|
||||
if (images.length > 0) {
|
||||
// 发送包含图片的消息 (支持多图和文字合并)
|
||||
// Send message with images (supports multiple images and text combined)
|
||||
const messageContent = {
|
||||
text: text || '', // 包含文字内容,如果有的话
|
||||
images: images // 包含所有图片数据
|
||||
};
|
||||
|
||||
if (rd.privateChatTargetId) {
|
||||
// 私聊图片消息加密并发送
|
||||
// Encrypt and send private image message
|
||||
const targetClient = rd.chat.channel[rd.privateChatTargetId];
|
||||
if (targetClient && targetClient.shared) {
|
||||
const clientMessagePayload = {
|
||||
a: 'm',
|
||||
t: 'image_private',
|
||||
d: messageContent
|
||||
};
|
||||
const encryptedClientMessage = rd.chat.encryptClientMessage(clientMessagePayload, targetClient.shared);
|
||||
const serverRelayPayload = {
|
||||
a: 'c',
|
||||
p: encryptedClientMessage,
|
||||
c: rd.privateChatTargetId
|
||||
};
|
||||
const encryptedMessageForServer = rd.chat.encryptServerMessage(serverRelayPayload, rd.chat.serverShared);
|
||||
rd.chat.sendMessage(encryptedMessageForServer);
|
||||
addMsg(messageContent, false, 'image_private');
|
||||
} else {
|
||||
addSystemMsg(`Cannot send private message to ${rd.privateChatTargetName}.User might not be fully connected.`)
|
||||
}
|
||||
} else {
|
||||
// 公共频道图片消息发送
|
||||
// Send image message to public channel
|
||||
rd.chat.sendChannelMessage('image', messageContent);
|
||||
addMsg(messageContent, false, 'image');
|
||||
}
|
||||
|
||||
imagePasteHandler.clearImages(); // 清除所有图片预览
|
||||
} else if (text) {
|
||||
// 发送纯文本消息
|
||||
// Send text-only message
|
||||
if (rd.privateChatTargetId) {
|
||||
// 私聊消息加密并发送
|
||||
// Encrypt and send private message
|
||||
const targetClient = rd.chat.channel[rd.privateChatTargetId];
|
||||
if (targetClient && targetClient.shared) {
|
||||
const clientMessagePayload = {
|
||||
a: 'm',
|
||||
t: 'text_private',
|
||||
d: text
|
||||
};
|
||||
const encryptedClientMessage = rd.chat.encryptClientMessage(clientMessagePayload, targetClient.shared);
|
||||
const serverRelayPayload = {
|
||||
a: 'c',
|
||||
p: encryptedClientMessage,
|
||||
c: rd.privateChatTargetId
|
||||
};
|
||||
const encryptedMessageForServer = rd.chat.encryptServerMessage(serverRelayPayload, rd.chat.serverShared);
|
||||
rd.chat.sendMessage(encryptedMessageForServer);
|
||||
addMsg(text, false, 'text_private');
|
||||
} else {
|
||||
addSystemMsg(`Cannot send private message to ${rd.privateChatTargetName}.User might not be fully connected.`)
|
||||
}
|
||||
} else {
|
||||
// 公共频道消息发送
|
||||
// Send public message
|
||||
rd.chat.sendChannelMessage('text', text);
|
||||
addMsg(text); }
|
||||
}
|
||||
|
||||
// 清空输入框并触发 input 事件
|
||||
// Clear input and trigger input event
|
||||
input.innerHTML = ''; // 清空输入框内容 / Clear input field content
|
||||
if (imagePasteHandler && typeof imagePasteHandler.refreshPlaceholder === 'function') {
|
||||
imagePasteHandler.refreshPlaceholder(); // 更新 placeholder 状态
|
||||
}
|
||||
autoGrowInput(); // 调整输入框高度
|
||||
}
|
||||
}
|
||||
|
||||
// 为发送按钮添加点击事件
|
||||
// Add click event for send button
|
||||
const sendButton = document.querySelector('.send-message-btn');
|
||||
if (sendButton) {
|
||||
sendButton.addEventListener('click', sendMessage);
|
||||
}
|
||||
|
||||
// 设置发送文件功能
|
||||
// Setup file sending functionality
|
||||
setupFileSend({
|
||||
@@ -230,10 +306,10 @@ window.addEventListener('DOMContentLoaded', () => {
|
||||
addMsg(message, false, 'file');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} }
|
||||
});
|
||||
|
||||
|
||||
// 判断是否为移动端
|
||||
// Check if the device is mobile
|
||||
const isMobile = () => window.innerWidth <= 768;
|
||||
|
||||
+23
-22
@@ -244,7 +244,6 @@ export function handleClientMessage(idx, msg) {
|
||||
}
|
||||
|
||||
let msgType = msg.type || 'text';
|
||||
|
||||
// Handle file messages
|
||||
if (msgType.startsWith('file_')) {
|
||||
// Import handleFileMessage function if needed
|
||||
@@ -252,22 +251,14 @@ export function handleClientMessage(idx, msg) {
|
||||
window.handleFileMessage(msg.data, msgType.includes('_private'));
|
||||
}
|
||||
|
||||
// Only add file_start messages to chat history
|
||||
// Send notification but don't add to messages array
|
||||
// (util.file.js already handles display via addOtherMsg)
|
||||
if (msgType === 'file_start' || msgType === 'file_start_private') {
|
||||
let realUserName = msg.userName;
|
||||
if (!realUserName && msg.clientId && newRd.userMap[msg.clientId]) {
|
||||
realUserName = newRd.userMap[msg.clientId].userName || newRd.userMap[msg.clientId].username || newRd.userMap[msg.clientId].name
|
||||
}
|
||||
|
||||
roomsData[idx].messages.push({
|
||||
type: 'other',
|
||||
text: msg.data,
|
||||
userName: realUserName,
|
||||
avatar: realUserName,
|
||||
msgType: msgType.includes('_private') ? 'file_private' : 'file',
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
const notificationMsgType = msgType.includes('_private') ? 'private file' : 'file';
|
||||
if (window.notifyMessage) {
|
||||
window.notifyMessage(newRd.roomName, notificationMsgType, `${msg.data.fileName}`, realUserName)
|
||||
@@ -276,30 +267,42 @@ export function handleClientMessage(idx, msg) {
|
||||
if (activeRoomIndex !== idx) {
|
||||
roomsData[idx].unreadCount = (roomsData[idx].unreadCount || 0) + 1;
|
||||
renderRooms(activeRoomIndex)
|
||||
} else {
|
||||
renderChatArea()
|
||||
}
|
||||
// No need to call renderChatArea() since util.file.js handles display
|
||||
}
|
||||
return;
|
||||
return; // Don't process file messages further
|
||||
}
|
||||
|
||||
// Handle legacy image detection
|
||||
if (!msgType.includes('_private') && msg.data && msg.data.startsWith('data:image/')) {
|
||||
msgType = 'image'
|
||||
// Handle image messages (both new and legacy formats)
|
||||
if (msgType === 'image' || msgType === 'image_private') {
|
||||
// Already has correct type
|
||||
} else if (!msgType.includes('_private')) {
|
||||
// Handle legacy image detection
|
||||
if (msg.data && typeof msg.data === 'string' && msg.data.startsWith('data:image/')) {
|
||||
msgType = 'image';
|
||||
} else if (msg.data && typeof msg.data === 'object' && msg.data.image) {
|
||||
msgType = 'image';
|
||||
}
|
||||
}
|
||||
|
||||
let realUserName = msg.userName;
|
||||
if (!realUserName && msg.clientId && newRd.userMap[msg.clientId]) {
|
||||
realUserName = newRd.userMap[msg.clientId].userName || newRd.userMap[msg.clientId].username || newRd.userMap[msg.clientId].name
|
||||
}
|
||||
|
||||
// Add message to messages array for chat history
|
||||
roomsData[idx].messages.push({
|
||||
type: 'other',
|
||||
text: msg.data,
|
||||
userName: realUserName,
|
||||
avatar: realUserName,
|
||||
msgType,
|
||||
msgType: msgType,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// Add message to chat display using addOtherMsg
|
||||
if (window.addOtherMsg) {
|
||||
window.addOtherMsg(msg.data, realUserName, realUserName, false, msgType);
|
||||
}
|
||||
|
||||
const notificationMsgType = msgType.includes('_private') ? `private ${msgType.split('_')[0]}` : msgType;
|
||||
if (window.notifyMessage) {
|
||||
window.notifyMessage(newRd.roomName, notificationMsgType, msg.data, realUserName)
|
||||
@@ -307,8 +310,6 @@ export function handleClientMessage(idx, msg) {
|
||||
if (activeRoomIndex !== idx) {
|
||||
roomsData[idx].unreadCount = (roomsData[idx].unreadCount || 0) + 1;
|
||||
renderRooms(activeRoomIndex)
|
||||
} else {
|
||||
renderChatArea()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
// Import DOM helpers
|
||||
// 导入 DOM 辅助函数
|
||||
import {
|
||||
$,
|
||||
on,
|
||||
createElement
|
||||
} from './util.dom.js';
|
||||
|
||||
// Process and compress image file
|
||||
// 处理并压缩图片文件
|
||||
export async function processImage(file, callback) {
|
||||
// Create image element for loading file
|
||||
// 创建图片元素用于加载文件
|
||||
const img = new Image();
|
||||
img.onload = function() {
|
||||
const maxW = 1280, // Max width
|
||||
maxH = 1280; // 最大宽度/高度
|
||||
let w = img.naturalWidth,
|
||||
h = img.naturalHeight;
|
||||
// Resize if too large
|
||||
// 如果图片过大则缩放
|
||||
if (w > maxW || h > maxH) {
|
||||
const scale = Math.min(maxW / w, maxH / h);
|
||||
w = Math.round(w * scale);
|
||||
h = Math.round(h * scale)
|
||||
}
|
||||
// Create canvas for drawing image
|
||||
// 创建画布用于绘制图片
|
||||
const canvas = createElement('canvas');
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0, w, h);
|
||||
let dataUrl;
|
||||
// Export as webp with 90% quality
|
||||
// 导出为 webp,90% 质量
|
||||
dataUrl = canvas.toDataURL('image/webp', 0.90);
|
||||
callback(dataUrl)
|
||||
};
|
||||
// Read file as data URL
|
||||
// 以 dataURL 方式读取文件
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
img.src = e.target.result
|
||||
};
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
|
||||
// Translate message key
|
||||
// 翻译消息 key
|
||||
function t(key) {
|
||||
const messages = {
|
||||
tooLarge: 'Image is too large (over 8MB)', // 图片过大(超过8MB)
|
||||
};
|
||||
return messages[key] || key
|
||||
}
|
||||
|
||||
// Create image preview element for input
|
||||
// 为输入框创建图片预览元素
|
||||
function createImagePreview(dataUrl) {
|
||||
const preview = createElement('div', {
|
||||
class: 'input-image-preview'
|
||||
});
|
||||
|
||||
const img = createElement('img', {
|
||||
src: dataUrl,
|
||||
class: 'input-image-preview-img'
|
||||
});
|
||||
|
||||
const removeBtn = createElement('button', {
|
||||
class: 'input-image-remove-btn',
|
||||
type: 'button'
|
||||
}, '×');
|
||||
|
||||
preview.appendChild(img);
|
||||
preview.appendChild(removeBtn);
|
||||
|
||||
return { preview, removeBtn };
|
||||
}
|
||||
|
||||
// Setup image paste functionality
|
||||
// 设置图片粘贴功能
|
||||
export function setupImagePaste(inputSelector) {
|
||||
const input = $(inputSelector);
|
||||
if (!input) return;
|
||||
|
||||
let currentImageDatas = []; // 支持多张图片
|
||||
const imagePreviewContainer = createElement('div', { class: 'image-preview-container' });
|
||||
input.parentNode.insertBefore(imagePreviewContainer, input);
|
||||
|
||||
// Function to update placeholder visibility
|
||||
function updatePlaceholderVisibility() {
|
||||
const placeholder = input.parentNode.querySelector('.input-field-placeholder');
|
||||
if (!placeholder) return;
|
||||
|
||||
const hasText = input.innerText.trim().length > 0;
|
||||
const hasImages = currentImageDatas.length > 0;
|
||||
|
||||
if (hasText || hasImages) {
|
||||
placeholder.style.opacity = '0';
|
||||
input.classList.remove('is-empty'); // 确保移除 is-empty
|
||||
} else {
|
||||
placeholder.style.opacity = '1';
|
||||
input.classList.add('is-empty'); // 确保添加 is-empty
|
||||
}
|
||||
}
|
||||
|
||||
// Initial check for placeholder
|
||||
updatePlaceholderVisibility();
|
||||
|
||||
// Listen to input events to update placeholder
|
||||
on(input, 'input', updatePlaceholderVisibility);
|
||||
on(input, 'focus', updatePlaceholderVisibility);
|
||||
on(input, 'blur', () => {
|
||||
// Defer blur check slightly to allow click on remove button
|
||||
setTimeout(updatePlaceholderVisibility, 100);
|
||||
});
|
||||
|
||||
on(input, 'paste', function(e) {
|
||||
if (!e.clipboardData) return;
|
||||
|
||||
let imageProcessed = false;
|
||||
for (const item of e.clipboardData.items) {
|
||||
if (item.type.startsWith('image/')) {
|
||||
const file = item.getAsFile();
|
||||
if (!file) continue;
|
||||
|
||||
if (file.size > 8 * 1024 * 1024) {
|
||||
alert(t('tooLarge'));
|
||||
continue;
|
||||
}
|
||||
|
||||
processImage(file, (dataUrl) => {
|
||||
const { preview, removeBtn } = createImagePreview(dataUrl);
|
||||
imagePreviewContainer.appendChild(preview);
|
||||
currentImageDatas.push({ dataUrl, previewElement: preview });
|
||||
|
||||
on(removeBtn, 'click', () => {
|
||||
preview.remove();
|
||||
currentImageDatas = currentImageDatas.filter(img => img.dataUrl !== dataUrl);
|
||||
updatePlaceholderVisibility();
|
||||
});
|
||||
updatePlaceholderVisibility();
|
||||
});
|
||||
imageProcessed = true;
|
||||
}
|
||||
}
|
||||
if (imageProcessed) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
return {
|
||||
getCurrentImages: () => currentImageDatas.map(img => img.dataUrl), // 返回所有图片数据
|
||||
clearImages: () => {
|
||||
imagePreviewContainer.innerHTML = ''; // 清空预览容器
|
||||
currentImageDatas = [];
|
||||
updatePlaceholderVisibility();
|
||||
},
|
||||
refreshPlaceholder: updatePlaceholderVisibility // 暴露 placeholder 更新函数
|
||||
};
|
||||
}
|
||||
|
||||
// Setup legacy image send functionality (for backward compatibility)
|
||||
// 设置旧版图片发送功能(向后兼容)
|
||||
export function setupImageSend({
|
||||
inputSelector,
|
||||
attachBtnSelector,
|
||||
fileInputSelector,
|
||||
onSend
|
||||
}) {
|
||||
const input = $(inputSelector);
|
||||
const attachBtn = $(attachBtnSelector);
|
||||
const fileInput = $(fileInputSelector);
|
||||
|
||||
if (fileInput) fileInput.setAttribute('accept', 'image/*');
|
||||
|
||||
if (attachBtn && fileInput) {
|
||||
// Click attach triggers file input
|
||||
// 点击附件按钮触发文件选择
|
||||
on(attachBtn, 'click', () => fileInput.click());
|
||||
|
||||
// Handle file input change
|
||||
// 处理文件选择变化
|
||||
on(fileInput, 'change', async function() {
|
||||
if (!fileInput.files || !fileInput.files.length) return;
|
||||
const file = fileInput.files[0];
|
||||
|
||||
// Only allow image files
|
||||
// 只允许图片文件
|
||||
if (!file.type.startsWith('image/')) return;
|
||||
|
||||
// Check file size
|
||||
// 检查文件大小
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
alert(t('tooLarge'));
|
||||
return
|
||||
}
|
||||
|
||||
processImage(file, (dataUrl) => {
|
||||
if (typeof onSend === 'function') onSend(dataUrl)
|
||||
});
|
||||
fileInput.value = ''
|
||||
})
|
||||
}
|
||||
|
||||
if (input) {
|
||||
// Paste image from clipboard
|
||||
// 从剪贴板粘贴图片
|
||||
on(input, 'paste', function(e) {
|
||||
if (!e.clipboardData) return;
|
||||
for (const item of e.clipboardData.items) {
|
||||
// Only handle image type
|
||||
// 只处理图片类型
|
||||
if (item.type.startsWith('image/')) {
|
||||
const file = item.getAsFile();
|
||||
if (!file) continue;
|
||||
|
||||
// Check file size
|
||||
// 检查文件大小
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
alert(t('tooLarge'));
|
||||
continue
|
||||
}
|
||||
|
||||
processImage(file, (dataUrl) => {
|
||||
if (typeof onSend === 'function') onSend(dataUrl)
|
||||
});
|
||||
e.preventDefault();
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user