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}

)}