feat: 重构首页Hero、导航菜单与页面布局统一

HomeHero:
- 重建每日推荐(左)+私人FM(右)双栏布局
- FM播放/暂停切换、不喜欢/下一首、背景流动动画、均衡器特效
- 修复FM数据获取(res.data.data双层结构)
- 歌单预加载改为hover懒加载避免502

导航优化:
- SearchBar顶部菜单: 首页/歌单/专辑/排行榜/MV/本地音乐
- 侧边栏隐藏MV和本地音乐(hideInSidebar)
- 修复搜索类型切换时失焦收起(@mousedown.prevent)

页面统一:
- 新建StickyTabPage通用布局组件(标题+吸顶tabs+内容slot)
- 歌单/专辑/MV/播客页面统一使用StickyTabPage重构
- CategorySelector第一项添加ml-0.5防scale裁切

播客优化:
- RadioCard简化去除订阅按钮、容忍radio为undefined
- 去除最近播放section、loadDashboard包含loadSubscribedRadios

i18n: 新碟上架→专辑(5语言)、新增fmTrash/fmNext(5语言)
This commit is contained in:
alger
2026-03-16 23:22:35 +08:00
parent 68b3700f3f
commit a3f91c45f0
17 changed files with 1184 additions and 1130 deletions
@@ -12,6 +12,7 @@
class="py-1.5 px-4 mr-3 inline-block rounded-full cursor-pointer transition-all duration-300 text-sm font-medium bg-gray-100 dark:bg-neutral-800 text-neutral-600 dark:text-neutral-400 hover:bg-gray-200 dark:hover:bg-neutral-700 hover:text-neutral-900 dark:hover:text-white"
:class="[
animationClass,
index === 0 ? 'ml-0.5' : '',
isActive(category) ? 'bg-primary text-white shadow-lg shadow-primary/25 scale-105' : ''
]"
:style="getAnimationDelay(index)"
@@ -0,0 +1,88 @@
<template>
<div class="h-full w-full bg-white transition-colors duration-500 dark:bg-black">
<n-scrollbar ref="scrollbarRef" class="h-full" :size="100" @scroll="handleScroll">
<div class="w-full pb-32">
<!-- Page Header (scrolls away) -->
<div ref="headerRef" class="page-padding pt-6 pb-2">
<h1 class="mb-2 text-2xl font-bold tracking-tight text-neutral-900 md:text-3xl dark:text-white">
{{ title }}
</h1>
<p v-if="description" class="text-neutral-500 dark:text-neutral-400">
{{ description }}
</p>
</div>
<!-- Tabs (sticky on scroll) -->
<div
class="sticky-tabs z-10 transition-shadow duration-200"
:class="isSticky ? 'sticky top-0 shadow-sm' : ''"
>
<category-selector
:model-value="modelValue"
:categories="categories"
:label-key="labelKey"
:value-key="valueKey"
@change="(val: any) => emit('change', val)"
/>
</div>
<!-- Content slot -->
<div class="page-padding pt-4">
<slot />
</div>
</div>
</n-scrollbar>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import CategorySelector from '@/components/common/CategorySelector.vue';
type Category = string | number | { [key: string]: any };
withDefaults(
defineProps<{
title: string;
description?: string;
modelValue: any;
categories: Category[];
labelKey?: string;
valueKey?: string;
}>(),
{
labelKey: 'label',
valueKey: 'value'
}
);
const emit = defineEmits<{
change: [value: any];
scroll: [e: any];
}>();
const scrollbarRef = ref();
const headerRef = ref<HTMLElement>();
const isSticky = ref(false);
const handleScroll = (e: any) => {
if (headerRef.value) {
const headerBottom = headerRef.value.offsetTop + headerRef.value.offsetHeight;
isSticky.value = e.target.scrollTop >= headerBottom;
}
emit('scroll', e);
};
const scrollTo = (options: ScrollToOptions) => {
scrollbarRef.value?.scrollTo(options);
};
defineExpose({ scrollbarRef, scrollTo });
</script>
<style scoped>
.sticky-tabs {
background: inherit;
}
</style>