feat(chat): 实现多视图滚动位置保存与恢复功能
- 在 App 组件中新增 viewKey,用于标识当前视图 - 通过属性链传递 viewKey 至 MessageList 以区分不同滚动上下文 - MessageList 中新增滚动位置缓存机制,使用 Map 保存各视图的滚动状态 - 在视图切换时自动恢复对应滚动位置,提升用户体验 - 保持消息新增时的自动滚动行为,兼顾视图切换与消息更新场景
This commit is contained in:
parent
bf724b133c
commit
8684ff9549
@ -474,6 +474,13 @@ function App() {
|
||||
return result
|
||||
}, [messages])
|
||||
|
||||
// 视图标识:用于 MessageList 保存/恢复每个视图的滚动位置
|
||||
const viewKey = useMemo(() => {
|
||||
if (schedulerView) return `scheduler:${schedulerView.jobId}`
|
||||
if (subAgentView) return `subagent:${subAgentView.taskId}`
|
||||
return 'main'
|
||||
}, [schedulerView, subAgentView])
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col bg-[var(--bg-primary)] text-[var(--text-primary)] overflow-hidden">
|
||||
{/* Header */}
|
||||
@ -686,6 +693,7 @@ function App() {
|
||||
onNavigateToSubAgent={handleNavigateToSubAgent}
|
||||
onStop={handleStopExecution}
|
||||
showThinking={showThinking}
|
||||
viewKey={viewKey}
|
||||
todoPanel={
|
||||
<TodoPanel
|
||||
todos={todos}
|
||||
|
||||
@ -13,6 +13,8 @@ interface ChatContainerProps {
|
||||
showThinking?: boolean
|
||||
/** 浮动待办面板,绝对定位在消息区域上方 */
|
||||
todoPanel?: React.ReactNode
|
||||
/** 视图标识,用于保存/恢复滚动位置 */
|
||||
viewKey?: string
|
||||
}
|
||||
|
||||
export function ChatContainer({
|
||||
@ -25,11 +27,12 @@ export function ChatContainer({
|
||||
onStop,
|
||||
showThinking = true,
|
||||
todoPanel,
|
||||
viewKey,
|
||||
}: ChatContainerProps) {
|
||||
return (
|
||||
<div className="flex h-full flex-col relative">
|
||||
<div className="flex-1 overflow-hidden relative">
|
||||
<MessageList messages={messages} onNavigateToSubAgent={onNavigateToSubAgent} showThinking={showThinking} />
|
||||
<MessageList messages={messages} onNavigateToSubAgent={onNavigateToSubAgent} showThinking={showThinking} viewKey={viewKey} />
|
||||
{todoPanel}
|
||||
</div>
|
||||
<MessageInput
|
||||
|
||||
@ -7,14 +7,22 @@ interface MessageListProps {
|
||||
messages: ChatMessage[]
|
||||
onNavigateToSubAgent?: (taskId: string, description: string) => void
|
||||
showThinking?: boolean
|
||||
/** 视图标识,用于保存/恢复滚动位置。不同视图间切换时保持各自的滚动位置。 */
|
||||
viewKey?: string
|
||||
}
|
||||
|
||||
export function MessageList({ messages, onNavigateToSubAgent, showThinking = true }: MessageListProps) {
|
||||
export function MessageList({ messages, onNavigateToSubAgent, showThinking = true, viewKey }: MessageListProps) {
|
||||
const bottomRef = useRef<HTMLDivElement>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const isAtBottomRef = useRef(true)
|
||||
const prevShowBottomRef = useRef(false)
|
||||
const prevShowTopRef = useRef(false)
|
||||
const prevViewKeyRef = useRef(viewKey)
|
||||
const viewKeyRef = useRef(viewKey)
|
||||
viewKeyRef.current = viewKey
|
||||
|
||||
// Per-view scroll position memory
|
||||
const scrollPositionsRef = useRef<Map<string, number>>(new Map())
|
||||
|
||||
const [showScrollToBottom, setShowScrollToBottom] = useState(false)
|
||||
const [showScrollToTop, setShowScrollToTop] = useState(false)
|
||||
@ -42,6 +50,12 @@ export function MessageList({ messages, onNavigateToSubAgent, showThinking = tru
|
||||
const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight
|
||||
const nearBottom = distanceFromBottom < 120
|
||||
|
||||
// Save scroll position for current view
|
||||
const key = viewKeyRef.current
|
||||
if (key) {
|
||||
scrollPositionsRef.current.set(key, el.scrollTop)
|
||||
}
|
||||
|
||||
isAtBottomRef.current = nearBottom
|
||||
|
||||
const shouldShowTop = el.scrollTop > el.clientHeight * 0.8
|
||||
@ -61,14 +75,36 @@ export function MessageList({ messages, onNavigateToSubAgent, showThinking = tru
|
||||
}
|
||||
}, [hasNewMessage])
|
||||
|
||||
// ---- auto-scroll: useLayoutEffect runs before browser processes scroll events ----
|
||||
// ---- auto-scroll: handle view switches and message updates ----
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const prevKey = prevViewKeyRef.current
|
||||
const viewChanged = prevKey !== viewKey
|
||||
prevViewKeyRef.current = viewKey
|
||||
|
||||
if (messages.length === 0) {
|
||||
isAtBottomRef.current = true
|
||||
return
|
||||
}
|
||||
|
||||
if (viewChanged) {
|
||||
// View switched (e.g. breadcrumb navigation): restore saved scroll position
|
||||
const key = viewKey ?? ''
|
||||
const savedPos = scrollPositionsRef.current.get(key)
|
||||
if (savedPos !== undefined && containerRef.current) {
|
||||
containerRef.current.scrollTop = savedPos
|
||||
const el = containerRef.current
|
||||
const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight
|
||||
isAtBottomRef.current = distanceFromBottom < 120
|
||||
return
|
||||
}
|
||||
// First time viewing this view: scroll to bottom
|
||||
isAtBottomRef.current = true
|
||||
bottomRef.current?.scrollIntoView({ behavior: 'instant' })
|
||||
return
|
||||
}
|
||||
|
||||
// Same view, messages changed: normal auto-scroll logic
|
||||
const lastMessage = messages[messages.length - 1]
|
||||
|
||||
if (lastMessage.role === 'user' || isAtBottomRef.current) {
|
||||
@ -76,7 +112,7 @@ export function MessageList({ messages, onNavigateToSubAgent, showThinking = tru
|
||||
} else {
|
||||
setHasNewMessage(true)
|
||||
}
|
||||
}, [messages])
|
||||
}, [messages, viewKey])
|
||||
|
||||
// ---- mount: always scroll to bottom if messages already loaded ----
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user