Files
douyin-archive/app/aweme/[awemeId]/page.tsx
T
feie9454andClaude Opus 5.5 b84fcec77b 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>
2026-11-09 00:50:09 +08:00

166 lines
4.9 KiB
TypeScript

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";
export async function generateMetadata({
params,
}: {
params: Promise<{ awemeId: string }>;
}): Promise<Metadata> {
const id = (await params).awemeId;
const [video, post] = await Promise.all([
prisma.video.findUnique({
where: { aweme_id: id },
select: { desc: true, author: { select: { nickname: true } } },
}),
prisma.imagePost.findUnique({
where: { aweme_id: id },
select: { desc: true, author: { select: { nickname: true } } },
}),
]);
const data = video || post;
if (!data) {
return {
title: "作品不存在",
};
}
const desc = data.desc || "查看作品详情";
const author = data.author.nickname;
const title =
desc.length > 50
? `${desc.slice(0, 50)}... - ${author}`
: `${desc} - ${author}`;
return {
title,
description: desc,
};
}
export default async function AwemeDetail({
params,
searchParams,
}: {
params: Promise<{ awemeId: string }>;
searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
const id = (await params).awemeId;
const [video, post] = await Promise.all([
prisma.video.findUnique({
where: { aweme_id: id },
include: { author: true, transcript: true },
}),
prisma.imagePost.findUnique({
where: { aweme_id: id },
include: { author: true, images: { orderBy: { order: "asc" } } },
}),
]);
if (!video && !post) return <main className="p-8">找不到该作品</main>;
const isVideo = Boolean(video);
// 获取评论总数
const commentsCount = await prisma.comment.count({
where: isVideo ? { videoId: id } : { imagePostId: id },
});
let data: AwemeData;
let transcript: VideoTranscript | null = null;
if (video) {
transcript = video.transcript ?? null;
data = {
type: "video",
aweme_id: video.aweme_id,
desc: video.desc,
created_at: video.created_at,
likesCount: Number(video.digg_count),
commentsCount,
author: {
nickname: video.author.nickname,
avatar_url: getFileUrl(video.author.avatar_url || "default-avatar.png"),
sec_uid: video.author.sec_uid,
},
cover_url: getFileUrl(video.cover_url ?? "default-cover.png"),
cover_size: { h: video.height ?? 0, w: video.width ?? 0 },
duration_ms: video.duration_ms,
video_url: getFileUrl(video.video_url),
width: video.width ?? null,
height: video.height ?? null,
content_tags: video.manual_content_tags,
manual_content_tags: video.manual_content_tags,
};
} else if (post) {
const coverImage = post.images[0];
data = {
type: "image",
aweme_id: post.aweme_id,
desc: post.desc,
created_at: post.created_at,
likesCount: Number(post.digg_count),
commentsCount,
author: {
nickname: post.author.nickname,
avatar_url: getFileUrl(post.author.avatar_url || "default-avatar.png"),
sec_uid: post.author.sec_uid,
},
cover_url: getFileUrl(coverImage?.url ?? "default-cover.png"),
cover_size: {
h: coverImage?.height ?? 0,
w: coverImage?.width ?? 0,
},
images: post.images.map((img) => ({
...img,
url: getFileUrl(img.url),
animated: img.animated ? getFileUrl(img.animated) : null,
})),
music_url: getFileUrl(post.music_url || "default-music.mp3"),
};
} else {
return <main className="p-8">找不到该作品</main>;
}
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,
]);
return (
<main className="min-h-screen w-full">
{/* 顶部条改为悬浮在媒体区域之上,避免 sticky 造成 Y 方向滚动条 */}
<div className="fixed left-3 top-3 z-30">
<BackButton
hrefFallback="/"
className="inline-flex items-center justify-center w-9 h-9 rounded-full bg-white/15 text-white border border-white/20 backdrop-blur hover:bg-white/25"
/>
</div>
<AwemeDetailClient
data={data}
neighbors={playback.neighbors}
playbackContext={playback.context}
contextQuery={playback.contextQuery}
authorWorksCount={authorWorksCount}
transcript={transcript}
/>
</main>
);
}
export const dynamic = "force-dynamic";