适配手机端底部状态

This commit is contained in:
北枳 2025-09-25 11:29:53 +08:00
parent 760937aa51
commit 3a2eaa1ecc
6 changed files with 189 additions and 27 deletions

View File

@ -95,8 +95,10 @@
--accent-foreground: 0 0% 98%; --accent-foreground: 0 0% 98%;
/* 自定义渐变色变量 */ /* 自定义渐变色变量 */
--custom-blue: 186 100% 70%; /* rgb(106, 244, 249) */ --custom-blue: 186 100% 70%;
--custom-purple: 280 100% 62%; /* rgb(199, 59, 255) */ /* rgb(106, 244, 249) */
--custom-purple: 280 100% 62%;
/* rgb(199, 59, 255) */
--custom-blue-rgb: 106, 244, 249; --custom-blue-rgb: 106, 244, 249;
--custom-purple-rgb: 199, 59, 255; --custom-purple-rgb: 199, 59, 255;
--destructive: 0 62.8% 30.6%; --destructive: 0 62.8% 30.6%;
@ -162,6 +164,7 @@ body {
.hide-scrollbar::-webkit-scrollbar { .hide-scrollbar::-webkit-scrollbar {
display: none !important; display: none !important;
} }
*::-webkit-scrollbar { *::-webkit-scrollbar {
display: none !important; display: none !important;
} }
@ -184,6 +187,7 @@ body {
border-radius: 8px; border-radius: 8px;
padding: 8px; padding: 8px;
} }
.button-NxtqWZ:hover { .button-NxtqWZ:hover {
background-color: #2f3237 !important; background-color: #2f3237 !important;
} }
@ -226,6 +230,7 @@ body {
height: 0; height: 0;
pointer-events: none; pointer-events: none;
} }
.ant-spin-nested-loading .ant-spin { .ant-spin-nested-loading .ant-spin {
max-height: none !important; max-height: none !important;
} }
@ -236,6 +241,7 @@ body {
opacity: 0; opacity: 0;
transform: translateY(-8px); transform: translateY(-8px);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
@ -262,3 +268,41 @@ body {
padding: unset !important; padding: unset !important;
margin: 0.25rem !important; margin: 0.25rem !important;
} }
/* 安全区域变量定义 */
:root {
--sat: env(safe-area-inset-top, 0px);
--sar: env(safe-area-inset-right, 0px);
--sab: env(safe-area-inset-bottom, 0px);
--sal: env(safe-area-inset-left, 0px);
}
/* 移动端适配:使用 dvh 动态视口高度 */
@supports (height: 100dvh) {
body {
height: 100dvh;
}
}
/* 移动端安全区域处理 */
@media (max-width: 768px) {
body {
/* 使用动态视口高度,考虑移动端浏览器地址栏 */
height: 100dvh;
height: calc(var(--vh, 1vh) * 100);
padding-bottom: env(safe-area-inset-bottom);
}
}
/* 针对移动端底部导航栏/状态栏的特殊处理 */
@media (max-width: 768px) and (display-mode: browser) {
.mobile-safe-bottom {
padding-bottom: max(1rem, env(safe-area-inset-bottom));
margin-bottom: max(1rem, env(safe-area-inset-bottom));
}
.mobile-viewport-height {
height: 100dvh;
height: calc(var(--vh, 1vh) * 100);
}
}

View File

