加了一堆模型和一堆功能

This commit is contained in:
2026-04-16 00:36:21 +08:00
parent 47c0863bab
commit 40de992516
34 changed files with 3111 additions and 718 deletions

View File

@@ -0,0 +1,233 @@
"use client";
import { useEffect, useState, useCallback } from "react";
import { useAuth, type AuthUser } from "@/lib/auth-context";
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
interface MemoryItem {
id: string;
memory: string;
created_at?: string;
updated_at?: string;
}
interface GroupedMemories {
label: string;
items: MemoryItem[];
}
function groupByTime(memories: MemoryItem[]): GroupedMemories[] {
const now = new Date();
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const weekAgo = new Date(todayStart.getTime() - 7 * 24 * 60 * 60 * 1000);
const today: MemoryItem[] = [];
const week: MemoryItem[] = [];
const older: MemoryItem[] = [];
for (const m of memories) {
const d = m.created_at ? new Date(m.created_at) : null;
if (!d || isNaN(d.getTime())) {
older.push(m);
} else if (d >= todayStart) {
today.push(m);
} else if (d >= weekAgo) {
week.push(m);
} else {
older.push(m);
}
}
const groups: GroupedMemories[] = [];
if (today.length > 0) groups.push({ label: "今天", items: today });
if (week.length > 0) groups.push({ label: "最近 7 天", items: week });
if (older.length > 0) groups.push({ label: "更早", items: older });
return groups;
}
function formatDate(dateStr?: string): string {
if (!dateStr) return "";
const d = new Date(dateStr);
if (isNaN(d.getTime())) return "";
const now = new Date();
const isThisYear = d.getFullYear() === now.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
const hour = String(d.getHours()).padStart(2, "0");
const min = String(d.getMinutes()).padStart(2, "0");
if (isThisYear) return `${month}-${day} ${hour}:${min}`;
return `${d.getFullYear()}-${month}-${day} ${hour}:${min}`;
}
export function ProfileModal({ onClose }: { onClose: () => void }) {
const { user, token } = useAuth();
const [memories, setMemories] = useState<MemoryItem[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchMemories = useCallback(async () => {
if (!token) return;
setLoading(true);
setError(null);
try {
const resp = await fetch(`${API_URL}/api/memory/list`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const data = await resp.json();
setMemories(data.memories || []);
if (data.error) setError(data.error);
} catch (e) {
setError(e instanceof Error ? e.message : "加载记忆失败");
} finally {
setLoading(false);
}
}, [token]);
useEffect(() => {
fetchMemories();
}, [fetchMemories]);
useEffect(() => {
function handleKey(e: KeyboardEvent) {
if (e.key === "Escape") onClose();
}
document.addEventListener("keydown", handleKey);
return () => document.removeEventListener("keydown", handleKey);
}, [onClose]);
if (!user) return null;
const groups = groupByTime(memories);
return (
<div
className="fixed inset-0 z-[100] flex items-center justify-center"
onClick={onClose}
>
{/* 遮罩层 */}
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
{/* 弹窗主体 */}
<div
className="relative w-[90vw] max-w-[560px] max-h-[80vh] rounded-2xl glass-panel
shadow-[0_0_40px_rgba(77,184,164,0.1)] border-[var(--border-glow)]
flex flex-col overflow-hidden"
style={{ animation: "slideUp 200ms ease-out" }}
onClick={(e) => e.stopPropagation()}
>
{/* 关闭按钮 */}
<button
onClick={onClose}
className="absolute top-3 right-3 p-1.5 rounded-lg text-[var(--text-secondary)]
hover:text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)]
transition-colors cursor-pointer z-10"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M18 6L6 18M6 6l12 12" />
</svg>
</button>
{/* 用户信息区 */}
<div className="px-6 pt-6 pb-4 flex items-center gap-4 flex-shrink-0">
<div className="w-14 h-14 rounded-full bg-[var(--accent)]/15 border-2 border-[var(--accent)]/40
flex items-center justify-center text-[var(--accent)] text-xl font-bold
shadow-[0_0_20px_rgba(77,184,164,0.12)]">
{(user.display_name || user.username).charAt(0).toUpperCase()}
</div>
<div className="flex-1 min-w-0">
<h2 className="text-lg font-semibold text-[var(--text-primary)] truncate">
{user.display_name || user.username}
</h2>
<div className="flex items-center gap-2 mt-0.5">
<span className="text-sm text-[var(--text-secondary)]">@{user.username}</span>
{user.is_admin && (
<span className="text-xs px-1.5 py-0.5 rounded-md bg-[var(--accent)]/10
text-[var(--accent)] border border-[var(--accent)]/30">
</span>
)}
</div>
</div>
</div>
{/* 分割线 */}
<div className="mx-6 h-px bg-[var(--border)]" />
{/* 记忆区标题 */}
<div className="px-6 pt-4 pb-2 flex items-center gap-2 flex-shrink-0">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" strokeWidth="2">
<path d="M12 2a7 7 0 017 7c0 2.38-1.19 4.47-3 5.74V17a2 2 0 01-2 2h-4a2 2 0 01-2-2v-2.26C6.19 13.47 5 11.38 5 9a7 7 0 017-7z" />
<path d="M10 21h4" />
</svg>
<span className="text-sm font-medium text-[var(--text-primary)]"></span>
{!loading && (
<span className="text-xs text-[var(--text-secondary)]">
{memories.length}
</span>
)}
</div>
{/* 记忆列表 */}
<div className="flex-1 overflow-y-auto px-6 pb-6 min-h-0">
{loading && (
<div className="flex items-center justify-center py-12">
<div className="w-5 h-5 border-2 border-[var(--accent)]/30 border-t-[var(--accent)]
rounded-full animate-spin" />
<span className="ml-3 text-sm text-[var(--text-secondary)]">...</span>
</div>
)}
{!loading && error && memories.length === 0 && (
<div className="text-center py-12">
<p className="text-sm text-[var(--hot)]">{error}</p>
</div>
)}
{!loading && !error && memories.length === 0 && (
<div className="text-center py-12">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="var(--text-secondary)"
strokeWidth="1.5" className="mx-auto mb-3 opacity-50">
<path d="M12 2a7 7 0 017 7c0 2.38-1.19 4.47-3 5.74V17a2 2 0 01-2 2h-4a2 2 0 01-2-2v-2.26C6.19 13.47 5 11.38 5 9a7 7 0 017-7z" />
<path d="M10 21h4" />
</svg>
<p className="text-sm text-[var(--text-secondary)]"></p>
<p className="text-xs text-[var(--text-secondary)] mt-1 opacity-60">
</p>
</div>
)}
{!loading && groups.map((group) => (
<div key={group.label} className="mb-4 last:mb-0">
<div className="text-xs font-medium text-[var(--text-secondary)] uppercase tracking-wider mb-2">
{group.label}
</div>
<div className="space-y-1.5">
{group.items.map((item) => (
<div
key={item.id}
className="px-3 py-2.5 rounded-lg bg-[var(--bg-tertiary)]/50
border border-[var(--border)] hover:border-[var(--border-glow)]
transition-colors group"
>
<p className="text-sm text-[var(--text-primary)] leading-relaxed">
{item.memory}
</p>
{item.created_at && (
<p className="text-xs text-[var(--text-secondary)] mt-1 opacity-0
group-hover:opacity-60 transition-opacity">
{formatDate(item.created_at)}
</p>
)}
</div>
))}
</div>
</div>
))}
</div>
</div>
</div>
);
}