mirror of
https://github.com/MoonTechLab/LunaTV.git
synced 2026-02-21 17:24:41 +08:00
feat: adjust mobile style, add force landscape button
This commit is contained in:
@@ -111,7 +111,7 @@ function DetailPageClient() {
|
||||
|
||||
return (
|
||||
<PageLayout activePath='/detail'>
|
||||
<div className='px-4 sm:px-10 py-4 sm:py-8 overflow-visible'>
|
||||
<div className='px-2 sm:px-10 py-4 sm:py-8 overflow-visible'>
|
||||
{/* 顶部返回按钮已移入右侧信息容器 */}
|
||||
{loading ? (
|
||||
<div className='flex items-center justify-center min-h-[60vh]'>
|
||||
@@ -144,7 +144,7 @@ function DetailPageClient() {
|
||||
window.location.href = '/';
|
||||
}
|
||||
}}
|
||||
className='absolute top-0 left-0 -translate-x-[60%] -translate-y-[30%] sm:-translate-x-[180%] sm:-translate-y-1/2 p-2 rounded transition-colors'
|
||||
className='absolute top-0 left-0 -translate-x-[40%] -translate-y-[30%] sm:-translate-x-[180%] sm:-translate-y-1/2 p-2 rounded transition-colors'
|
||||
>
|
||||
<svg
|
||||
className='h-5 w-5 text-gray-500 hover:text-green-600 transition-colors'
|
||||
@@ -309,7 +309,7 @@ function DetailPageClient() {
|
||||
共 {detail.episodes.length} 集
|
||||
</div>
|
||||
</div>
|
||||
<div className='grid grid-cols-3 gap-2 sm:flex sm:flex-wrap sm:gap-4'>
|
||||
<div className='grid grid-cols-3 gap-2 sm:grid-cols-[repeat(auto-fit,_minmax(6rem,_1fr))] sm:gap-4'>
|
||||
{detail.episodes.map((episode, idx) => (
|
||||
<a
|
||||
key={idx}
|
||||
|
||||
@@ -200,7 +200,7 @@ function DoubanPageClient() {
|
||||
) : (
|
||||
<>
|
||||
{/* 内容网格 */}
|
||||
<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'>
|
||||
<div className='grid grid-cols-3 gap-x-2 gap-y-12 px-0 sm: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) => (
|
||||
|
||||
@@ -130,7 +130,7 @@ function HomeClient() {
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className='px-4 sm:px-10 py-4 sm:py-8 overflow-visible'>
|
||||
<div className='px-2 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-2 gap-x-2 gap-y-20 px-2 sm:grid-cols-[repeat(auto-fill,_minmax(11rem,_1fr))] sm:gap-x-8 sm:px-4'>
|
||||
<div className='justify-start grid grid-cols-3 gap-x-2 gap-y-14 sm:gap-y-20 px-2 sm:grid-cols-[repeat(auto-fill,_minmax(11rem,_1fr))] sm:gap-x-8 sm:px-4'>
|
||||
{favoriteItems.map((item) => (
|
||||
<div key={item.id + item.source} className='w-full'>
|
||||
<VideoCard {...item} from='favorites' />
|
||||
@@ -175,7 +175,7 @@ function HomeClient() {
|
||||
{collections.map((collection) => (
|
||||
<div
|
||||
key={collection.title}
|
||||
className='min-w-[180px] w-44 sm:min-w-[280px] sm:w-72'
|
||||
className='min-w-[150px] w-44 sm:min-w-[280px] sm:w-72'
|
||||
>
|
||||
<CollectionCard
|
||||
title={collection.title}
|
||||
@@ -201,7 +201,7 @@ function HomeClient() {
|
||||
Array.from({ length: 8 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className='min-w-[140px] w-36 sm:min-w-[180px] sm:w-44'
|
||||
className='min-w-[96px] w-24 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>
|
||||
@@ -213,7 +213,7 @@ function HomeClient() {
|
||||
hotMovies.map((movie, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className='min-w-[140px] w-36 sm:min-w-[180px] sm:w-44'
|
||||
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
|
||||
>
|
||||
<DemoCard title={movie.title} poster={movie.poster} />
|
||||
</div>
|
||||
@@ -232,7 +232,7 @@ function HomeClient() {
|
||||
Array.from({ length: 8 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className='min-w-[140px] w-36 sm:min-w-[180px] sm:w-44'
|
||||
className='min-w-[96px] w-24 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>
|
||||
@@ -244,7 +244,7 @@ function HomeClient() {
|
||||
hotTvShows.map((show, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className='min-w-[140px] w-36 sm:min-w-[180px] sm:w-44'
|
||||
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
|
||||
>
|
||||
<DemoCard title={show.title} poster={show.poster} />
|
||||
</div>
|
||||
|
||||
@@ -90,6 +90,12 @@ function PlayPageClient() {
|
||||
// 是否显示旋转提示(5s 后自动隐藏)
|
||||
const [showOrientationTip, setShowOrientationTip] = useState(false);
|
||||
const orientationTipTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
// 当前是否处于竖屏,用于控制"强制横屏"按钮显隐
|
||||
const [isPortrait, setIsPortrait] = useState(
|
||||
typeof window !== 'undefined'
|
||||
? window.matchMedia('(orientation: portrait)').matches
|
||||
: true
|
||||
);
|
||||
|
||||
// 长按三倍速相关状态
|
||||
const [isLongPressing, setIsLongPressing] = useState(false);
|
||||
@@ -1029,6 +1035,9 @@ function PlayPageClient() {
|
||||
const update = () => {
|
||||
const portrait = mql.matches;
|
||||
|
||||
// 更新竖屏状态
|
||||
setIsPortrait(portrait);
|
||||
|
||||
// 在进入竖屏时显示提示,5 秒后自动隐藏
|
||||
if (portrait) {
|
||||
setShowOrientationTip(true);
|
||||
@@ -1068,7 +1077,25 @@ function PlayPageClient() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 进入/退出全屏时锁定/解锁横屏
|
||||
// 用户点击悬浮按钮 -> 请求全屏并锁定横屏
|
||||
const handleForceLandscape = async () => {
|
||||
try {
|
||||
const el: any = artRef.current || document.documentElement;
|
||||
if (el.requestFullscreen) {
|
||||
await el.requestFullscreen();
|
||||
} else if (el.webkitRequestFullscreen) {
|
||||
el.webkitRequestFullscreen();
|
||||
}
|
||||
|
||||
if (screen.orientation && (screen.orientation as any).lock) {
|
||||
await (screen.orientation as any).lock('landscape');
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('强制横屏失败:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// 进入/退出全屏时锁定/解锁横屏(保持原有逻辑)
|
||||
useEffect(() => {
|
||||
if (typeof document === 'undefined') return;
|
||||
|
||||
@@ -1293,6 +1320,28 @@ function PlayPageClient() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 强制横屏按钮:仅在移动端竖屏时显示 */}
|
||||
{isPortrait && (
|
||||
<button
|
||||
onClick={handleForceLandscape}
|
||||
className='fixed bottom-16 left-4 z-[195] w-10 h-10 rounded-full bg-gray-800 text-white flex items-center justify-center md:hidden'
|
||||
>
|
||||
<svg
|
||||
className='w-6 h-6'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
viewBox='0 0 24 24'
|
||||
>
|
||||
<path
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
strokeWidth={2}
|
||||
d='M3 18v-6a3 3 0 013-3h12M21 6v6a3 3 0 01-3 3H6'
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 换源加载遮罩 */}
|
||||
{sourceChanging && (
|
||||
<div className='fixed inset-0 bg-black/50 z-[200] flex items-center justify-center'>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client';
|
||||
|
||||
import { Search } from 'lucide-react';
|
||||
@@ -37,8 +38,10 @@ function SearchPageClient() {
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// 自动聚焦搜索框
|
||||
searchInputRef.current?.focus();
|
||||
// 自动聚焦搜索框:仅当 URL 中没有搜索参数时
|
||||
if (!searchParams.get('q')) {
|
||||
searchInputRef.current?.focus();
|
||||
}
|
||||
|
||||
// 加载搜索历史
|
||||
(async () => {
|
||||
@@ -125,7 +128,7 @@ function SearchPageClient() {
|
||||
</div>
|
||||
) : showResults ? (
|
||||
// 搜索结果
|
||||
<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:px-4'>
|
||||
<div className='justify-start grid grid-cols-3 gap-x-2 gap-y-14 sm:gap-y-20 px-0 sm:px-2 sm:grid-cols-[repeat(auto-fill,_minmax(11rem,_1fr))] sm:gap-x-8 sm:px-4'>
|
||||
{searchResults.map((item) => (
|
||||
<div key={item.id} className='w-full'>
|
||||
<VideoCard {...item} from='search' />
|
||||
|
||||
@@ -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-10 w-10 sm:h-12 sm:w-12 text-gray-600' />
|
||||
<Icon className='h-8 w-8 sm:h-12 sm:w-12 text-gray-600' />
|
||||
</div>
|
||||
|
||||
{/* Hover 蒙版效果 - 参考 DemoCard */}
|
||||
@@ -29,7 +29,7 @@ export default function CollectionCard({
|
||||
{/* 标题 - absolute 定位,类似 DemoCard */}
|
||||
<div className='absolute top-[calc(100%+0.5rem)] left-0 right-0'>
|
||||
<div className='flex flex-col items-center justify-center'>
|
||||
<h3 className='text-sm font-medium text-gray-800 truncate w-full text-center'>
|
||||
<h3 className='text-xs sm:text-sm font-medium text-gray-800 truncate w-full text-center'>
|
||||
{title}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -80,7 +80,7 @@ export default function ContinueWatching({ className }: ContinueWatchingProps) {
|
||||
Array.from({ length: 6 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className='min-w-[140px] w-36 sm:min-w-[180px] sm:w-44'
|
||||
className='min-w-[96px] w-24 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>
|
||||
@@ -95,7 +95,7 @@ export default function ContinueWatching({ className }: ContinueWatchingProps) {
|
||||
return (
|
||||
<div
|
||||
key={record.key}
|
||||
className='min-w-[140px] w-36 sm:min-w-[180px] sm:w-44'
|
||||
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
|
||||
>
|
||||
<VideoCard
|
||||
id={id}
|
||||
|
||||
@@ -92,7 +92,7 @@ const DemoCard = ({ title, poster }: DemoCardProps) => {
|
||||
{/* 信息层 */}
|
||||
<div className='absolute top-[calc(100%+0.2rem)] left-0 right-0'>
|
||||
<div className='flex flex-col items-center justify-center'>
|
||||
<span className='text-gray-900 font-semibold truncate w-full text-center'>
|
||||
<span className='text-gray-900 font-semibold truncate w-full text-center text-xs sm:text-sm'>
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ const DoubanCardSkeleton = () => {
|
||||
{/* 信息层骨架 */}
|
||||
<div className='absolute top-[calc(100%+0.5rem)] left-0 right-0'>
|
||||
<div className='flex flex-col items-center justify-center'>
|
||||
<div className='h-4 w-32 bg-gray-200 rounded animate-pulse mb-2'></div>
|
||||
<div className='h-4 w-24 sm:w-32 bg-gray-200 rounded animate-pulse mb-2'></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -102,7 +102,7 @@ export default function ScrollableRow({
|
||||
>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className='flex space-x-6 overflow-x-auto scrollbar-hide pb-14'
|
||||
className='flex space-x-6 overflow-x-auto scrollbar-hide pb-10 sm:pb-14'
|
||||
onScroll={checkScroll}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -177,7 +177,7 @@ export default function VideoCard({
|
||||
<PlayCircleSolid fillColor={playHover ? '#22c55e' : 'none'} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='absolute bottom-4 right-4 flex items-center gap-6'>
|
||||
<div className='absolute bottom-2 right-2 sm:bottom-4 sm:right-4 flex items-center gap-6'>
|
||||
{!hideCheckCircle && (
|
||||
<span
|
||||
onClick={handleDeleteRecord}
|
||||
@@ -188,7 +188,7 @@ export default function VideoCard({
|
||||
</span>
|
||||
)}
|
||||
{favorited && (
|
||||
<span className='inline-flex w-6 h-6 pointer-events-none' />
|
||||
<span className='inline-flex w-4 h-4 sm:w-6 sm:h-6 pointer-events-none' />
|
||||
)}
|
||||
{!favorited && (
|
||||
<span
|
||||
@@ -197,7 +197,7 @@ export default function VideoCard({
|
||||
className='inline-flex items-center justify-center pointer-events-auto'
|
||||
>
|
||||
<Heart
|
||||
className={`h-6 w-6 stroke-[2] ${
|
||||
className={`h-4 w-4 sm:h-6 sm:w-6 stroke-[2] ${
|
||||
favorited ? 'text-red-500' : 'text-white/90'
|
||||
}`}
|
||||
fill={favorited ? 'currentColor' : 'none'}
|
||||
@@ -209,8 +209,10 @@ export default function VideoCard({
|
||||
|
||||
{/* 集数指示器 - 绿色小圆球 */}
|
||||
{episodes && episodes > 1 && (
|
||||
<div className='absolute top-2 right-2 w-6 h-6 bg-green-500 rounded-full flex items-center justify-center'>
|
||||
<span className='text-white text-xs font-bold'>{episodes}</span>
|
||||
<div className='absolute top-2 right-2 w-4 h-4 sm:w-6 sm:h-6 bg-green-500 rounded-full flex items-center justify-center'>
|
||||
<span className='text-white text-[0.5rem] sm:text-xs font-bold'>
|
||||
{episodes}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -226,8 +228,8 @@ export default function VideoCard({
|
||||
|
||||
{/* 当前播放集数 */}
|
||||
{currentEpisode && episodes && episodes > 1 && (
|
||||
<div className='absolute top-2 left-2 w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center'>
|
||||
<span className='text-white text-xs font-bold'>
|
||||
<div className='absolute top-2 left-2 w-4 h-4 sm:w-6 sm:h-6 bg-blue-500 rounded-full flex items-center justify-center'>
|
||||
<span className='text-white text-[0.5rem] sm:text-xs font-bold'>
|
||||
{currentEpisode}
|
||||
</span>
|
||||
</div>
|
||||
@@ -237,11 +239,11 @@ export default function VideoCard({
|
||||
{/* 信息层 */}
|
||||
<div className='absolute top-[calc(100%+0.2rem)] left-0 right-0'>
|
||||
<div className='flex flex-col items-center justify-center'>
|
||||
<span className='text-gray-900 font-semibold truncate w-full text-center'>
|
||||
<span className='text-gray-900 font-semibold truncate w-full text-center text-xs sm:text-sm'>
|
||||
{title}
|
||||
</span>
|
||||
{source && (
|
||||
<span className='text-gray-500 text-xs w-full text-center mt-1'>
|
||||
<span className='text-gray-500 text-[0.5rem] sm:text-xs w-full text-center mt-1'>
|
||||
<span className='inline-block border border-gray-500/60 rounded px-2 py-[1px]'>
|
||||
{source_name}
|
||||
</span>
|
||||
@@ -252,14 +254,14 @@ export default function VideoCard({
|
||||
|
||||
{/* 收藏夹始终显示红心 */}
|
||||
{favorited && (
|
||||
<div className='absolute bottom-4 right-4 flex items-center'>
|
||||
<div className='absolute bottom-2 right-2 sm:bottom-4 sm:right-4 flex items-center'>
|
||||
<span
|
||||
onClick={handleToggleFavorite}
|
||||
title={favorited ? '移除收藏' : '加入收藏'}
|
||||
className='inline-flex items-center justify-center'
|
||||
>
|
||||
<Heart
|
||||
className={`h-6 w-6 stroke-[2] ${
|
||||
className={`h-4 w-4 sm:h-6 sm:w-6 stroke-[2] ${
|
||||
favorited ? 'text-red-500' : 'text-white/90'
|
||||
}`}
|
||||
fill={favorited ? 'currentColor' : 'none'}
|
||||
|
||||
Reference in New Issue
Block a user