From cd287964400de1945056c38ceb653c9eec0f2564 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Wed, 15 Jul 2026 12:44:37 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=80=9A=E7=94=A8=E8=81=8A?= =?UTF-8?q?=E5=A4=A9=E5=9B=BE=E7=89=87=E9=A2=84=E8=A7=88=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ImagePreview / ChatImage:全屏预览、ESC 关闭、下载 - 接入工作台、访客 Widget、对话记录 --- web/src/components/common/ImagePreview.tsx | 121 +++++++++++++++++++++ web/src/pages/agent/ChatHistory.tsx | 3 +- web/src/pages/agent/Dashboard.tsx | 3 +- web/src/widgets/VisitorChat.tsx | 5 +- 4 files changed, 128 insertions(+), 4 deletions(-) create mode 100644 web/src/components/common/ImagePreview.tsx diff --git a/web/src/components/common/ImagePreview.tsx b/web/src/components/common/ImagePreview.tsx new file mode 100644 index 0000000..0710139 --- /dev/null +++ b/web/src/components/common/ImagePreview.tsx @@ -0,0 +1,121 @@ +import { useEffect, useCallback, useState, type MouseEvent } from 'react' +import { createPortal } from 'react-dom' +import { CloseOutlined, DownloadOutlined, ZoomInOutlined } from '@ant-design/icons' + +export interface ImagePreviewProps { + open: boolean + src: string + alt?: string + onClose: () => void +} + +/** + * 通用大图预览:遮罩 + 居中大图,支持 ESC / 点遮罩关闭,可下载。 + */ +export function ImagePreview({ open, src, alt = '图片预览', onClose }: ImagePreviewProps) { + const onKey = useCallback((e: KeyboardEvent) => { + if (e.key === 'Escape') onClose() + }, [onClose]) + + useEffect(() => { + if (!open) return + document.addEventListener('keydown', onKey) + const prev = document.body.style.overflow + document.body.style.overflow = 'hidden' + return () => { + document.removeEventListener('keydown', onKey) + document.body.style.overflow = prev + } + }, [open, onKey]) + + if (!open || !src) return null + + const stop = (e: MouseEvent) => e.stopPropagation() + + const download = () => { + const a = document.createElement('a') + a.href = src + a.download = alt || 'image' + a.target = '_blank' + a.rel = 'noopener noreferrer' + a.click() + } + + const node = ( +
+
+ + +
+ {alt} +
+ ) + + return createPortal(node, document.body) +} + +export interface ChatImageProps { + src: string + alt?: string + /** 缩略图样式 */ + className?: string +} + +/** + * 聊天内可点击图片:缩略图 + 通用 ImagePreview + */ +export function ChatImage({ src, alt = '聊天图片', className = '' }: ChatImageProps) { + const [open, setOpen] = useState(false) + + if (!src) return null + + return ( + <> + + setOpen(false)} /> + + ) +} + +export default ImagePreview diff --git a/web/src/pages/agent/ChatHistory.tsx b/web/src/pages/agent/ChatHistory.tsx index 53388b0..8225b6c 100644 --- a/web/src/pages/agent/ChatHistory.tsx +++ b/web/src/pages/agent/ChatHistory.tsx @@ -5,6 +5,7 @@ import { getCustomers, getSession, getSessions, type Customer, type Message, type Session, type SessionEvent, } from '@/services/api' +import { ChatImage } from '@/components/common/ImagePreview' const statusColors: Record = { active: 'blue', waiting: 'orange', ended: 'green', archived: 'default', @@ -212,7 +213,7 @@ const ChatHistory = () => { {isAgent ? '客服' : '访客'} {message.type === 'image' ? ( - 图片 + ) : (
{message.content}
)} diff --git a/web/src/pages/agent/Dashboard.tsx b/web/src/pages/agent/Dashboard.tsx index 177cf29..445cf26 100644 --- a/web/src/pages/agent/Dashboard.tsx +++ b/web/src/pages/agent/Dashboard.tsx @@ -6,6 +6,7 @@ import { ExportOutlined, BookOutlined, PictureOutlined, } from '@ant-design/icons' import EmojiPicker, { insertAtCursor } from '@/components/common/EmojiPicker' +import { ChatImage } from '@/components/common/ImagePreview' import { useAuth } from '@/stores/auth' import { addSessionNote, claimSession, endSession, getAvailableAgents, getCustomers, getKnowledgeEntries, @@ -725,7 +726,7 @@ const Dashboard = () => { }`} > {message.type === 'image' ? ( - 聊天图片 + ) : (

{message.content}

)} diff --git a/web/src/widgets/VisitorChat.tsx b/web/src/widgets/VisitorChat.tsx index 6cae776..28672a9 100644 --- a/web/src/widgets/VisitorChat.tsx +++ b/web/src/widgets/VisitorChat.tsx @@ -4,6 +4,7 @@ import { StarFilled, MinusOutlined, PictureOutlined, CustomerServiceOutlined, } from '@ant-design/icons' import EmojiPicker, { insertAtCursor } from '@/components/common/EmojiPicker' +import { ChatImage } from '@/components/common/ImagePreview' interface Message { id: number @@ -468,7 +469,7 @@ const VisitorChat = ({
{msg.type === 'image' ? ( - 图片 + ) : (

{msg.content}

)} @@ -481,7 +482,7 @@ const VisitorChat = ({
{msg.type === 'image' ? ( - 图片 + ) : (

{msg.content}

)}