{{ timeDisplay[0] }}
{{ timeDisplay[1] }}
@@ -303,6 +301,9 @@ const resolveSavedPageIndex = (type: MobilePageType): number => {
// 默认落在「信息」页,并尽量恢复上次浏览的页面类型
const pageIndex = ref(resolveSavedPageIndex(savedPageType));
+const pageTransitionDisabled = ref(false);
+const pageSwipeBlocked = ref(false);
+let pageTransitionTimer = 0;
const lyricHeaderHorizontalPadding = computed(() => {
const padding = Math.max(0, settingStore.lyricHorizontalOffset);
@@ -326,15 +327,30 @@ const currentPageType = computed
(() => {
return "info";
});
-// 下拉关闭手势捕获区高度:信息页覆盖顶栏 + 封面区域;歌词页含顶栏 + 歌曲信息条;
-// 评论页仅顶栏(让出滚动空间给评论列表)
-const dragHandleHeight = computed(() => {
+// 下拉关闭手势捕获区:信息页覆盖顶栏 + 封面区域;歌词页限定在歌曲信息块
+const dragHandleStyle = computed(() => {
if (currentPageType.value === "info") {
- // 32vh 表示相对屏幕高度的比例热区,需走 --page-zoom-100vh 防止被 transform 二次缩放
- return "calc(40px + var(--mobile-safe-top) + var(--page-zoom-100vh, 100vh) * 0.32)";
+ return {
+ top: "0",
+ left: "0",
+ right: "0",
+ height: "calc(40px + var(--mobile-safe-top) + var(--page-zoom-100vh, 100vh) * 0.32)",
+ };
+ }
+ if (currentPageType.value === "lyric") {
+ return {
+ top: "calc(52px + var(--mobile-safe-top))",
+ left: "16px",
+ right: "72px",
+ height: "74px",
+ };
}
- if (currentPageType.value === "lyric") return "calc(140px + var(--mobile-safe-top))";
- return "calc(56px + var(--mobile-safe-top))";
+ return {
+ top: "0",
+ left: "0",
+ right: "0",
+ height: "calc(56px + var(--mobile-safe-top))",
+ };
});
// 顶部区域下拉关闭:整个全屏播放器(含背景蒙层)跟手下移,露出底部主页面
@@ -431,6 +447,7 @@ const DIRECTION_LOCK_TOLERANCE = 8;
const { lengthX: topLengthX, lengthY: topLengthY } = useSwipe(dragHandleRef, {
threshold: 0,
+ passive: true,
onSwipeStart: () => {
directionLock = null;
dragStarted = false;
@@ -500,9 +517,19 @@ const { lengthX: topLengthX, lengthY: topLengthY } = useSwipe(dragHandleRef, {
onBeforeUnmount(() => {
if (rafId) cancelAnimationFrame(rafId);
+ if (pageTransitionTimer) window.clearTimeout(pageTransitionTimer);
resetInlineStyles();
});
+const disablePageTransitionOnce = () => {
+ pageTransitionDisabled.value = true;
+ if (pageTransitionTimer) window.clearTimeout(pageTransitionTimer);
+ pageTransitionTimer = window.setTimeout(() => {
+ pageTransitionTimer = 0;
+ pageTransitionDisabled.value = false;
+ }, 80);
+};
+
const artistName = computed(() => {
const artists = musicStore.playSong.artists;
if (Array.isArray(artists)) {
@@ -513,6 +540,7 @@ const artistName = computed(() => {
// 页面可用性变化时按页面语义迁移 pageIndex
watch([hasComment, hasLyric], (_n, [prevHasComment, prevHasLyric]) => {
+ disablePageTransitionOnce();
const prevCommentIdx = prevHasComment ? 0 : -1;
const prevInfoIdx = prevHasComment ? 1 : 0;
const prevLyricIdx = prevHasLyric ? prevInfoIdx + 1 : -1;
@@ -529,9 +557,22 @@ watch(currentPageType, (t) => {
savedPageType = t;
});
+const isNoPageSwipeTarget = (event: TouchEvent) => {
+ const target = event.target;
+ return target instanceof HTMLElement && Boolean(target.closest("[data-no-page-swipe]"));
+};
+
const { direction, isSwiping, lengthX, lengthY } = useSwipe(mobileStart, {
threshold: 5,
+ passive: true,
+ onSwipeStart: (event) => {
+ pageSwipeBlocked.value = isNoPageSwipeTarget(event);
+ },
onSwipeEnd: () => {
+ if (pageSwipeBlocked.value) {
+ pageSwipeBlocked.value = false;
+ return;
+ }
if (totalPages.value <= 1) return;
// 仅在主方向为水平时触发翻页,避免上下滑动歌词/评论误触
if (Math.abs(lengthX.value) <= Math.abs(lengthY.value)) return;
@@ -546,7 +587,8 @@ const { direction, isSwiping, lengthX, lengthY } = useSwipe(mobileStart, {
// 当前滑动是否为水平方向(用于跟手位移)
const isHorizontalSwipe = computed(
- () => isSwiping.value && Math.abs(lengthX.value) > Math.abs(lengthY.value),
+ () =>
+ !pageSwipeBlocked.value && isSwiping.value && Math.abs(lengthX.value) > Math.abs(lengthY.value),
);
const contentTransform = computed(() => {
@@ -592,6 +634,7 @@ const contentTransform = computed(() => {
top: 0;
left: 0;
right: 0;
+ height: 0;
z-index: 5;
pointer-events: auto;
background: transparent;
@@ -664,7 +707,8 @@ const contentTransform = computed(() => {
// 横滑交给 useSwipe
touch-action: pan-y;
- &.swiping {
+ &.swiping,
+ &.no-transition {
transition: none;
}
diff --git a/src/components/Player/MainPlayer.vue b/src/components/Player/MainPlayer.vue
index 39a55f6ff..9eee92dff 100644
--- a/src/components/Player/MainPlayer.vue
+++ b/src/components/Player/MainPlayer.vue
@@ -309,6 +309,8 @@ let dragLastDy = 0;
let dragOpenTravel = 0;
let dragOpenResetTimer = 0;
let dragOpenCloseTimer = 0;
+let dragOpenSession = 0;
+let isPlayerMounted = true;
const OPEN_THRESHOLD = 100;
const setDragOpenFlag = (v: boolean) => {
@@ -339,6 +341,31 @@ const cancelDragOpenTimers = (flushClose = false) => {
if (flushClose) pendingCloseFinalize = null;
};
+const resetDragOpenMotion = () => {
+ if (dragOpenRaf) {
+ cancelAnimationFrame(dragOpenRaf);
+ dragOpenRaf = 0;
+ }
+ if (dragOpenParent) {
+ dragOpenParent.style.transition = "";
+ dragOpenParent.style.transform = "";
+ dragOpenParent.style.borderRadius = "";
+ dragOpenParent.style.transformOrigin = "";
+ dragOpenParent.style.willChange = "";
+ dragOpenParent.style.pointerEvents = "";
+ dragOpenParent.style.backfaceVisibility = "";
+ }
+ if (dragOpenMain) {
+ dragOpenMain.style.transition = "";
+ dragOpenMain.style.transform = "";
+ dragOpenMain.style.opacity = "";
+ dragOpenMain.style.willChange = "";
+ }
+ dragOpenParent = null;
+ dragOpenMain = null;
+ dragOpenPending = 0;
+};
+
const writeDragOpen = (dy: number) => {
const progress = Math.max(0, Math.min(1, dy / dragOpenTravel));
const translate = (1 - progress) * dragStartTop;
@@ -366,7 +393,9 @@ const scheduleDragOpenFlush = (dy: number) => {
// 此时直接放弃会导致 FullPlayer 被 onMobileEnter 的 100vh 起始 transform 卡住,
// 因此使用一次重试,最多 8 帧,仍找不到则放弃并复位 showFullPlayer。
let initDragOpenRetry = 0;
+let finishDragOpenRetry = 0;
const INIT_DRAG_OPEN_MAX_RETRY = 8;
+const FINISH_DRAG_OPEN_MAX_RETRY = 8;
const applyDragOpenInline = (parent: HTMLElement, main: HTMLElement | null) => {
dragOpenParent = parent;
@@ -391,9 +420,10 @@ const applyDragOpenInline = (parent: HTMLElement, main: HTMLElement | null) => {
const initDragOpen = () => {
// 取消上一轮手势遗留的清理 timer,避免清掉本次 inline 样式
cancelDragOpenTimers();
+ const session = dragOpenSession;
initDragOpenRetry = 0;
const tryAttach = () => {
- if (!dragOpenActive) return;
+ if (!dragOpenActive || session !== dragOpenSession) return;
const parent = document.querySelector(".full-player") as HTMLElement | null;
const main = document.getElementById("main");
if (parent) {
@@ -412,6 +442,7 @@ const initDragOpen = () => {
dragOpenLocked = null;
setDragOpenFlag(false);
statusStore.showFullPlayer = false;
+ resetDragOpenMotion();
};
tryAttach();
};
@@ -440,10 +471,12 @@ const resetDragOpen = () => {
dragOpenMain = null;
dragOpenActive = false;
dragOpenLocked = null;
+ finishDragOpenRetry = 0;
setDragOpenFlag(false);
};
const finishDragOpen = (dy: number) => {
+ if (!isPlayerMounted) return;
const shouldOpen = dy > OPEN_THRESHOLD;
// 终止还在排队的 initDragOpen 重试,避免下一帧重试命中 .full-player 后用 applyDragOpenInline
// 覆盖本函数刚写好的开/关动画样式(竞态:用户极快释放时挂载阶段重试链尚未结束)
@@ -452,26 +485,24 @@ const finishDragOpen = (dy: number) => {
cancelAnimationFrame(dragOpenRaf);
dragOpenRaf = 0;
}
- // 兜底:若挂载阶段重试未成功捕获到 .full-player,dragOpenParent 仍为 null,
- // 此时 onMobileEnter 已把元素定位到 100vh 离屏。无论开/关结果都必须现在重查并清掉 inline 样式,
- // 否则 FullPlayer 会被永久卡在屏幕外(showFullPlayer 为 true 但用户什么都看不到)。
+ // 兜底:若挂载阶段重试未成功捕获到 .full-player,先等挂载完成再执行开/关结算
if (!dragOpenParent) {
const parent = document.querySelector(".full-player") as HTMLElement | null;
if (parent) {
dragOpenParent = parent;
- // 清掉 onMobileEnter 留下的离屏 transform 及其它内联样式
- parent.style.transition = "";
- parent.style.transform = "";
- parent.style.borderRadius = "";
- parent.style.transformOrigin = "";
- parent.style.willChange = "";
- parent.style.pointerEvents = "";
- parent.style.backfaceVisibility = "";
+ } else if (finishDragOpenRetry++ < FINISH_DRAG_OPEN_MAX_RETRY) {
+ requestAnimationFrame(() => finishDragOpen(dy));
+ return;
+ } else {
+ statusStore.showFullPlayer = false;
+ resetDragOpen();
+ return;
}
if (!dragOpenMain) {
dragOpenMain = document.getElementById("main");
}
}
+ finishDragOpenRetry = 0;
if (shouldOpen) {
if (dragOpenParent) {
dragOpenParent.style.transition = "transform 0.28s cubic-bezier(0.22, 1, 0.36, 1)";
@@ -617,6 +648,7 @@ const onPointerMove = (e: PointerEvent) => {
// 卸载时清理 timer / rAF / 残留内联样式,避免组件销毁后 setTimeout 触达陈旧 DOM 引用
onBeforeUnmount(() => {
+ isPlayerMounted = false;
cancelDragOpenTimers(true);
if (dragOpenRaf) {
cancelAnimationFrame(dragOpenRaf);
diff --git a/src/views/Home/HomeMobile.vue b/src/views/Home/HomeMobile.vue
index 989d81747..ff5872baa 100644
--- a/src/views/Home/HomeMobile.vue
+++ b/src/views/Home/HomeMobile.vue
@@ -158,6 +158,8 @@ import { prefetchListCovers } from "@/composables/useCoverCache";
import SvgIcon from "@/components/Global/SvgIcon.vue";
import SImage from "@/components/UI/s-image.vue";
+const normalizeImageUrl = (url?: string) => url?.replace(/^http:/, "https:") || "";
+
const router = useRouter();
const dataStore = useDataStore();
const settingStore = useSettingStore();
@@ -260,13 +262,13 @@ const fetchFromNetwork = async (silent: boolean) => {
// 并行加载所有数据
const [playlistRes, albumRes, artistRes, videoRes] = await Promise.all([
// 推荐歌单
- personalized(isLogin() ? 21 : 20).catch(() => ({ result: [] })),
+ personalized("playlist", isLogin() ? 21 : 20).catch(() => ({ result: [] })),
// 新碟上架
newAlbumsAll().catch(() => ({ albums: [] })),
// 歌手推荐
topArtists(6).catch(() => ({ artists: [] })),
// 推荐 MV
- allMv().catch(() => ({ data: [] })),
+ allMv("全部", "全部", "上升最快").catch(() => ({ data: [] })),
]);
// 处理歌单数据
@@ -275,7 +277,7 @@ const fetchFromNetwork = async (silent: boolean) => {
.map((pl: any) => ({
id: pl.id,
name: pl.name,
- cover: pl.picUrl,
+ cover: normalizeImageUrl(pl.picUrl),
playCount: pl.playcount || 0,
}));
@@ -283,7 +285,7 @@ const fetchFromNetwork = async (silent: boolean) => {
albumRec.value = (albumRes.albums || []).map((album: any) => ({
id: album.id,
name: album.name,
- cover: album.picUrl,
+ cover: normalizeImageUrl(album.picUrl),
artist: album.artist?.name || "未知艺术家",
}));
@@ -291,7 +293,7 @@ const fetchFromNetwork = async (silent: boolean) => {
artistRec.value = (artistRes.artists || []).map((artist: any) => ({
id: artist.id,
name: artist.name,
- cover: artist.picUrl,
+ cover: normalizeImageUrl(artist.img1v1Url || artist.picUrl),
}));
// 处理 MV 数据
@@ -476,6 +478,7 @@ onMounted(() => {
font-size: 12px;
line-height: 1.4;
display: -webkit-box;
+ line-clamp: 2;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
@@ -522,6 +525,7 @@ onMounted(() => {
font-size: 12px;
line-height: 1.4;
display: -webkit-box;
+ line-clamp: 1;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
@@ -532,6 +536,7 @@ onMounted(() => {
font-size: 11px;
color: var(--n-text-color-3);
display: -webkit-box;
+ line-clamp: 1;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
@@ -565,6 +570,7 @@ onMounted(() => {
font-size: 12px;
text-align: center;
display: -webkit-box;
+ line-clamp: 1;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
@@ -640,6 +646,7 @@ onMounted(() => {
font-size: 14px;
line-height: 1.4;
display: -webkit-box;
+ line-clamp: 2;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;