diff --git a/frontend/app/(main)/dashboard/page.tsx b/frontend/app/(main)/dashboard/page.tsx index 1f564c1..de03552 100644 --- a/frontend/app/(main)/dashboard/page.tsx +++ b/frontend/app/(main)/dashboard/page.tsx @@ -1,32 +1,112 @@ import { auth } from "@/auth"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Globe, ExternalLink } from "lucide-react"; +import Link from "next/link"; + +const SERVER_API_URL = process.env.SERVER_API_URL || "http://backend:8080"; + +interface Bookmark { + id: number; + title: string; + url: string; + description: string; + icon: string; + category: string; +} + +async function fetchPublicBookmarks(): Promise { + try { + const res = await fetch(`${SERVER_API_URL}/api/bookmarks/public`, { + next: { revalidate: 0 }, + }); + if (!res.ok) return []; + const data = await res.json(); + return data.bookmarks?.slice(0, 6) || []; + } catch { + return []; + } +} export default async function DashboardPage() { const session = await auth(); const user = session?.user as any; + const isAdmin = user?.role === "admin"; + const bookmarks = await fetchPublicBookmarks(); return (
-

欢迎,{user?.name || user?.email}

- - - 用户信息 - - -

- 用户名: - {user?.name} -

-

- 邮箱: - {user?.email} -

-

- 角色: - {user?.role} -

-
-
+

+ 欢迎,{user?.name || user?.email} +

+ +
+ + + 用户信息 + + +

+ 用户名: + {user?.name} +

+

+ 邮箱: + {user?.email} +

+

+ 角色: + {user?.role} +

+
+
+ + + + 最近书签 + {isAdmin && ( + + 管理 → + + )} + + + {bookmarks.length === 0 ? ( +

+ 还没有公开书签 +

+ ) : ( +
+ {bookmarks.map((bm) => ( + +
+ {bm.icon ? ( + + ) : ( + + )} +
+ {bm.title} + +
+ ))} +
+ )} +
+
+
); } diff --git a/frontend/app/(main)/layout.tsx b/frontend/app/(main)/layout.tsx index 6bee1a2..1bc098e 100644 --- a/frontend/app/(main)/layout.tsx +++ b/frontend/app/(main)/layout.tsx @@ -22,6 +22,11 @@ export default async function MainLayout({ 仪表盘 + {user?.role === "admin" && ( + + 书签 + + )}
{ "use server"; diff --git a/frontend/app/bookmarks/bookmark-manager.tsx b/frontend/app/bookmarks/bookmark-manager.tsx new file mode 100644 index 0000000..8a733b3 --- /dev/null +++ b/frontend/app/bookmarks/bookmark-manager.tsx @@ -0,0 +1,321 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { Card, CardContent } from "@/components/ui/card"; +import { + Pencil, + Trash2, + Plus, + ExternalLink, + Globe, +} from "lucide-react"; + +interface Bookmark { + id: number; + title: string; + url: string; + description: string; + icon: string; + category: string; + sortOrder: number; +} + +export function BookmarkManager() { + const [bookmarks, setBookmarks] = useState([]); + const [categories, setCategories] = useState([]); + const [loading, setLoading] = useState(true); + const [dialogOpen, setDialogOpen] = useState(false); + const [editing, setEditing] = useState(null); + + const [title, setTitle] = useState(""); + const [url, setUrl] = useState(""); + const [description, setDescription] = useState(""); + const [icon, setIcon] = useState(""); + const [category, setCategory] = useState(""); + const [sortOrder, setSortOrder] = useState(0); + + async function fetchBookmarks() { + setLoading(true); + try { + const res = await fetch("/api/proxy/bookmarks"); + if (!res.ok) throw new Error("failed to fetch"); + const data = await res.json(); + setBookmarks(data.bookmarks || []); + setCategories(data.categories || []); + } catch { + setBookmarks([]); + setCategories([]); + } finally { + setLoading(false); + } + } + + useEffect(() => { + fetchBookmarks(); + }, []); + + function openAdd() { + setEditing(null); + setTitle(""); + setUrl(""); + setDescription(""); + setIcon(""); + setCategory(""); + setSortOrder(0); + setDialogOpen(true); + } + + function openEdit(bm: Bookmark) { + setEditing(bm); + setTitle(bm.title); + setUrl(bm.url); + setDescription(bm.description); + setIcon(bm.icon); + setCategory(bm.category); + setSortOrder(bm.sortOrder); + setDialogOpen(true); + } + + async function handleSave() { + const payload = { + title, + url, + description, + icon, + category: category || "默认", + sortOrder, + }; + + try { + const res = editing + ? await fetch(`/api/proxy/bookmarks/${editing.id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }) + : await fetch("/api/proxy/bookmarks", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + + if (!res.ok) throw new Error("save failed"); + setDialogOpen(false); + fetchBookmarks(); + } catch { + alert("保存失败,请检查输入"); + } + } + + async function handleDelete(id: number) { + if (!confirm("确定要删除这个书签吗?")) return; + try { + const res = await fetch(`/api/proxy/bookmarks/${id}`, { + method: "DELETE", + }); + if (!res.ok) throw new Error("delete failed"); + fetchBookmarks(); + } catch { + alert("删除失败"); + } + } + + const grouped = bookmarks.reduce>((acc, bm) => { + const cat = bm.category || "默认"; + if (!acc[cat]) acc[cat] = []; + acc[cat].push(bm); + return acc; + }, {}); + + if (loading) { + return

加载中...

; + } + + return ( +
+
+

+ 共 {bookmarks.length} 个书签 +

+ +
+ + {bookmarks.length === 0 ? ( +
+ +

还没有书签

+ +
+ ) : ( + Object.entries(grouped).map(([cat, items]) => ( +
+

+ {cat} +

+
+ {items.map((bm) => ( + + +
+ {bm.icon ? ( + { + (e.target as HTMLImageElement).style.display = + "none"; + }} + /> + ) : ( + + )} +
+
+ + {bm.title} + + + {bm.description && ( +

+ {bm.description} +

+ )} +
+
+ + +
+
+
+ ))} +
+
+ )) + )} + + + + + + {editing ? "编辑书签" : "添加书签"} + + +
+
+ + setTitle(e.target.value)} + placeholder="例如:GitHub" + /> +
+
+ + setUrl(e.target.value)} + placeholder="https://github.com" + /> +
+
+ + setDescription(e.target.value)} + placeholder="简短描述" + /> +
+
+
+ + setCategory(e.target.value)} + placeholder="默认" + list="categories" + /> + + {categories.map((c) => ( + +
+
+ + + setSortOrder(Number(e.target.value)) + } + /> +
+
+
+ + setIcon(e.target.value)} + placeholder="https://example.com/favicon.ico" + /> +
+
+ + + + +
+
+
+ ); +} diff --git a/frontend/app/bookmarks/page.tsx b/frontend/app/bookmarks/page.tsx new file mode 100644 index 0000000..091ede8 --- /dev/null +++ b/frontend/app/bookmarks/page.tsx @@ -0,0 +1,22 @@ +import { auth } from "@/auth"; +import { redirect } from "next/navigation"; +import { BookmarkManager } from "./bookmark-manager"; + +export default async function BookmarksPage() { + const session = await auth(); + if (!session?.user) { + redirect("/login"); + } + + const role = (session.user as any)?.role; + if (role !== "admin") { + redirect("/unauthorized"); + } + + return ( +
+

书签管理

+ +
+ ); +} diff --git a/frontend/app/home-dock.tsx b/frontend/app/home-dock.tsx index b6dd2ab..4cc22d7 100644 --- a/frontend/app/home-dock.tsx +++ b/frontend/app/home-dock.tsx @@ -2,13 +2,15 @@ import { Dock, DockIcon } from "@/components/ui/dock"; import { signOut } from "next-auth/react"; -import { Home, User, LogOut, Download, BookOpen, LayoutDashboard } from "lucide-react"; +import { Home, User, LogOut, Download, BookOpen, LayoutDashboard, Bookmark } from "lucide-react"; export function HomeDock({ isAuthenticated, + isAdmin, onLoginClick, }: { isAuthenticated: boolean; + isAdmin: boolean; onLoginClick?: () => void; }) { return ( @@ -46,6 +48,13 @@ export function HomeDock({ )} + {isAdmin && ( + + + + + + )} {isAuthenticated ? (