feat: 播放页按来源列表切换,新增作者/搜索/hashtag 侧栏
- 播放上下文:作品链接带上来源(搜索、标签、作者、hashtag),上一条/下一条 在该列表内移动;当前作品不在列表中时退回全部作品的发布时间顺序 - 顶部提示标签显示当前列表与位置,点击打开侧栏列表,✕ 退出 - 侧栏改为「评论 | TA的作品 | 搜索结果/#hashtag」标签页,作品封面网格可就地切换 - 作者模式:打开「TA的作品」进入,关闭侧栏不退出,点提示 ✕ 后回到原来的列表 - 从搜索结果进入或点 hashtag 时自动打开对应列表 - 描述中的 hashtag 可点击,按完整 hashtag 精确匹配(#丰川祥子 不含 #丰川祥子cos) - 新增 /api/playlist;搜索 SQL 抽到 lib/search-query.ts 与播放页共用,保证顺序一致 - 移动端:底部抽屉、点作品后收起,媒体区支持上下滑切换 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import type { Metadata } from "next";
|
||||
import BackButton from "@/app/components/BackButton";
|
||||
import { getFileUrl } from "@/lib/minio";
|
||||
import { parsePlaybackContext } from "@/lib/playback-context";
|
||||
import { resolvePlayback } from "@/lib/playback-context.server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import AwemeDetailClient from "./Client";
|
||||
import type { AwemeData, VideoTranscript } from "./types";
|
||||
@@ -45,8 +47,10 @@ export async function generateMetadata({
|
||||
|
||||
export default async function AwemeDetail({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: Promise<{ awemeId: string }>;
|
||||
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
||||
}) {
|
||||
const id = (await params).awemeId;
|
||||
|
||||
@@ -71,12 +75,10 @@ export default async function AwemeDetail({
|
||||
});
|
||||
|
||||
let data: AwemeData;
|
||||
let currentCreatedAt: Date;
|
||||
let transcript: VideoTranscript | null = null;
|
||||
|
||||
if (video) {
|
||||
transcript = video.transcript ?? null;
|
||||
currentCreatedAt = video.created_at;
|
||||
data = {
|
||||
type: "video",
|
||||
aweme_id: video.aweme_id,
|
||||
@@ -100,7 +102,6 @@ export default async function AwemeDetail({
|
||||
};
|
||||
} else if (post) {
|
||||
const coverImage = post.images[0];
|
||||
currentCreatedAt = post.created_at;
|
||||
data = {
|
||||
type: "image",
|
||||
aweme_id: post.aweme_id,
|
||||
@@ -129,65 +130,16 @@ export default async function AwemeDetail({
|
||||
return <main className="p-8">找不到该作品</main>;
|
||||
}
|
||||
|
||||
// Compute prev/next neighbors by created_at across videos and image posts
|
||||
const [newerVideo, newerPost, olderVideo, olderPost] = await Promise.all([
|
||||
prisma.video.findFirst({
|
||||
where: { created_at: { gt: currentCreatedAt } },
|
||||
orderBy: { created_at: "asc" },
|
||||
select: { aweme_id: true, created_at: true },
|
||||
}),
|
||||
prisma.imagePost.findFirst({
|
||||
where: { created_at: { gt: currentCreatedAt } },
|
||||
orderBy: { created_at: "asc" },
|
||||
select: { aweme_id: true, created_at: true },
|
||||
}),
|
||||
prisma.video.findFirst({
|
||||
where: { created_at: { lt: currentCreatedAt } },
|
||||
orderBy: { created_at: "desc" },
|
||||
select: { aweme_id: true, created_at: true },
|
||||
}),
|
||||
prisma.imagePost.findFirst({
|
||||
where: { created_at: { lt: currentCreatedAt } },
|
||||
orderBy: { created_at: "desc" },
|
||||
select: { aweme_id: true, created_at: true },
|
||||
}),
|
||||
const authorId = video?.authorId ?? post?.authorId ?? null;
|
||||
const [playback, authorWorksCount] = await Promise.all([
|
||||
resolvePlayback(id, parsePlaybackContext(await searchParams)),
|
||||
authorId
|
||||
? Promise.all([
|
||||
prisma.video.count({ where: { authorId } }),
|
||||
prisma.imagePost.count({ where: { authorId } }),
|
||||
]).then(([videos, posts]) => videos + posts)
|
||||
: 0,
|
||||
]);
|
||||
const pickPrev = (() => {
|
||||
const cands: { aweme_id: string; created_at: Date }[] = [];
|
||||
if (newerVideo)
|
||||
cands.push({
|
||||
aweme_id: newerVideo.aweme_id,
|
||||
created_at: newerVideo.created_at,
|
||||
});
|
||||
if (newerPost)
|
||||
cands.push({
|
||||
aweme_id: newerPost.aweme_id,
|
||||
created_at: newerPost.created_at,
|
||||
});
|
||||
if (cands.length === 0) return null;
|
||||
cands.sort((a, b) => +a.created_at - +b.created_at);
|
||||
return { aweme_id: cands[0].aweme_id };
|
||||
})();
|
||||
const pickNext = (() => {
|
||||
const cands: { aweme_id: string; created_at: Date }[] = [];
|
||||
if (olderVideo)
|
||||
cands.push({
|
||||
aweme_id: olderVideo.aweme_id,
|
||||
created_at: olderVideo.created_at,
|
||||
});
|
||||
if (olderPost)
|
||||
cands.push({
|
||||
aweme_id: olderPost.aweme_id,
|
||||
created_at: olderPost.created_at,
|
||||
});
|
||||
if (cands.length === 0) return null;
|
||||
cands.sort((a, b) => +b.created_at - +a.created_at);
|
||||
return { aweme_id: cands[0].aweme_id };
|
||||
})();
|
||||
const neighbors: {
|
||||
prev: { aweme_id: string } | null;
|
||||
next: { aweme_id: string } | null;
|
||||
} = { prev: pickPrev, next: pickNext };
|
||||
|
||||
return (
|
||||
<main className="min-h-screen w-full">
|
||||
@@ -200,7 +152,10 @@ export default async function AwemeDetail({
|
||||
</div>
|
||||
<AwemeDetailClient
|
||||
data={data}
|
||||
neighbors={neighbors}
|
||||
neighbors={playback.neighbors}
|
||||
playbackContext={playback.context}
|
||||
contextQuery={playback.contextQuery}
|
||||
authorWorksCount={authorWorksCount}
|
||||
transcript={transcript}
|
||||
/>
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user