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 = (
+
+
+
+
+
+

+
+ )
+
+ 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}
)}