- 播放上下文:作品链接带上来源(搜索、标签、作者、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>
83 lines
2.9 KiB
TypeScript
83 lines
2.9 KiB
TypeScript
// 播放上下文:播放页「上一条 / 下一条」在哪个列表里移动。
|
|
// 它放在 /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}`;
|
|
}
|