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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user