用户系统
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import { AuthProvider } from "@/lib/auth-context";
|
||||
import { AuthGuard } from "@/lib/auth-guard";
|
||||
import { AppProvider } from "@/lib/app-context";
|
||||
import "./globals.css";
|
||||
|
||||
@@ -22,7 +24,11 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<body className="antialiased">
|
||||
<AppProvider>{children}</AppProvider>
|
||||
<AuthProvider>
|
||||
<AuthGuard>
|
||||
<AppProvider>{children}</AppProvider>
|
||||
</AuthGuard>
|
||||
</AuthProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
96
art-agent/frontend/src/app/login/page.tsx
Normal file
96
art-agent/frontend/src/app/login/page.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
"use client";
|
||||
|
||||
import { useState, type FormEvent } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useAuth } from "@/lib/auth-context";
|
||||
|
||||
export default function LoginPage() {
|
||||
const { login, isAuthenticated } = useAuth();
|
||||
const router = useRouter();
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
if (isAuthenticated) {
|
||||
router.replace("/");
|
||||
return null;
|
||||
}
|
||||
|
||||
async function handleSubmit(e: FormEvent) {
|
||||
e.preventDefault();
|
||||
if (!username.trim() || !password) return;
|
||||
setError("");
|
||||
setLoading(true);
|
||||
try {
|
||||
await login(username.trim(), password);
|
||||
router.replace("/");
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "登录失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen flex items-center justify-center bg-[var(--bg-primary)]">
|
||||
<div className="w-full max-w-sm mx-4">
|
||||
<div className="text-center mb-8">
|
||||
<div className="text-4xl mb-3">🎨</div>
|
||||
<h1 className="text-2xl font-semibold text-[var(--text-primary)]">EPEEKit</h1>
|
||||
<p className="text-sm text-[var(--text-secondary)] mt-1">AI 美术资源生成工具</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="用户名"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
autoFocus
|
||||
autoComplete="username"
|
||||
className="w-full px-4 py-3 rounded-lg
|
||||
bg-[var(--bg-secondary)] border border-[var(--border)]
|
||||
text-[var(--text-primary)] placeholder:text-[var(--text-secondary)]
|
||||
focus:outline-none focus:border-[var(--accent)]
|
||||
transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
className="w-full px-4 py-3 rounded-lg
|
||||
bg-[var(--bg-secondary)] border border-[var(--border)]
|
||||
text-[var(--text-primary)] placeholder:text-[var(--text-secondary)]
|
||||
focus:outline-none focus:border-[var(--accent)]
|
||||
transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-sm text-red-400 bg-red-400/10 rounded-lg px-4 py-2.5">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !username.trim() || !password}
|
||||
className="w-full py-3 rounded-lg font-medium
|
||||
bg-[var(--accent)] text-white
|
||||
hover:bg-[var(--accent-hover)]
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
transition-colors cursor-pointer"
|
||||
>
|
||||
{loading ? "登录中..." : "登录"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user