- ShareModal: 分享弹窗组件,支持内容控制和二维码 - MessageSelector: 消息选择器,支持按轮次选择分享内容 - ShareNavigator: 分享页面导航组件,支持目录和返回顶部
177 lines
5.7 KiB
TypeScript
177 lines
5.7 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import { List, ChevronUp, X } from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
interface MessageAnchor {
|
|
id: string;
|
|
index: number;
|
|
preview: string;
|
|
}
|
|
|
|
interface ShareNavigatorProps {
|
|
messages: Array<{
|
|
id: string;
|
|
role: string;
|
|
content: string;
|
|
}>;
|
|
}
|
|
|
|
export function ShareNavigator({ messages }: ShareNavigatorProps) {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [showBackTop, setShowBackTop] = useState(false);
|
|
const [activeIndex, setActiveIndex] = useState<number | null>(null);
|
|
|
|
// 提取用户消息作为目录项
|
|
const userMessages: MessageAnchor[] = messages
|
|
.filter((msg) => msg.role === 'user')
|
|
.map((msg, idx) => ({
|
|
id: msg.id,
|
|
index: idx + 1,
|
|
preview: msg.content.slice(0, 30) + (msg.content.length > 30 ? '...' : ''),
|
|
}));
|
|
|
|
// 监听滚动,显示/隐藏回到顶部按钮
|
|
useEffect(() => {
|
|
const handleScroll = () => {
|
|
setShowBackTop(window.scrollY > 500);
|
|
|
|
// 更新当前可视消息
|
|
const messageElements = document.querySelectorAll('[data-msg-index]');
|
|
let currentIndex: number | null = null;
|
|
|
|
messageElements.forEach((el) => {
|
|
const rect = el.getBoundingClientRect();
|
|
if (rect.top <= 200 && rect.bottom > 0) {
|
|
currentIndex = parseInt(el.getAttribute('data-msg-index') || '0');
|
|
}
|
|
});
|
|
|
|
setActiveIndex(currentIndex);
|
|
};
|
|
|
|
window.addEventListener('scroll', handleScroll);
|
|
handleScroll(); // 初始化
|
|
|
|
return () => window.removeEventListener('scroll', handleScroll);
|
|
}, []);
|
|
|
|
// 跳转到指定消息
|
|
const scrollToMessage = useCallback((id: string, index: number) => {
|
|
const element = document.getElementById(`msg-${id}`);
|
|
if (element) {
|
|
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
// 更新 URL hash
|
|
window.history.replaceState(null, '', `#msg-${index}`);
|
|
}
|
|
setIsOpen(false);
|
|
}, []);
|
|
|
|
// 回到顶部
|
|
const scrollToTop = useCallback(() => {
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
window.history.replaceState(null, '', window.location.pathname);
|
|
}, []);
|
|
|
|
// 如果用户消息少于 2 条,不显示导航
|
|
if (userMessages.length < 2) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{/* 浮动导航按钮组 */}
|
|
<div className="fixed right-6 bottom-6 z-50 flex flex-col items-end gap-3">
|
|
{/* 回到顶部按钮 */}
|
|
{showBackTop && (
|
|
<button
|
|
onClick={scrollToTop}
|
|
className="w-10 h-10 bg-white border border-gray-200 rounded-full shadow-lg flex items-center justify-center text-gray-600 hover:bg-gray-50 hover:text-[#DB6639] transition-all"
|
|
title="回到顶部"
|
|
>
|
|
<ChevronUp size={20} />
|
|
</button>
|
|
)}
|
|
|
|
{/* 目录按钮 */}
|
|
<button
|
|
onClick={() => setIsOpen(!isOpen)}
|
|
className={cn(
|
|
'w-10 h-10 rounded-full shadow-lg flex items-center justify-center transition-all',
|
|
isOpen
|
|
? 'bg-[#DB6639] text-white'
|
|
: 'bg-white border border-gray-200 text-gray-600 hover:bg-gray-50 hover:text-[#DB6639]'
|
|
)}
|
|
title="消息目录"
|
|
>
|
|
{isOpen ? <X size={20} /> : <List size={20} />}
|
|
</button>
|
|
</div>
|
|
|
|
{/* 目录面板 */}
|
|
{isOpen && (
|
|
<>
|
|
{/* 背景遮罩 */}
|
|
<div
|
|
className="fixed inset-0 z-40"
|
|
onClick={() => setIsOpen(false)}
|
|
/>
|
|
|
|
{/* 目录内容 */}
|
|
<div className="fixed right-6 bottom-20 z-50 w-72 max-h-[60vh] bg-white rounded-lg shadow-xl border border-gray-200 overflow-hidden">
|
|
{/* 头部 */}
|
|
<div className="px-4 py-3 border-b border-gray-100 bg-gray-50">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm font-medium text-gray-700">
|
|
消息目录
|
|
</span>
|
|
<span className="text-xs text-gray-500">
|
|
{userMessages.length} 条问题
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 目录列表 */}
|
|
<div className="overflow-y-auto max-h-[calc(60vh-48px)]">
|
|
{userMessages.map((msg) => (
|
|
<button
|
|
key={msg.id}
|
|
onClick={() => scrollToMessage(msg.id, msg.index)}
|
|
className={cn(
|
|
'w-full px-4 py-3 text-left hover:bg-gray-50 transition-colors border-b border-gray-50 last:border-b-0',
|
|
activeIndex === msg.index && 'bg-orange-50 border-l-2 border-l-[#DB6639]'
|
|
)}
|
|
>
|
|
<div className="flex items-start gap-2">
|
|
<span
|
|
className={cn(
|
|
'flex-shrink-0 w-5 h-5 rounded-full text-xs flex items-center justify-center mt-0.5',
|
|
activeIndex === msg.index
|
|
? 'bg-[#DB6639] text-white'
|
|
: 'bg-gray-200 text-gray-600'
|
|
)}
|
|
>
|
|
{msg.index}
|
|
</span>
|
|
<span
|
|
className={cn(
|
|
'text-sm leading-relaxed',
|
|
activeIndex === msg.index
|
|
? 'text-[#DB6639] font-medium'
|
|
: 'text-gray-700'
|
|
)}
|
|
>
|
|
{msg.preview}
|
|
</span>
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
}
|