用户系统

This commit is contained in:
2026-04-15 00:21:43 +08:00
parent 97bbb3f306
commit 47c0863bab
23 changed files with 1640 additions and 56 deletions

View File

@@ -1,6 +1,6 @@
"use client";
import { useCallback, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { ChatMessages } from "@/components/chat/chat-messages";
import { ChatInput } from "@/components/chat/chat-input";
import { Sidebar } from "@/components/sidebar/sidebar";
@@ -23,21 +23,76 @@ export default function Home() {
setSidebarCollapsed,
} = useApp();
const messages = activeSession?.messages ?? [];
const [isLoading, setIsLoading] = useState(false);
const [streamingText, setStreamingText] = useState("");
const [streamingImages, setStreamingImages] = useState<ImageAsset[]>([]);
const [statusText, setStatusText] = useState("");
const [pendingAnnotation, setPendingAnnotation] = useState<AnnotationData | null>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const scrollPositions = useRef<Map<string, number>>(new Map());
const prevSessionId = useRef<string | null>(null);
const isSwitching = useRef(false);
const [showScrollBtn, setShowScrollBtn] = useState(false);
const scrollToBottom = () => {
setTimeout(() => {
scrollRef.current?.scrollTo({
top: scrollRef.current.scrollHeight,
behavior: "smooth",
});
}, 50);
};
const scrollToBottom = useCallback((instant?: boolean) => {
if (isSwitching.current && !instant) return;
const el = scrollRef.current;
if (!el) return;
if (instant) {
el.scrollTop = el.scrollHeight;
} else {
setTimeout(() => {
scrollRef.current?.scrollTo({
top: scrollRef.current.scrollHeight,
behavior: "smooth",
});
}, 50);
}
}, []);
// 实时记录当前会话的滚动位置 + 判断是否显示"回到底部"按钮
useEffect(() => {
const el = scrollRef.current;
if (!el || !activeSessionId) return;
const handler = () => {
if (!isSwitching.current) {
scrollPositions.current.set(activeSessionId, el.scrollTop);
}
const distFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
setShowScrollBtn(distFromBottom > 200);
};
el.addEventListener("scroll", handler, { passive: true });
return () => el.removeEventListener("scroll", handler);
}, [activeSessionId]);
// 会话切换:标记 switching等 DOM 更新后恢复位置
useEffect(() => {
if (!activeSessionId) return;
if (prevSessionId.current && prevSessionId.current !== activeSessionId) {
isSwitching.current = true;
}
prevSessionId.current = activeSessionId;
}, [activeSessionId]);
useEffect(() => {
if (!activeSessionId || !isSwitching.current) return;
requestAnimationFrame(() => {
const el = scrollRef.current;
if (!el) return;
const saved = scrollPositions.current.get(activeSessionId);
if (saved !== undefined) {
el.scrollTop = saved;
} else {
el.scrollTop = el.scrollHeight;
}
const distFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
setShowScrollBtn(distFromBottom > 200);
isSwitching.current = false;
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeSessionId, messages.length]);
const handleSend = useCallback(
async (text: string, refImageServerUrl: string | null, imageModel: string | null = null) => {
@@ -198,15 +253,13 @@ export default function Home() {
setStatusText("");
scrollToBottom();
},
[activeSession, activeSessionId, appendMessage, addAsset, updateSessionThumbnail, pendingAnnotation]
[activeSession, activeSessionId, appendMessage, addAsset, updateSessionThumbnail, pendingAnnotation, scrollToBottom]
);
const handleAnnotationComplete = useCallback((data: AnnotationData) => {
setPendingAnnotation(data);
}, []);
const messages = activeSession?.messages ?? [];
return (
<div className="h-screen flex flex-col">
<TopNav />
@@ -298,6 +351,24 @@ export default function Home() {
)}
</div>
{showScrollBtn && (
<button
onClick={() => scrollToBottom()}
className="absolute bottom-16 right-4 z-10
w-8 h-8 rounded-full flex items-center justify-center
bg-[var(--bg-tertiary)] border border-[var(--border)]
text-[var(--text-secondary)] hover:text-[var(--text-primary)]
hover:border-[var(--accent)] shadow-lg
transition-all duration-200 cursor-pointer
animate-[fadeIn_150ms_ease-out]"
title="回到底部"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="6 9 12 15 18 9" />
</svg>
</button>
)}
<ChatInput onSend={handleSend} disabled={isLoading} />
</main>