@ -43,7 +43,7 @@ export default function RootLayout({
<head> <head>
<title>MovieFlow - AI Movie Studio</title> <title>MovieFlow - AI Movie Studio</title>
<meta name="description" content="Share your story, or just a few words, and our AI turns it into a great film. We remove the barriers to creation. At MovieFlow, everyone is a movie master." /> <meta name="description" content="Share your story, or just a few words, and our AI turns it into a great film. We remove the barriers to creation. At MovieFlow, everyone is a movie master." />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<link rel="icon" type="image/x-icon" sizes="16x16" href="/favicon.ico?v=1" /> <link rel="icon" type="image/x-icon" sizes="16x16" href="/favicon.ico?v=1" />
<link rel="icon" type="image/x-icon" sizes="32x32" href="/favicon.ico?v=1" /> <link rel="icon" type="image/x-icon" sizes="32x32" href="/favicon.ico?v=1" />
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico?v=1" /> <link rel="shortcut icon" type="image/x-icon" href="/favicon.ico?v=1" />

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { Sidebar } from './sidebar'; import { Sidebar } from './sidebar';
import { TopBar } from './top-bar'; import { TopBar } from './top-bar';
import { useDeviceType } from '@/hooks/useDeviceType'; import { useDeviceType } from '@/hooks/useDeviceType';
@ -10,9 +10,28 @@ interface DashboardLayoutProps {
} }
export function DashboardLayout({ children }: DashboardLayoutProps) { export function DashboardLayout({ children }: DashboardLayoutProps) {
const [sidebarCollapsed, setSidebarCollapsed] = useState(true); // 默认收起状态 const [sidebarCollapsed, setSidebarCollapsed] = useState(true);
const { deviceType, isMobile, isTablet, isDesktop } = useDeviceType(); const { deviceType, isMobile, isTablet, isDesktop } = useDeviceType();
// 处理移动端视口高度动态计算
useEffect(() => {
if (isMobile || isTablet) {
const setVH = () => {
const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
};
setVH();
window.addEventListener('resize', setVH);
window.addEventListener('orientationchange', setVH);
return () => {
window.removeEventListener('resize', setVH);
window.removeEventListener('orientationchange', setVH);
};
}
}, [isMobile, isTablet]);
// 根据设备类型设置布局样式 // 根据设备类型设置布局样式
const getLayoutStyles = () => { const getLayoutStyles = () => {
if (isMobile || isTablet) { if (isMobile || isTablet) {
@ -29,13 +48,28 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
}; };
}; };
// 获取移动端容器类名
const getMobileContainerClasses = () => {
if (isMobile || isTablet) {
return "mobile-viewport-height mobile-safe-bottom";
}
return "";
};
return ( return (
<div className="min-h-screen bg-background"> <div className="min-h-screen bg-background">
<TopBar collapsed={sidebarCollapsed} isDesktop={isDesktop} /> <TopBar collapsed={sidebarCollapsed} isDesktop={isDesktop} />
{isDesktop && <Sidebar collapsed={sidebarCollapsed} onToggle={setSidebarCollapsed} />} {isDesktop && <Sidebar collapsed={sidebarCollapsed} onToggle={setSidebarCollapsed} />}
<div <div
className="h-[calc(100vh-4rem)] top-[4rem] fixed right-0 bottom-0 px-4" className={`top-[4rem] fixed right-0 bottom-0 px-4 ${getMobileContainerClasses()}`}
style={getLayoutStyles()}> style={{
...getLayoutStyles(),
// 移动端使用动态高度计算
height: (isMobile || isTablet)
? 'calc(100dvh - 4rem)'
: 'calc(100vh - 4rem)'
}}
>
{children} {children}
</div> </div>
</div> </div>

View File

@ -2,16 +2,16 @@ import { useState, useEffect } from 'react';
// 定义设备类型枚举 // 定义设备类型枚举
export enum DeviceType { export enum DeviceType {
MOBILE = 'mobile', // 手机 MOBILE = 'mobile',
TABLET = 'tablet', // 平板 TABLET = 'tablet',
DESKTOP = 'desktop' // 桌面端 DESKTOP = 'desktop'
} }
// 定义屏幕断点 // 定义屏幕断点
const BREAKPOINTS = { const BREAKPOINTS = {
MOBILE: 480, // 0-480px 为手机 MOBILE: 480,
TABLET: 1024, // 481-1024px 为平板 TABLET: 1024,
DESKTOP: 1025 // 1025px 及以上为桌面端 DESKTOP: 1025
}; };
export function useDeviceType() { export function useDeviceType() {
@ -22,35 +22,39 @@ export function useDeviceType() {
}); });
useEffect(() => { useEffect(() => {
/**
*
*/
const getDeviceType = (width: number): DeviceType => { const getDeviceType = (width: number): DeviceType => {
if (width <= BREAKPOINTS.MOBILE) return DeviceType.MOBILE; if (width <= BREAKPOINTS.MOBILE) return DeviceType.MOBILE;
if (width <= BREAKPOINTS.TABLET) return DeviceType.TABLET; if (width <= BREAKPOINTS.TABLET) return DeviceType.TABLET;
return DeviceType.DESKTOP; return DeviceType.DESKTOP;
}; };
/**
*
*/
const handleResize = () => { const handleResize = () => {
const width = window.innerWidth; const width = window.innerWidth;
const height = window.innerHeight; const height = window.innerHeight;
setWindowSize({ width, height }); setWindowSize({ width, height });
setDeviceType(getDeviceType(width)); setDeviceType(getDeviceType(width));
// 移动端动态视口高度处理
if (width <= BREAKPOINTS.TABLET) {
const vh = height * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
}
}; };
// 初始化设备类型 // 初始化
handleResize(); handleResize();
// 添加窗口大小变化监听 // 添加事件监听
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
window.addEventListener('orientationchange', () => {
// 延迟处理以确保获取正确的视口尺寸
setTimeout(handleResize, 100);
});
// 清理监听器
return () => { return () => {
window.removeEventListener('resize', handleResize); window.removeEventListener('resize', handleResize);
window.removeEventListener('orientationchange', handleResize);
}; };
}, []); }, []);
@ -59,6 +63,8 @@ export function useDeviceType() {
windowSize, windowSize,
isMobile: deviceType === DeviceType.MOBILE, isMobile: deviceType === DeviceType.MOBILE,
isTablet: deviceType === DeviceType.TABLET, isTablet: deviceType === DeviceType.TABLET,
isDesktop: deviceType === DeviceType.DESKTOP isDesktop: deviceType === DeviceType.DESKTOP,
/** 是否为移动端设备(包括平板) */
isMobileDevice: deviceType === DeviceType.MOBILE || deviceType === DeviceType.TABLET
}; };
} }

64
hooks/useSafeArea.ts Normal file
View File

@ -0,0 +1,64 @@
import { useState, useEffect } from 'react';
interface SafeAreaInsets {
top: number;
right: number;
bottom: number;
left: number;
}
export function useSafeArea() {
const [safeAreaInsets, setSafeAreaInsets] = useState<SafeAreaInsets>({
top: 0,
right: 0,
bottom: 0,
left: 0
});
const [viewportHeight, setViewportHeight] = useState(0);
useEffect(() => {
const updateSafeArea = () => {
// 获取 CSS 环境变量
const computedStyle = getComputedStyle(document.documentElement);
const top = parseInt(computedStyle.getPropertyValue('--sat').replace('px', '')) || 0;
const right = parseInt(computedStyle.getPropertyValue('--sar').replace('px', '')) || 0;
const bottom = parseInt(computedStyle.getPropertyValue('--sab').replace('px', '')) || 0;
const left = parseInt(computedStyle.getPropertyValue('--sal').replace('px', '')) || 0;
setSafeAreaInsets({ top, right, bottom, left });
// 设置动态视口高度
const vh = window.innerHeight;
setViewportHeight(vh);
document.documentElement.style.setProperty('--vh', `${vh * 0.01}px`);
};
updateSafeArea();
window.addEventListener('resize', updateSafeArea);
window.addEventListener('orientationchange', updateSafeArea);
// 延迟更新以处理移动端浏览器地址栏变化
const timeoutId = setTimeout(updateSafeArea, 500);
return () => {
window.removeEventListener('resize', updateSafeArea);
window.removeEventListener('orientationchange', updateSafeArea);
clearTimeout(timeoutId);
};
}, []);
return {
safeAreaInsets,
viewportHeight,
/** 获取考虑安全区域的样式 */
getSafeAreaStyle: (includeBottom = true) => ({
paddingTop: `max(1rem, ${safeAreaInsets.top}px)`,
paddingRight: `max(1rem, ${safeAreaInsets.right}px)`,
paddingBottom: includeBottom ? `max(1rem, ${safeAreaInsets.bottom}px)` : undefined,
paddingLeft: `max(1rem, ${safeAreaInsets.left}px)`,
})
};
}

View File

@ -90,6 +90,20 @@ module.exports = {
'100': '100ms', '100': '100ms',
'200': '200ms', '200': '200ms',
'300': '300ms', '300': '300ms',
},
spacing: {
'safe-top': 'env(safe-area-inset-top)',
'safe-right': 'env(safe-area-inset-right)',
'safe-bottom': 'env(safe-area-inset-bottom)',
'safe-left': 'env(safe-area-inset-left)',
},
height: {
'dvh': '100dvh',
'safe-screen': 'calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom))',
},
minHeight: {
'dvh': '100dvh',
'safe-screen': 'calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom))',
} }
}, },
}, },