diff --git a/client/css/style.css b/client/css/style.css
index 19a25d0..8228d85 100644
--- a/client/css/style.css
+++ b/client/css/style.css
@@ -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;
+ }
+}
+
diff --git a/client/js/chat.js b/client/js/chat.js
index 5b31a65..5624ee9 100644
--- a/client/js/chat.js
+++ b/client/js/chat.js
@@ -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 = `
`
+ 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 `
`;
+ }).join('');
+ if (messageText && imageElements) {
+ // Mixed content: text + images
+ contentHtml = `
+
${messageText}
+ ${imageElements}
+
`;
+ } 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 = `
+
${messageText}
+

+
`;
+ } else {
+ // Image only
+ contentHtml = `
`;
+ }
+ } else {
+ // Legacy format: plain image data URL
+ const safeImgSrc = escapeHTML(text).replace(/javascript:/gi, '');
+ contentHtml = `
`;
+ }
} 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 = `
`
+ }); 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 `
`;
+ }).join('');
+ if (messageText && imageElements) {
+ // Mixed content: text + images
+ contentHtml = `
+
${messageText}
+ ${imageElements}
+
`;
+ } 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 = `
+
${messageText}
+

+
`;
+ } else {
+ // Image only
+ contentHtml = `
`;
+ }
+ } else {
+ // Legacy format: plain image data URL
+ const safeImgSrc = escapeHTML(msg).replace(/javascript:/gi, '');
+ contentHtml = `
`;
+ }
} else if (msgType === 'file' || msgType === 'file_private') {
// Handle file messages
contentHtml = renderFileMessage(msg, false); } else {
diff --git a/client/js/main.js b/client/js/main.js
index d097f63..a682e9f 100644
--- a/client/js/main.js
+++ b/client/js/main.js
@@ -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;
diff --git a/client/js/room.js b/client/js/room.js
index f5af706..00117c1 100644
--- a/client/js/room.js
+++ b/client/js/room.js
@@ -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()
}
}
diff --git a/client/js/util.image.js b/client/js/util.image.js
new file mode 100644
index 0000000..20479ce
--- /dev/null
+++ b/client/js/util.image.js
@@ -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
+ }
+ }
+ })
+ }
+}