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:
@@ -0,0 +1,82 @@
|
||||
// 播放上下文:播放页「上一条 / 下一条」在哪个列表里移动。
|
||||
// 它放在 /aweme/<id> 的查询参数里,刷新、新标签页打开、后退都不会丢:
|
||||
// (无) 全部作品,按发布时间倒序(和首页一致)
|
||||
// ?ctx=tag&tag=美女 某个标签
|
||||
// ?ctx=author&author=<secUid> 某个作者的作品
|
||||
// ?ctx=search&q=百姓&tag=… 某次搜索的结果
|
||||
// ?ctx=hashtag&hashtag=丰川祥子 描述里带某个 hashtag 的作品
|
||||
// 这个文件只放纯函数,客户端组件也可以引用;查数据库的部分在 playback-context.server.ts。
|
||||
import { normalizeHashtag } from "@/lib/hashtag";
|
||||
import { isVideoContentTag } from "@/lib/video-tags";
|
||||
|
||||
export type PlaybackContext =
|
||||
| { kind: "search"; q: string; tag: string | null }
|
||||
| { kind: "tag"; tag: string }
|
||||
| { kind: "hashtag"; name: string }
|
||||
| { kind: "author"; secUid: string };
|
||||
|
||||
/** 播放页侧栏网格里的一项(/api/playlist 的返回) */
|
||||
export type PlaylistItem = {
|
||||
aweme_id: string;
|
||||
type: "video" | "image";
|
||||
desc: string;
|
||||
cover_url: string | null;
|
||||
likes: number;
|
||||
};
|
||||
|
||||
export type PlaylistResponse = { items: PlaylistItem[]; total: number };
|
||||
|
||||
type ParamSource =
|
||||
| URLSearchParams
|
||||
| Record<string, string | string[] | undefined>;
|
||||
|
||||
function readParam(source: ParamSource, key: string): string {
|
||||
const value =
|
||||
source instanceof URLSearchParams ? source.get(key) : source[key];
|
||||
return (Array.isArray(value) ? value[0] : value)?.trim() ?? "";
|
||||
}
|
||||
|
||||
export function parsePlaybackContext(
|
||||
source: ParamSource,
|
||||
): PlaybackContext | null {
|
||||
const kind = readParam(source, "ctx");
|
||||
const tag = readParam(source, "tag");
|
||||
const validTag = tag && isVideoContentTag(tag) ? tag : null;
|
||||
|
||||
if (kind === "search") {
|
||||
const q = readParam(source, "q");
|
||||
if (!q && !validTag) return null;
|
||||
return { kind: "search", q, tag: validTag };
|
||||
}
|
||||
if (kind === "tag" && validTag) return { kind: "tag", tag: validTag };
|
||||
if (kind === "hashtag") {
|
||||
const name = normalizeHashtag(readParam(source, "hashtag"));
|
||||
if (name) return { kind: "hashtag", name };
|
||||
}
|
||||
if (kind === "author") {
|
||||
const secUid = readParam(source, "author");
|
||||
if (secUid) return { kind: "author", secUid };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 生成带上下文的查询串(含开头的 ?);没有上下文时返回空串。 */
|
||||
export function playbackContextQuery(ctx: PlaybackContext | null): string {
|
||||
if (!ctx) return "";
|
||||
const params = new URLSearchParams({ ctx: ctx.kind });
|
||||
if (ctx.kind === "search") {
|
||||
if (ctx.q) params.set("q", ctx.q);
|
||||
if (ctx.tag) params.set("tag", ctx.tag);
|
||||
} else if (ctx.kind === "tag") {
|
||||
params.set("tag", ctx.tag);
|
||||
} else if (ctx.kind === "hashtag") {
|
||||
params.set("hashtag", ctx.name);
|
||||
} else {
|
||||
params.set("author", ctx.secUid);
|
||||
}
|
||||
return `?${params.toString()}`;
|
||||
}
|
||||
|
||||
export function awemeHref(awemeId: string, query = ""): string {
|
||||
return `/aweme/${awemeId}${query}`;
|
||||
}
|
||||
Reference in New Issue
Block a user