- 播放上下文:作品链接带上来源(搜索、标签、作者、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>
166 lines
4.9 KiB
TypeScript
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";
|