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:
2026-11-09 00:50:09 +08:00
co-authored by Claude Opus 5.5
parent 182ca9cf05
commit b84fcec77b
28 changed files with 1592 additions and 282 deletions
+90 -6
View File
@@ -50,8 +50,8 @@ export function useNavigation({
// 预取路由
useEffect(() => {
if (!neighbors) return;
if (neighbors.next) router.prefetch(`/aweme/${neighbors.next.aweme_id}`);
if (neighbors.prev) router.prefetch(`/aweme/${neighbors.prev.aweme_id}`);
if (neighbors.next) router.prefetch(neighbors.next.href);
if (neighbors.prev) router.prefetch(neighbors.prev.href);
}, [neighbors, router]);
// 鼠标滚轮切换
@@ -72,10 +72,10 @@ export function useNavigation({
if (dy > 0 && neighbors?.next) {
wheelCooldownRef.current = now;
router.push(`/aweme/${neighbors.next.aweme_id}`);
router.push(neighbors.next.href);
} else if (dy < 0 && neighbors?.prev) {
wheelCooldownRef.current = now;
router.push(`/aweme/${neighbors.prev.aweme_id}`);
router.push(neighbors.prev.href);
}
};
@@ -83,6 +83,90 @@ export function useNavigation({
return () => el.removeEventListener("wheel", onWheel);
}, [neighbors, mediaContainerRef, router]);
// 触屏上下滑切换(上滑下一条,下滑上一条)。只认明显的竖向手势,
// 不影响图文的左右滑动;从按钮、链接、控制栏(data-no-swipe)上开始的触摸不处理。
useEffect(() => {
const el = mediaContainerRef.current;
if (!el) return;
let start: { x: number; y: number; t: number } | null = null;
let vertical = false;
const onTouchStart = (e: TouchEvent): void => {
const target = e.target as HTMLElement | null;
if (
e.touches.length !== 1 ||
target?.closest(
"button, a, input, textarea, select, [role='slider'], [data-no-swipe]",
)
) {
start = null;
return;
}
const touch = e.touches[0];
start = { x: touch.clientX, y: touch.clientY, t: performance.now() };
vertical = false;
};
const onTouchMove = (e: TouchEvent): void => {
if (!start) return;
if (e.touches.length !== 1) {
start = null;
return;
}
const touch = e.touches[0];
const dx = touch.clientX - start.x;
const dy = touch.clientY - start.y;
if (!vertical && Math.abs(dy) > 12 && Math.abs(dy) > Math.abs(dx) * 1.5) {
vertical = true;
}
// 竖向手势由我们处理,阻止浏览器下拉刷新 / 回弹
if (vertical && e.cancelable) e.preventDefault();
};
const onTouchEnd = (e: TouchEvent): void => {
if (!start) return;
const touch = e.changedTouches[0];
const dy = touch.clientY - start.y;
const elapsed = performance.now() - start.t;
const wasVertical = vertical;
start = null;
vertical = false;
if (!wasVertical) return;
// 快速轻扫,或者慢慢拖过屏幕高度的四分之一
const flick = Math.abs(dy) > 60 && elapsed < 800;
const longDrag = Math.abs(dy) > window.innerHeight * 0.25;
if (!flick && !longDrag) return;
const now = performance.now();
if (now - wheelCooldownRef.current < 500) return;
if (dy < 0 && neighbors?.next) {
wheelCooldownRef.current = now;
router.push(neighbors.next.href);
} else if (dy > 0 && neighbors?.prev) {
wheelCooldownRef.current = now;
router.push(neighbors.prev.href);
}
};
const onTouchCancel = (): void => {
start = null;
vertical = false;
};
el.addEventListener("touchstart", onTouchStart, { passive: true });
el.addEventListener("touchmove", onTouchMove, { passive: false });
el.addEventListener("touchend", onTouchEnd);
el.addEventListener("touchcancel", onTouchCancel);
return () => {
el.removeEventListener("touchstart", onTouchStart);
el.removeEventListener("touchmove", onTouchMove);
el.removeEventListener("touchend", onTouchEnd);
el.removeEventListener("touchcancel", onTouchCancel);
};
}, [neighbors, mediaContainerRef, router]);
// 键盘快捷键(支持向右箭头 / D 键区分短按与长按 2 倍速,以及视频 [ ] 设置 AB 循环)
useEffect(() => {
const clearLongPressTimer = (): void => {
@@ -124,7 +208,7 @@ export function useNavigation({
if (now - wheelCooldownRef.current < 700) return;
if (neighbors?.prev) {
wheelCooldownRef.current = now;
router.push(`/aweme/${neighbors.prev.aweme_id}`);
router.push(neighbors.prev.href);
}
} else if (key === "arrowdown" || key === "s" || code === "KeyS") {
e.preventDefault();
@@ -132,7 +216,7 @@ export function useNavigation({
if (now - wheelCooldownRef.current < 700) return;
if (neighbors?.next) {
wheelCooldownRef.current = now;
router.push(`/aweme/${neighbors.next.aweme_id}`);
router.push(neighbors.next.href);
}
} else if (key === "arrowleft" || key === "a" || code === "KeyA") {
e.preventDefault();