From 3d48abdf730f8f3e0d61f499ea0c1caff251679d Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 29 Sep 2026 02:42:21 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BD=91=E9=A1=B5=E7=89=88=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E7=B2=98=E8=B4=B4=E5=9B=BE=E7=89=87=E5=8F=91=E9=80=81=E3=80=81?= =?UTF-8?q?=E8=81=8A=E5=A4=A9=E5=9B=BE=E7=89=87=E4=BF=9D=E5=AD=98=E5=88=B0?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 老板反馈:"没有复制粘贴图片功能,只能保存在电脑里面发,图片也没有保存功能" - 聊天输入框新增粘贴图片检测:剪贴板里有图片(截图工具/复制图片)时直接走发图逻辑, 不再要求先存到电脑里再用文件选择器上传;普通文本粘贴不受影响 - "查看大图"预览里新增"保存图片"下载按钮,取消息自带的原始文件名, 取不到时按mime类型生成默认文件名 Co-Authored-By: Claude Sonnet 5 --- .../features/messages/AttachmentPreview.tsx | 36 +++++++++++++++++-- .../features/messages/ConversationView.tsx | 24 +++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/web/src/features/messages/AttachmentPreview.tsx b/web/src/features/messages/AttachmentPreview.tsx index 26b65d74..d5294595 100644 --- a/web/src/features/messages/AttachmentPreview.tsx +++ b/web/src/features/messages/AttachmentPreview.tsx @@ -10,6 +10,24 @@ interface ImageData { mime?: string; width?: number; height?: number; + /** Drafty.insertImage's own `filename` field -- present on every image this WEB client sent + * (see sendImage in lib/tinode/attachments.ts). */ + filename?: string; + /** mobileAttachmentContentOf's `name` -- the equivalent field on an image sent from the iOS/Android + * app, which flows through a completely different (non-Drafty) flat shape than the web client's + * own `IM` entity above; MessageBubble passes that content straight through as this same ImageData + * shape, so both need a name field here even though the two clients don't call it the same thing. */ + name?: string; +} + +/** Best-effort filename for the "保存图片" download below -- prefers whatever name the sender's own + * client attached (see ImageData's own field comments), falling back to a generic name derived from + * the mime type for the inline-base64/mobile-relay cases where neither field is set. */ +function imageDownloadName(data: ImageData): string { + if (data.filename) return data.filename; + if (data.name) return data.name; + const ext = data.mime?.split('/')[1]?.replace('jpeg', 'jpg') || 'jpg'; + return `image.${ext}`; } const LIGHTBOX_ZOOM_MIN = 1; @@ -22,7 +40,7 @@ const LIGHTBOX_ZOOM_STEP = 0.5; * Zoom is CSS transform scale/translate on the `` itself, not a native browser zoom -- wheel * and the +/- buttons zoom around the center; dragging only does anything once zoomed in, otherwise * the image has nowhere to pan to and a drag gesture would just fight the backdrop's click-to-close. */ -function ImageLightbox({ src, onClose }: { src: string; onClose: () => void }) { +function ImageLightbox({ src, filename, onClose }: { src: string; filename: string; onClose: () => void }) { const [zoom, setZoom] = useState(LIGHTBOX_ZOOM_MIN); const [pan, setPan] = useState({ x: 0, y: 0 }); const imgRef = useRef(null); @@ -92,6 +110,20 @@ function ImageLightbox({ src, onClose }: { src: string; onClose: () => void }) { > + {/* Real anchor + `download`, same technique FileAttachment already relies on below -- works + for both inline `data:` URIs (small images) and the `blob:` URL useAuthedBlobUrl fetches + for larger ones, triggering a normal save-to-disk with no extra round trip. User report + (老板, 2026-09-29): "图片也没有保存功能" -- right-click "另存为" technically already works on + an ``, but wasn't discoverable enough; this puts an explicit, visible affordance here, + same place every mainstream IM's own "查看大图" view puts it. */} + + + @@ -144,7 +176,7 @@ export function ImageAttachment({ data, locked }: { data: ImageData; locked?: bo - {viewing ? setViewing(false)} /> : null} + {viewing ? setViewing(false)} /> : null} ); } diff --git a/web/src/features/messages/ConversationView.tsx b/web/src/features/messages/ConversationView.tsx index 8b04ac21..08261b4f 100644 --- a/web/src/features/messages/ConversationView.tsx +++ b/web/src/features/messages/ConversationView.tsx @@ -936,6 +936,29 @@ export function ConversationView() { } }; + /** User report (老板, 2026-09-29): "没有复制粘贴图片功能,只能保存在电脑里面发" -- e.g. a screenshot + * tool, or "复制图片" from a browser/chat app, only ever puts raw pixel data on the clipboard, never + * a File the hidden `` above can see; without this, getting that image into a + * message required saving it to disk first and picking it via handlePickImage. `item.getAsFile()` + * returns a real File, indistinguishable from one chosen through the file picker, so this reuses + * the exact same send path. Only preventDefault once an actual image item is found -- an ordinary + * text paste (no image on the clipboard at all) is left completely untouched. */ + const handleComposerPaste = (e: React.ClipboardEvent) => { + const items = e.clipboardData?.items; + if (!items) return; + for (let i = 0; i < items.length; i += 1) { + const item = items[i]; + if (item.kind === 'file' && item.type.startsWith('image/')) { + const file = item.getAsFile(); + if (!file) continue; + e.preventDefault(); + const reply = consumePendingReply(); + sendImage(topic, file, reply).catch((err) => showError(err, '图片发送失败')); + return; + } + } + }; + return (
@@ -1214,6 +1237,7 @@ export function ConversationView() { value={draft} onChange={handleDraftChange} onKeyDown={handleKeyDown} + onPaste={handleComposerPaste} placeholder="输入消息,Enter 发送,Shift+Enter 换行" className="min-h-[72px] flex-1 resize-none bg-transparent text-[15px] leading-normal outline-none placeholder:text-text-placeholder-1" />