{author.nickname}
+{author.nickname}
++ 抖音号:{author.unique_id || author.short_id || '未知'} +
++ {author.signature} +
+ )} + +diff --git a/app/api/author/[secUid]/feed/route.ts b/app/api/author/[secUid]/feed/route.ts
new file mode 100644
index 0000000..aac8924
--- /dev/null
+++ b/app/api/author/[secUid]/feed/route.ts
@@ -0,0 +1,67 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { prisma } from '@/lib/prisma';
+import type { FeedItem, FeedResponse } from '@/app/types/feed';
+import { getFileUrl } from '@/lib/minio';
+
+export async function GET(req: NextRequest, { params }: { params: Promise<{ secUid: string }> }) {
+ const secUid = (await params).secUid;
+ const { searchParams } = new URL(req.url);
+ const limitParam = searchParams.get('limit');
+ const beforeParam = searchParams.get('before');
+
+ const limit = Math.min(Math.max(Number(limitParam ?? '24'), 1), 60); // 1..60
+ const before = beforeParam ? new Date(beforeParam) : null;
+
+ // fetch chunk from both tables
+ const [videos, posts] = await Promise.all([
+ prisma.video.findMany({
+ where: {
+ authorId: secUid,
+ ...(before ? { created_at: { lt: before } } : {})
+ },
+ orderBy: { created_at: 'desc' },
+ take: limit,
+ include: { author: true },
+ }),
+ prisma.imagePost.findMany({
+ where: {
+ authorId: secUid,
+ ...(before ? { created_at: { lt: before } } : {})
+ },
+ orderBy: { created_at: 'desc' },
+ take: limit,
+ include: { author: true, images: { orderBy: { order: 'asc' }, take: 1 } },
+ }),
+ ]);
+
+ const merged: FeedItem[] = [
+ ...videos.map((v) => ({
+ type: "video" as const,
+ aweme_id: v.aweme_id,
+ created_at: v.created_at,
+ desc: v.desc,
+ video_url: getFileUrl(v.video_url),
+ cover_url: getFileUrl(v.cover_url ?? 'default_cover.png'),
+ width: v.width ?? null,
+ height: v.height ?? null,
+ author: { nickname: v.author.nickname, avatar_url: getFileUrl(v.author.avatar_url ?? ''), sec_uid: v.author.sec_uid },
+ likes: Number(v.digg_count)
+ })),
+ ...posts.map((p) => ({
+ type: "image" as const,
+ aweme_id: p.aweme_id,
+ created_at: p.created_at,
+ desc: p.desc,
+ cover_url: getFileUrl(p.images?.[0]?.url ?? null),
+ width: p.images?.[0]?.width ?? null,
+ height: p.images?.[0]?.height ?? null,
+ author: { nickname: p.author.nickname, avatar_url: getFileUrl(p.author.avatar_url ?? ''), sec_uid: p.author.sec_uid },
+ likes: Number(p.digg_count)
+ })),
+ ].sort((a, b) => +new Date(b.created_at) - +new Date(a.created_at))
+ .slice(0, limit);
+
+ const nextCursor = merged.length > 0 ? new Date(merged[merged.length - 1].created_at as any).toISOString() : null;
+ const payload: FeedResponse = { items: merged, nextCursor };
+ return NextResponse.json(payload);
+}
diff --git a/app/api/feed/route.ts b/app/api/feed/route.ts
index 7989cbd..7c5a45f 100644
--- a/app/api/feed/route.ts
+++ b/app/api/feed/route.ts
@@ -41,7 +41,7 @@ export async function GET(req: NextRequest) {
cover_url: getFileUrl(v.cover_url ?? 'default_cover.png'),
width: v.width ?? null,
height: v.height ?? null,
- author: { nickname: v.author.nickname, avatar_url: getFileUrl(v.author.avatar_url ?? '') },
+ author: { nickname: v.author.nickname, avatar_url: getFileUrl(v.author.avatar_url ?? ''), sec_uid: v.author.sec_uid },
likes: Number(v.digg_count)
})),
...posts.map((p) => ({
@@ -52,7 +52,7 @@ export async function GET(req: NextRequest) {
cover_url: getFileUrl(p.images?.[0]?.url ?? null),
width: p.images?.[0]?.width ?? null,
height: p.images?.[0]?.height ?? null,
- author: { nickname: p.author.nickname, avatar_url: getFileUrl(p.author.avatar_url ?? '') },
+ author: { nickname: p.author.nickname, avatar_url: getFileUrl(p.author.avatar_url ?? ''), sec_uid: p.author.sec_uid },
likes: Number(p.digg_count)
})),
].sort((a, b) => +new Date(b.created_at) - +new Date(a.created_at))
diff --git a/app/author/[secUid]/page.tsx b/app/author/[secUid]/page.tsx
new file mode 100644
index 0000000..aae069d
--- /dev/null
+++ b/app/author/[secUid]/page.tsx
@@ -0,0 +1,123 @@
+import { prisma } from "@/lib/prisma";
+import { getFileUrl } from "@/lib/minio";
+import FeedMasonry from "@/app/components/FeedMasonry";
+import BackButton from "@/app/components/BackButton";
+import { FeedItem } from "@/app/types/feed";
+import { notFound } from "next/navigation";
+import Image from "next/image";
+
+export default async function AuthorPage({ params }: { params: Promise<{ secUid: string }> }) {
+ const secUid = (await params).secUid;
+
+ const author = await prisma.author.findUnique({
+ where: { sec_uid: secUid },
+ });
+
+ if (!author) {
+ notFound();
+ }
+
+ // Initial feed fetch
+ const limit = 24;
+ const [videos, posts] = await Promise.all([
+ prisma.video.findMany({
+ where: { authorId: secUid },
+ orderBy: { created_at: 'desc' },
+ take: limit,
+ include: { author: true },
+ }),
+ prisma.imagePost.findMany({
+ where: { authorId: secUid },
+ orderBy: { created_at: 'desc' },
+ take: limit,
+ include: { author: true, images: { orderBy: { order: 'asc' }, take: 1 } },
+ }),
+ ]);
+
+ const initialItems: FeedItem[] = [
+ ...videos.map((v) => ({
+ type: "video" as const,
+ aweme_id: v.aweme_id,
+ created_at: v.created_at,
+ desc: v.desc,
+ video_url: getFileUrl(v.video_url),
+ cover_url: getFileUrl(v.cover_url ?? 'default_cover.png'),
+ width: v.width ?? null,
+ height: v.height ?? null,
+ author: { nickname: v.author.nickname, avatar_url: getFileUrl(v.author.avatar_url ?? ''), sec_uid: v.author.sec_uid },
+ likes: Number(v.digg_count)
+ })),
+ ...posts.map((p) => ({
+ type: "image" as const,
+ aweme_id: p.aweme_id,
+ created_at: p.created_at,
+ desc: p.desc,
+ cover_url: getFileUrl(p.images?.[0]?.url ?? null),
+ width: p.images?.[0]?.width ?? null,
+ height: p.images?.[0]?.height ?? null,
+ author: { nickname: p.author.nickname, avatar_url: getFileUrl(p.author.avatar_url ?? ''), sec_uid: p.author.sec_uid },
+ likes: Number(p.digg_count)
+ })),
+ ].sort((a, b) => +new Date(b.created_at) - +new Date(a.created_at))
+ .slice(0, limit);
+
+ const initialCursor = initialItems.length > 0 ? new Date(initialItems[initialItems.length - 1].created_at as any).toISOString() : null;
+
+ return (
+
+ 抖音号:{author.unique_id || author.short_id || '未知'}
+
+ {author.signature}
+ {author.nickname}
+ {author.nickname}
+ 作品
+