Add admin pinning and user favorites with role management

This commit is contained in:
爱喝水的木子
2026-03-20 13:55:27 +08:00
parent e6788d0e8f
commit 8e6bd210a8
19 changed files with 629 additions and 101 deletions

View File

@@ -4,8 +4,9 @@ import { AdminUserManager } from "@/components/AdminUserManager";
import { CreatePostForm } from "@/components/CreatePostForm";
import { cookieName, isAdminSession, verifySession } from "@/lib/auth";
import { getDb } from "@/lib/mongo";
import { buildOwnedPostFilter, serializePost } from "@/lib/posts";
import { buildOwnedPostFilter, buildPinnedSort, serializePost } from "@/lib/posts";
import { findUserById, getEffectiveDailyPostLimit, getShanghaiDayRange } from "@/lib/users";
import { Post } from "@/types/post";
export const dynamic = "force-dynamic";
@@ -13,11 +14,17 @@ type ManagedUser = {
id: string;
username: string;
displayName: string;
role: "admin" | "user";
role: "user" | "sponsor" | "admin";
dailyPostLimit: number;
postCount: number;
todayPostCount: number;
posts: Array<{ slug: string; title: string; createdAt: string }>;
posts: Array<{ slug: string; title: string; createdAt: string; isPinned?: boolean }>;
};
const ROLE_LABELS: Record<ManagedUser["role"], string> = {
user: "普通",
sponsor: "赞助",
admin: "管理员"
};
async function fetchRecentPosts(session: Awaited<ReturnType<typeof verifySession>>) {
@@ -25,7 +32,7 @@ async function fetchRecentPosts(session: Awaited<ReturnType<typeof verifySession
const posts = await db
.collection("posts")
.find(buildOwnedPostFilter(session), { projection: { markdown: 0 } })
.sort({ createdAt: -1 })
.sort(buildPinnedSort())
.limit(20)
.toArray();
@@ -38,6 +45,33 @@ async function fetchRecentPosts(session: Awaited<ReturnType<typeof verifySession
}));
}
async function fetchFavoritePosts(session: Awaited<ReturnType<typeof verifySession>>): Promise<Post[]> {
if (!session?.uid) {
return [];
}
const db = await getDb();
const favorites = await db
.collection("favorites")
.find({ ownerId: session.uid }, { projection: { postSlug: 1, createdAt: 1 } })
.sort({ createdAt: -1 })
.limit(20)
.toArray();
const slugs = favorites.map((item: any) => item.postSlug).filter(Boolean);
if (slugs.length === 0) {
return [];
}
const posts = await db
.collection("posts")
.find({ slug: { $in: slugs } }, { projection: { markdown: 0 } })
.toArray();
const postMap = new Map(posts.map((post: any) => [post.slug, serializePost(post)]));
return slugs.map((slug) => postMap.get(slug)).filter(Boolean) as Post[];
}
async function fetchAvailableTags(session: Awaited<ReturnType<typeof verifySession>>) {
const db = await getDb();
const tags = await db
@@ -90,8 +124,20 @@ async function fetchManagedUsers(): Promise<ManagedUser[]> {
const posts = await db
.collection("posts")
.find({}, { projection: { slug: 1, title: 1, createdAt: 1, ownerId: 1, author: 1 } })
.sort({ createdAt: -1 })
.find(
{},
{
projection: {
slug: 1,
title: 1,
createdAt: 1,
ownerId: 1,
author: 1,
isPinned: 1
}
}
)
.sort(buildPinnedSort())
.toArray();
const authorToUserId = new Map<string, string>();
@@ -104,7 +150,10 @@ async function fetchManagedUsers(): Promise<ManagedUser[]> {
const postCountMap = new Map<string, number>();
const todayCountMap = new Map<string, number>();
const postsByOwner = new Map<string, Array<{ slug: string; title: string; createdAt: string }>>();
const postsByOwner = new Map<
string,
Array<{ slug: string; title: string; createdAt: string; isPinned?: boolean }>
>();
posts.forEach((post: any) => {
const resolvedOwnerId =
@@ -115,7 +164,8 @@ async function fetchManagedUsers(): Promise<ManagedUser[]> {
list.push({
slug: post.slug,
title: post.title ?? "未命名",
createdAt: post.createdAt ?? new Date().toISOString()
createdAt: post.createdAt ?? new Date().toISOString(),
isPinned: Boolean(post.isPinned)
});
postsByOwner.set(resolvedOwnerId, list);
postCountMap.set(resolvedOwnerId, (postCountMap.get(resolvedOwnerId) ?? 0) + 1);
@@ -134,7 +184,10 @@ async function fetchManagedUsers(): Promise<ManagedUser[]> {
id,
username: user.username ?? "",
displayName: user.displayName ?? user.username ?? "",
role: user.role === "admin" ? "admin" : "user",
role:
user.role === "admin" || user.role === "sponsor" || user.role === "user"
? user.role
: "user",
dailyPostLimit: getEffectiveDailyPostLimit(user),
postCount: postCountMap.get(id) ?? 0,
todayPostCount: todayCountMap.get(id) ?? 0,
@@ -148,9 +201,11 @@ export default async function AdminPage() {
const token = cookies().get(cookieName)?.value;
const session = await verifySession(token);
const adminView = isAdminSession(session);
const roleLabel = ROLE_LABELS[(session?.role as ManagedUser["role"]) || "user"];
const [recentPosts, availableTags, publishQuota, managedUsers] = await Promise.all([
const [recentPosts, favoritePosts, availableTags, publishQuota, managedUsers] = await Promise.all([
fetchRecentPosts(session),
fetchFavoritePosts(session),
fetchAvailableTags(session),
fetchPublishQuota(session),
adminView ? fetchManagedUsers() : Promise.resolve([] as ManagedUser[])
@@ -159,11 +214,24 @@ export default async function AdminPage() {
return (
<div className="space-y-6">
<section className="rounded-2xl bg-white/80 p-5 shadow-sm ring-1 ring-slate-100">
<div className="space-y-2">
<h1 className="text-2xl font-semibold text-slate-900"></h1>
<p className="text-sm text-slate-500">
</p>
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="space-y-2">
<h1 className="text-2xl font-semibold text-slate-900"></h1>
<p className="text-sm text-slate-500">
/
</p>
</div>
<div className="flex items-center gap-3">
<span className="rounded-full bg-slate-100 px-3 py-1 text-sm text-slate-700 ring-1 ring-slate-200">
{session?.name || "未登录"} · {roleLabel}
</span>
<a
href="/stats"
className="rounded-full bg-brand-50 px-4 py-2 text-sm font-medium text-brand-700 ring-1 ring-brand-100 hover:bg-brand-100"
>
</a>
</div>
</div>
</section>
@@ -173,7 +241,21 @@ export default async function AdminPage() {
todayCount={publishQuota.todayCount}
/>
<AdminPostList initialPosts={recentPosts} canDelete={false} />
<AdminPostList
initialPosts={recentPosts}
title="我的内容"
description="你只能编辑自己的内容;管理员可在这里快速置顶或删除自己的内容。"
canDelete={adminView}
canPin={adminView}
/>
<AdminPostList
initialPosts={favoritePosts}
title="我的收藏"
description="收藏仅自己可见,方便回看喜欢的内容。"
emptyText="你还没有收藏任何内容。"
showEdit={false}
/>
{adminView ? <AdminUserManager initialUsers={managedUsers} currentUserId={session?.uid || ""} /> : null}
</div>