feat: mobile style

This commit is contained in:
shinya
2025-06-22 23:48:02 +08:00
parent dbae421973
commit e25671eed2
13 changed files with 200 additions and 28 deletions

View File

@@ -111,7 +111,7 @@ function DetailPageClient() {
return (
<PageLayout activePath='/detail'>
<div className='px-10 py-8 overflow-visible'>
<div className='px-4 sm:px-10 py-4 sm:py-8 overflow-visible'>
{/* 顶部返回按钮已移入右侧信息容器 */}
{loading ? (
<div className='flex items-center justify-center min-h-[60vh]'>

View File

@@ -179,7 +179,7 @@ function DoubanPageClient() {
return (
<PageLayout activePath={getActivePath()}>
<div className='px-10 py-8 overflow-visible'>
<div className='px-4 sm:px-10 py-4 sm:py-8 overflow-visible'>
{/* 页面标题 */}
<div className='mb-8'>
<h1 className='text-3xl font-bold text-gray-800 mb-2'>
@@ -200,7 +200,7 @@ function DoubanPageClient() {
) : (
<>
{/* 内容网格 */}
<div className='grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-x-8 gap-y-20 px-4'>
<div className='grid grid-cols-2 gap-x-2 gap-y-12 px-2 sm:grid-cols-[repeat(auto-fit,minmax(180px,1fr))] sm:gap-x-8 sm:gap-y-20 sm:px-4'>
{loading
? // 显示骨架屏
skeletonData.map((index) => (
@@ -208,7 +208,7 @@ function DoubanPageClient() {
))
: // 显示实际数据
doubanData.map((item, index) => (
<div key={`${item.title}-${index}`} className='w-44'>
<div key={`${item.title}-${index}`} className='w-full'>
<DemoCard title={item.title} poster={item.poster} />
</div>
))}

View File

@@ -73,3 +73,17 @@ body {
rgba(0, 0, 0, 0.8) 100%
);
}
/* 隐藏移动端(<768px垂直滚动条 */
@media (max-width: 767px) {
html,
body {
-ms-overflow-style: none; /* IE & Edge */
scrollbar-width: none; /* Firefox */
}
html::-webkit-scrollbar,
body::-webkit-scrollbar {
display: none; /* Chrome Safari */
}
}

View File

@@ -130,7 +130,7 @@ function HomeClient() {
return (
<PageLayout>
<div className='px-10 py-8'>
<div className='px-4 sm:px-10 py-4 sm:py-8 overflow-visible'>
{/* 顶部 Tab 切换 */}
<div className='mb-8 flex justify-center'>
<CapsuleSwitch
@@ -150,7 +150,7 @@ function HomeClient() {
<h2 className='mb-4 text-xl font-bold text-gray-800 text-left'>
</h2>
<div className='justify-start grid grid-cols-[repeat(auto-fill,_minmax(11rem,_1fr))] gap-x-8 gap-y-20 px-4'>
<div className='justify-start grid grid-cols-2 gap-x-2 gap-y-6 px-2 sm:grid-cols-[repeat(auto-fill,_minmax(11rem,_1fr))] sm:gap-x-8 sm:gap-y-20 sm:px-4'>
{favoriteItems.map((item) => (
<div key={item.id + item.source} className='w-full'>
<VideoCard {...item} from='favorites' />
@@ -173,7 +173,10 @@ function HomeClient() {
</h2>
<ScrollableRow scrollDistance={800}>
{collections.map((collection) => (
<div key={collection.title} className='min-w-[280px] w-72'>
<div
key={collection.title}
className='min-w-[180px] w-44 sm:min-w-[280px] sm:w-72'
>
<CollectionCard
title={collection.title}
icon={collection.icon}
@@ -196,7 +199,10 @@ function HomeClient() {
{loading
? // 加载状态显示灰色占位数据
Array.from({ length: 8 }).map((_, index) => (
<div key={index} className='min-w-[180px] w-44'>
<div
key={index}
className='min-w-[140px] w-36 sm:min-w-[180px] sm:w-44'
>
<div className='relative aspect-[2/3] w-full overflow-hidden rounded-lg bg-gray-200 animate-pulse'>
<div className='absolute inset-0 bg-gray-300'></div>
</div>
@@ -205,7 +211,10 @@ function HomeClient() {
))
: // 显示真实数据
hotMovies.map((movie, index) => (
<div key={index} className='min-w-[180px] w-44'>
<div
key={index}
className='min-w-[140px] w-36 sm:min-w-[180px] sm:w-44'
>
<DemoCard title={movie.title} poster={movie.poster} />
</div>
))}
@@ -221,7 +230,10 @@ function HomeClient() {
{loading
? // 加载状态显示灰色占位数据
Array.from({ length: 8 }).map((_, index) => (
<div key={index} className='min-w-[180px] w-44'>
<div
key={index}
className='min-w-[140px] w-36 sm:min-w-[180px] sm:w-44'
>
<div className='relative aspect-[2/3] w-full overflow-hidden rounded-lg bg-gray-200 animate-pulse'>
<div className='absolute inset-0 bg-gray-300'></div>
</div>
@@ -230,7 +242,10 @@ function HomeClient() {
))
: // 显示真实数据
hotTvShows.map((show, index) => (
<div key={index} className='min-w-[180px] w-44'>
<div
key={index}
className='min-w-[140px] w-36 sm:min-w-[180px] sm:w-44'
>
<DemoCard title={show.title} poster={show.poster} />
</div>
))}

View File

@@ -99,7 +99,7 @@ function SearchPageClient() {
return (
<PageLayout activePath='/search'>
<div className='px-10 py-8 overflow-visible mb-10'>
<div className='px-4 sm:px-10 py-4 sm:py-8 overflow-visible mb-10'>
{/* 搜索框 */}
<div className='mb-8'>
<form onSubmit={handleSearch} className='max-w-2xl mx-auto'>
@@ -125,7 +125,7 @@ function SearchPageClient() {
</div>
) : showResults ? (
// 搜索结果
<div className='justify-start grid grid-cols-[repeat(auto-fill,_minmax(11rem,_1fr))] gap-x-8 gap-y-20 px-4'>
<div className='justify-start grid grid-cols-2 gap-x-2 gap-y-20 px-2 sm:grid-cols-[repeat(auto-fill,_minmax(11rem,_1fr))] sm:gap-x-8 sm:gap-y-20 sm:px-4'>
{searchResults.map((item) => (
<div key={item.id} className='w-full'>
<VideoCard {...item} from='search' />

View File

@@ -23,7 +23,7 @@ const CapsuleSwitch: React.FC<CapsuleSwitchProps> = ({
<button
key={opt.value}
onClick={() => onChange(opt.value)}
className={`w-20 px-3 py-2 rounded-full text-sm font-medium transition-all duration-200 ${
className={`w-16 px-3 py-1 text-xs sm:w-20 sm:py-2 sm:text-sm rounded-full font-medium transition-all duration-200 ${
active === opt.value
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-700 hover:text-gray-900'

View File

@@ -19,7 +19,7 @@ export default function CollectionCard({
<div className='relative aspect-[5/3] w-full overflow-hidden rounded-xl bg-gray-200 border border-gray-300/50'>
{/* 图标容器 */}
<div className='absolute inset-0 flex items-center justify-center'>
<Icon className='h-14 w-14 text-gray-600' />
<Icon className='h-10 w-10 sm:h-12 sm:w-12 text-gray-600' />
</div>
{/* Hover 蒙版效果 - 参考 DemoCard */}

View File

@@ -78,7 +78,10 @@ export default function ContinueWatching({ className }: ContinueWatchingProps) {
{loading
? // 加载状态显示灰色占位数据
Array.from({ length: 6 }).map((_, index) => (
<div key={index} className='min-w-[180px] w-44'>
<div
key={index}
className='min-w-[140px] w-36 sm:min-w-[180px] sm:w-44'
>
<div className='relative aspect-[2/3] w-full overflow-hidden rounded-lg bg-gray-200 animate-pulse'>
<div className='absolute inset-0 bg-gray-300'></div>
</div>
@@ -90,7 +93,10 @@ export default function ContinueWatching({ className }: ContinueWatchingProps) {
playRecords.map((record) => {
const { source, id } = parseKey(record.key);
return (
<div key={record.key} className='min-w-[180px] w-44'>
<div
key={record.key}
className='min-w-[140px] w-36 sm:min-w-[180px] sm:w-44'
>
<VideoCard
id={id}
title={record.title}

View File

@@ -1,6 +1,6 @@
const DoubanCardSkeleton = () => {
return (
<div className='w-44'>
<div className='w-full'>
<div className='group relative w-full rounded-lg bg-transparent shadow-none flex flex-col'>
{/* 海报骨架 - 2:3 比例 */}
<div className='relative aspect-[2/3] w-full overflow-hidden rounded-lg bg-gray-200 animate-pulse'>

View File

@@ -0,0 +1,99 @@
'use client';
import {
Film,
Home,
MessageCircleHeart,
MountainSnow,
Search,
Star,
Swords,
Tv,
VenetianMask,
} from 'lucide-react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
const MobileBottomNav = () => {
const pathname = usePathname();
const navItems = [
{ icon: Home, label: '首页', href: '/' },
{ icon: Search, label: '搜索', href: '/search' },
{
icon: Film,
label: '热门电影',
href: '/douban?type=movie&tag=热门&title=热门电影',
},
{
icon: Tv,
label: '热门剧集',
href: '/douban?type=tv&tag=热门&title=热门剧集',
},
{
icon: Star,
label: '豆瓣 Top250',
href: '/douban?type=movie&tag=top250&title=豆瓣 Top250',
},
{ icon: Swords, label: '美剧', href: '/douban?type=tv&tag=美剧' },
{
icon: MessageCircleHeart,
label: '韩剧',
href: '/douban?type=tv&tag=韩剧',
},
{ icon: MountainSnow, label: '日剧', href: '/douban?type=tv&tag=日剧' },
{ icon: VenetianMask, label: '日漫', href: '/douban?type=tv&tag=日本动画' },
];
const isActive = (href: string) => {
const typeMatch = href.match(/type=([^&]+)/)?.[1];
const tagMatch = href.match(/tag=([^&]+)/)?.[1];
// 解码URL以进行正确的比较
const decodedActive = decodeURIComponent(pathname);
const decodedItemHref = decodeURIComponent(href);
return (
decodedActive === decodedItemHref ||
(decodedActive.startsWith('/douban') &&
decodedActive.includes(`type=${typeMatch}`) &&
decodedActive.includes(`tag=${tagMatch}`))
);
};
return (
<nav
className='md:hidden fixed left-0 right-0 z-20 bg-white/90 backdrop-blur-xl border-t border-gray-200/50 overflow-x-auto overscroll-x-contain whitespace-nowrap scrollbar-hide'
style={{
/* 紧贴视口底部,同时在内部留出安全区高度 */
bottom: 0,
paddingBottom: 'env(safe-area-inset-bottom)',
}}
>
<ul className='flex items-center'>
{navItems.map((item) => {
const active = isActive(item.href);
return (
<li key={item.href} className='flex-shrink-0 w-1/4'>
<Link
href={item.href}
className='flex flex-col items-center justify-center w-full h-14 gap-1 text-xs'
>
<item.icon
className={`h-6 w-6 ${
active ? 'text-green-600' : 'text-gray-500'
}`}
/>
<span className={active ? 'text-green-600' : 'text-gray-600'}>
{item.label}
</span>
</Link>
</li>
);
})}
</ul>
</nav>
);
};
export default MobileBottomNav;

View File

@@ -0,0 +1,18 @@
import Link from 'next/link';
const MobileHeader = () => {
return (
<header className='md:hidden w-full bg-white/70 backdrop-blur-xl border-b border-gray-200/50 shadow-sm'>
<div className='h-12 flex items-center justify-center'>
<Link
href='/'
className='text-2xl font-bold text-green-600 tracking-tight hover:opacity-80 transition-opacity'
>
MoonTV
</Link>
</div>
</header>
);
};
export default MobileHeader;

View File

@@ -1,3 +1,5 @@
import MobileBottomNav from './MobileBottomNav';
import MobileHeader from './MobileHeader';
import { useSidebar } from './Sidebar';
import Sidebar from './Sidebar';
@@ -10,16 +12,33 @@ const PageLayout = ({ children, activePath = '/' }: PageLayoutProps) => {
const { isCollapsed } = useSidebar();
return (
<div className='grid grid-cols-[auto_1fr] w-full'>
<Sidebar activePath={activePath} />
<div
className={`min-w-0 transition-all duration-300 ${
isCollapsed ? 'col-start-2' : 'col-start-2'
}`}
>
{children}
<>
{/* 桌面端布局 */}
<div className='hidden md:grid md:grid-cols-[auto_1fr] w-full'>
<Sidebar activePath={activePath} />
<div
className={`min-w-0 transition-all duration-300 ${
isCollapsed ? 'col-start-2' : 'col-start-2'
}`}
>
{children}
</div>
</div>
</div>
{/* 移动端布局 */}
<div className='md:hidden flex flex-col min-h-screen w-full'>
<MobileHeader />
<main
className='flex-1 pb-14'
style={{
paddingBottom: 'calc(3.5rem + env(safe-area-inset-bottom))',
}}
>
{children}
</main>
<MobileBottomNav />
</div>
</>
);
};

View File

@@ -154,7 +154,8 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
return (
<SidebarContext.Provider value={contextValue}>
<div className='flex'>
{/* 在移动端隐藏侧边栏 */}
<div className='hidden md:flex'>
<aside
data-sidebar
className={`fixed top-0 left-0 h-screen bg-white/40 backdrop-blur-xl transition-all duration-300 border-r border-gray-200/50 z-10 shadow-lg ${