feat(聊天页面): 实现搜索结果跳转高亮

- 聊天页面支持通过 URL 参数定位消息
- MessageBubble 组件添加高亮状态支持
- 新增消息高亮动画样式,支持亮色/暗色主题
- 跳转后自动滚动到目标消息并高亮闪烁
- 3秒后自动清除高亮效果
This commit is contained in:
gaoziman 2025-12-24 22:51:04 +08:00
parent 57e8631e10
commit 0f8fd2ce1f
3 changed files with 86 additions and 3 deletions

View File

@ -30,14 +30,19 @@ export default function ChatPage({ params }: PageProps) {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const initialMessage = searchParams.get('message'); const initialMessage = searchParams.get('message');
const highlightParam = searchParams.get('highlight'); // 搜索高亮参数
const { user } = useAuth(); const { user } = useAuth();
const { setOptimizedPrompt } = usePromptOptimizer(); const { setOptimizedPrompt } = usePromptOptimizer();
const [sidebarOpen, setSidebarOpen] = useState(true); const [sidebarOpen, setSidebarOpen] = useState(true);
const messagesEndRef = useRef<HTMLDivElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null);
const messagesContainerRef = useRef<HTMLDivElement>(null);
const [isNewChat, setIsNewChat] = useState(false); const [isNewChat, setIsNewChat] = useState(false);
const [initialMessageSent, setInitialMessageSent] = useState(false); const [initialMessageSent, setInitialMessageSent] = useState(false);
// 高亮消息状态
const [highlightedMessageId, setHighlightedMessageId] = useState<string | null>(null);
// 标题下拉菜单状态 // 标题下拉菜单状态
const [titleMenuOpen, setTitleMenuOpen] = useState(false); const [titleMenuOpen, setTitleMenuOpen] = useState(false);
const [isEditingTitle, setIsEditingTitle] = useState(false); const [isEditingTitle, setIsEditingTitle] = useState(false);
@ -128,6 +133,36 @@ export default function ChatPage({ params }: PageProps) {
scrollToBottom(); scrollToBottom();
}, [messages]); }, [messages]);
// 处理搜索高亮:滚动到目标消息并高亮
useEffect(() => {
if (highlightParam && messages.length > 0) {
// 查找目标消息
const targetMessage = messages.find(m => m.id === highlightParam);
if (targetMessage) {
// 设置高亮状态
setHighlightedMessageId(highlightParam);
// 延迟滚动,确保 DOM 已渲染
setTimeout(() => {
const messageElement = document.querySelector(`[data-message-id="${highlightParam}"]`);
if (messageElement) {
messageElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 100);
// 清除 URL 中的 highlight 参数
router.replace(`/chat/${chatId}`, { scroll: false });
// 3秒后清除高亮状态
const timer = setTimeout(() => {
setHighlightedMessageId(null);
}, 3000);
return () => clearTimeout(timer);
}
}
}, [highlightParam, messages, chatId, router]);
// 聚焦标题输入框 // 聚焦标题输入框
useEffect(() => { useEffect(() => {
if (isEditingTitle && titleInputRef.current) { if (isEditingTitle && titleInputRef.current) {
@ -595,6 +630,7 @@ export default function ChatPage({ params }: PageProps) {
onSaveToNote={message.role === 'assistant' && !isStreaming ? handleSaveToNote : undefined} onSaveToNote={message.role === 'assistant' && !isStreaming ? handleSaveToNote : undefined}
onLinkClick={handleLinkClick} onLinkClick={handleLinkClick}
conversationId={chatId} conversationId={chatId}
isHighlighted={highlightedMessageId === message.id}
/> />
)) ))
)} )}

View File

@ -616,3 +616,36 @@ pre[class*="language-"] {
[data-theme="dark"] .token.italic { [data-theme="dark"] .token.italic {
font-style: italic; font-style: italic;
} }
/* ========================================
消息搜索高亮动画
用于搜索跳转后高亮显示目标消息
风格轻量闪烁类似搜索引擎高亮
======================================== */
@keyframes messageHighlight {
0% {
background-color: rgba(255, 235, 120, 0.6);
}
100% {
background-color: transparent;
}
}
.message-highlight {
animation: messageHighlight 2.5s ease-out forwards;
border-radius: var(--radius-md);
}
/* 暗色模式下使用更暗的黄色 */
[data-theme="dark"] .message-highlight {
animation-name: messageHighlightDark;
}
@keyframes messageHighlightDark {
0% {
background-color: rgba(255, 200, 50, 0.25);
}
100% {
background-color: transparent;
}
}

View File

@ -56,6 +56,8 @@ interface MessageBubbleProps {
onLinkClick?: (url: string) => void; onLinkClick?: (url: string) => void;
/** 对话 ID用于关联笔记来源 */ /** 对话 ID用于关联笔记来源 */
conversationId?: string; conversationId?: string;
/** 是否高亮显示(搜索跳转时使用) */
isHighlighted?: boolean;
} }
// 格式化文件大小 // 格式化文件大小
@ -75,7 +77,7 @@ function getDocumentIcon(type: string) {
return FileText; return FileText;
} }
export function MessageBubble({ message, user, thinkingContent, isStreaming, error, images, searchImages, searchVideos, uploadedImages, uploadedDocuments, usedTools, pyodideStatus, onRegenerate, onSaveToNote, onLinkClick, conversationId }: MessageBubbleProps) { export function MessageBubble({ message, user, thinkingContent, isStreaming, error, images, searchImages, searchVideos, uploadedImages, uploadedDocuments, usedTools, pyodideStatus, onRegenerate, onSaveToNote, onLinkClick, conversationId, isHighlighted }: MessageBubbleProps) {
const isUser = message.role === 'user'; const isUser = message.role === 'user';
const [thinkingExpanded, setThinkingExpanded] = useState(false); const [thinkingExpanded, setThinkingExpanded] = useState(false);
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
@ -138,7 +140,13 @@ export function MessageBubble({ message, user, thinkingContent, isStreaming, err
if (isUser) { if (isUser) {
return ( return (
<div className="flex justify-end items-start gap-3 mb-8 animate-fade-in group"> <div
data-message-id={message.id}
className={cn(
"flex justify-end items-start gap-3 mb-8 animate-fade-in group",
isHighlighted && "message-highlight"
)}
>
<div className="max-w-[70%] relative"> <div className="max-w-[70%] relative">
{/* 用户上传的图片 */} {/* 用户上传的图片 */}
{uploadedImages && uploadedImages.length > 0 && ( {uploadedImages && uploadedImages.length > 0 && (
@ -227,7 +235,13 @@ export function MessageBubble({ message, user, thinkingContent, isStreaming, err
} }
return ( return (
<div className="flex items-start gap-4 mb-8 animate-fade-in"> <div
data-message-id={message.id}
className={cn(
"flex items-start gap-4 mb-8 animate-fade-in",
isHighlighted && "message-highlight"
)}
>
{/* AI 图标 */} {/* AI 图标 */}
<div className="flex-shrink-0 mt-4"> <div className="flex-shrink-0 mt-4">
<AILogo size={28} /> <AILogo size={28} />