feat:针对移动端优化

This commit is contained in:
alger
2025-12-19 00:23:24 +08:00
parent 70f1044dd9
commit 8e1259d2aa
18 changed files with 2299 additions and 189 deletions
+17 -6
View File
@@ -1,5 +1,9 @@
<template>
<div class="layout-page">
<!-- 移动端使用专用布局平板模式下使用 PC 布局 -->
<mobile-layout v-if="isPhone && !settingsStore.setData?.tabletMode" :is-phone="isPhone" />
<!-- PC / 浏览器移动端 / 平板模式 保持原有布局 -->
<div v-else class="layout-page" :class="{ mobile: settingsStore.isMobile }">
<div id="layout-main" class="layout-main">
<title-bar />
<div class="layout-main-page">
@@ -7,7 +11,7 @@
<app-menu v-if="!settingsStore.isMobile" class="menu" :menus="menuStore.menus" />
<div class="main">
<!-- 搜索栏 -->
<search-bar />
<search-bar class="search-bar" />
<!-- 主页面路由 -->
<div
class="main-content"
@@ -25,7 +29,8 @@
</router-view>
</div>
<play-bottom />
<app-menu v-if="shouldShowMobileMenu" class="menu" :menus="menuStore.menus" />
<!-- 移动端底部菜单浏览器模拟移动端时使用 -->
<app-menu v-if="shouldShowMobileMenu" class="menu mobile-menu" :menus="menuStore.menus" />
</div>
</div>
<!-- 底部音乐播放 -->
@@ -42,7 +47,6 @@
/>
</template>
</div>
<install-app-modal v-if="!isElectron"></install-app-modal>
<update-modal v-if="isElectron" />
<playlist-drawer v-model="showPlaylistDrawer" :song-id="currentSongId" />
<sleep-timer-top v-if="!settingsStore.isMobile" />
@@ -65,7 +69,6 @@ import { computed, defineAsyncComponent, onMounted, provide, ref } from 'vue';
import { useRoute } from 'vue-router';
import DownloadDrawer from '@/components/common/DownloadDrawer.vue';
import InstallAppModal from '@/components/common/InstallAppModal.vue';
import PlayBottom from '@/components/common/PlayBottom.vue';
import UpdateModal from '@/components/common/UpdateModal.vue';
import SleepTimerTop from '@/components/player/SleepTimerTop.vue';
@@ -76,6 +79,9 @@ import { usePlayerStore } from '@/store/modules/player';
import { useSettingsStore } from '@/store/modules/settings';
import { isElectron } from '@/utils';
// 移动端专用布局
import MobileLayout from './MobileLayout.vue';
const keepAliveInclude = computed(() => {
const allRoutes = [...homeRouter, ...otherRouter];
@@ -118,6 +124,9 @@ const shouldShowMobileMenu = computed(() => {
provide('shouldShowMobileMenu', shouldShowMobileMenu);
// 使用 settingsStore.isMobile 进行移动端检测而不是 Capacitor 设备检测
const isPhone = computed(() => settingsStore.isMobile);
onMounted(() => {
settingsStore.initializeSettings();
settingsStore.initializeTheme();
@@ -144,7 +153,7 @@ provide('openPlaylistDrawer', openPlaylistDrawer);
}
.layout-main {
@apply w-full h-full relative text-gray-900 dark:text-white;
@apply w-full h-full relative text-gray-900 dark:text-white;
}
.layout-main-page {
@@ -173,10 +182,12 @@ provide('openPlaylistDrawer', openPlaylistDrawer);
overflow: auto;
display: block;
flex: none;
position: relative;
}
.mobile-content {
height: calc(100vh - 75px);
position: relative;
}
}
</style>
+133
View File
@@ -0,0 +1,133 @@
<template>
<div id="layout-main" class="mobile-layout mobile" :class="{ 'has-safe-area': isPhone }">
<!-- 顶部头部 -->
<mobile-header />
<!-- 主内容区域 -->
<div
class="mobile-content"
:class="{ 'has-bottom-menu': shouldShowBottomMenu, 'has-player': isPlay }"
>
<router-view v-slot="{ Component }" class="mobile-page">
<keep-alive :include="keepAliveInclude">
<component :is="Component" />
</keep-alive>
</router-view>
</div>
<!-- 底部播放条 -->
<mobile-play-bar v-if="isPlay" />
<!-- 底部导航菜单 -->
<div v-if="shouldShowBottomMenu" class="mobile-bottom-menu">
<app-menu class="mobile-menu" :menus="menuStore.menus" />
</div>
<!-- 其他弹窗/抽屉 -->
<playlist-drawer v-model="showPlaylistDrawer" :song-id="currentSongId" />
<playing-list-drawer />
</div>
</template>
<script setup lang="ts">
import { computed, defineAsyncComponent, provide, ref } from 'vue';
import { useRoute } from 'vue-router';
import homeRouter from '@/router/home';
import otherRouter from '@/router/other';
import { useMenuStore } from '@/store/modules/menu';
import { usePlayerStore } from '@/store/modules/player';
import MobileHeader from './components/MobileHeader.vue';
const AppMenu = defineAsyncComponent(() => import('./components/AppMenu.vue'));
const MobilePlayBar = defineAsyncComponent(() => import('@/components/player/MobilePlayBar.vue'));
const PlayingListDrawer = defineAsyncComponent(
() => import('@/components/player/PlayingListDrawer.vue')
);
const PlaylistDrawer = defineAsyncComponent(() => import('@/components/common/PlaylistDrawer.vue'));
const props = defineProps<{
isPhone: boolean;
}>();
const route = useRoute();
const playerStore = usePlayerStore();
const menuStore = useMenuStore();
// 提供是否有安全区域
provide('hasSafeArea', props.isPhone);
// 是否有播放的歌曲
const isPlay = computed(() => playerStore.playMusic && playerStore.playMusic.id);
// 是否显示底部菜单
const shouldShowBottomMenu = computed(() => {
const menuPaths = menuStore.menus.map((item: any) => item.path);
return menuPaths.includes(route.path) && !playerStore.musicFull;
});
// 提供给 MobilePlayBar 使用,用于调整播放栏位置
provide('shouldShowMobileMenu', shouldShowBottomMenu);
// Keep-alive 配置
const keepAliveInclude = computed(() => {
const allRoutes = [...homeRouter, ...otherRouter];
return allRoutes
.filter((item) => item.meta?.keepAlive)
.map((item) =>
typeof item.name === 'string' ? item.name.charAt(0).toUpperCase() + item.name.slice(1) : ''
)
.filter(Boolean);
});
// 歌单抽屉
const showPlaylistDrawer = ref(false);
const currentSongId = ref<number | undefined>();
// 提供打开歌单抽屉的方法
const openPlaylistDrawer = (songId: number, isOpen: boolean = true) => {
currentSongId.value = songId;
showPlaylistDrawer.value = isOpen;
playerStore.setMusicFull(false);
playerStore.setPlayListDrawerVisible(!isOpen);
};
provide('openPlaylistDrawer', openPlaylistDrawer);
</script>
<style lang="scss" scoped>
.mobile-layout {
@apply w-screen h-screen flex flex-col;
@apply bg-light dark:bg-black;
@apply overflow-hidden;
position: relative;
}
.mobile-content {
@apply flex-1 overflow-auto;
// // 只有底部菜单
// &.has-bottom-menu:not(.has-player) {
// padding-bottom: calc(60px + var(--safe-area-inset-bottom, 0px));
// }
// // 只有播放栏
// &.has-player:not(.has-bottom-menu) {
// padding-bottom: calc(70px + var(--safe-area-inset-bottom, 0px));
// }
}
.mobile-page {
@apply h-full;
}
// 底部菜单固定在底部
.mobile-bottom-menu {
@apply bg-light dark:bg-black;
@apply border-t border-gray-200 dark:border-gray-800;
}
.mobile-menu {
@apply w-full;
}
</style>
+3 -1
View File
@@ -172,9 +172,11 @@ const toggleMenu = () => {
.app-menu {
max-width: 100%;
width: 100vw;
position: fixed;
position: relative;
bottom: 0;
left: 0;
z-index: 99;
@apply bg-light dark:bg-black border-t border-gray-200 dark:border-gray-700;
z-index: 99999;
@apply bg-light dark:bg-black border-none border-gray-200 dark:border-gray-700;
@@ -0,0 +1,113 @@
<template>
<div class="mobile-header" :class="{ 'safe-area-top': hasSafeArea }">
<!-- 左侧区域 -->
<div class="header-left">
<div v-if="showBack" class="header-btn" @click="goBack">
<i class="ri-arrow-left-s-line"></i>
</div>
<div v-else class="header-logo">
<span class="logo-text">Alger</span>
</div>
</div>
<!-- 中间标题 -->
<div class="header-title">
<span v-if="title">{{ t(title) }}</span>
</div>
<!-- 右侧区域 -->
<div class="header-right">
<div class="header-btn" @click="openSearch">
<i class="ri-search-line"></i>
</div>
<div class="header-btn" @click="openSettings">
<i class="ri-settings-3-line"></i>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, inject } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
// 注入是否有安全区域
const hasSafeArea = inject('hasSafeArea', false);
// 是否显示返回按钮
const showBack = computed(() => {
return route.meta.back === true;
});
// 页面标题
const title = computed(() => {
return (route.meta.title as string) || '';
});
// 返回上一页
const goBack = () => {
router.back();
};
// 打开搜索
const openSearch = () => {
router.push('/mobile-search');
};
const openSettings = () => {
router.push('/set');
};
</script>
<style lang="scss" scoped>
.mobile-header {
@apply flex items-center justify-between px-4 py-3;
@apply bg-light dark:bg-black;
@apply border-b border-gray-100 dark:border-gray-800;
min-height: 56px;
&.safe-area-top {
padding-top: calc(var(--safe-area-inset-top, 0px) + 16px);
}
}
.header-left {
@apply flex items-center;
min-width: 80px;
}
.header-logo {
@apply flex items-center;
.logo-text {
@apply text-lg font-bold text-green-500;
}
}
.header-title {
@apply flex-1 text-center;
span {
@apply text-base font-medium text-gray-900 dark:text-white;
}
}
.header-right {
@apply flex items-center gap-2;
min-width: 80px;
justify-content: flex-end;
}
.header-btn {
@apply flex items-center justify-center;
@apply w-10 h-10 rounded-full;
@apply text-xl text-gray-600 dark:text-gray-300;
@apply active:bg-gray-100 dark:active:bg-gray-800;
@apply transition-colors duration-150;
}
</style>
+2 -9
View File
@@ -1,5 +1,5 @@
<template>
<div class="search-box flex">
<div class="search-box flex search-bar">
<div v-if="showBackButton" class="back-button" @click="goBack">
<i class="ri-arrow-left-line"></i>
</div>
@@ -364,14 +364,9 @@ const checkForUpdates = async () => {
};
const toGithubRelease = () => {
if (updateInfo.value.hasUpdate) {
settingsStore.showUpdateModal = true;
} else {
window.open('https://github.com/algerkong/AlgerMusicPlayer/releases', '_blank');
}
window.location.href = 'https://donate.alger.fun/download';
};
// ==================== 搜索建议相关的状态和方法 ====================
const suggestions = ref<string[]>([]);
const showSuggestions = ref(false);
const suggestionsLoading = ref(false);
@@ -446,8 +441,6 @@ const handleKeydown = (event: KeyboardEvent) => {
break;
}
};
// ================================================================
</script>
<style lang="scss" scoped>