forked from 77media/video-flow
适配手机端底部状态
This commit is contained in:
parent
760937aa51
commit
3a2eaa1ecc
@ -95,8 +95,10 @@
|
||||
--accent-foreground: 0 0% 98%;
|
||||
|
||||
/* 自定义渐变色变量 */
|
||||
--custom-blue: 186 100% 70%; /* rgb(106, 244, 249) */
|
||||
--custom-purple: 280 100% 62%; /* rgb(199, 59, 255) */
|
||||
--custom-blue: 186 100% 70%;
|
||||
/* rgb(106, 244, 249) */
|
||||
--custom-purple: 280 100% 62%;
|
||||
/* rgb(199, 59, 255) */
|
||||
--custom-blue-rgb: 106, 244, 249;
|
||||
--custom-purple-rgb: 199, 59, 255;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
@ -162,6 +164,7 @@ body {
|
||||
.hide-scrollbar::-webkit-scrollbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
display: none !important;
|
||||
}
|
||||
@ -184,6 +187,7 @@ body {
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.button-NxtqWZ:hover {
|
||||
background-color: #2f3237 !important;
|
||||
}
|
||||
@ -226,6 +230,7 @@ body {
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ant-spin-nested-loading .ant-spin {
|
||||
max-height: none !important;
|
||||
}
|
||||
@ -236,6 +241,7 @@ body {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
@ -262,3 +268,41 @@ body {
|
||||
padding: unset !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);
|
||||
}
|
||||
}
|
||||
@ -43,7 +43,7 @@ export default function RootLayout({
|
||||
<head>
|
||||
<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="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="32x32" href="/favicon.ico?v=1" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico?v=1" />
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Sidebar } from './sidebar';
|
||||
import { TopBar } from './top-bar';
|
||||
import { useDeviceType } from '@/hooks/useDeviceType';
|
||||
@ -10,9 +10,28 @@ interface DashboardLayoutProps {
|
||||
}
|
||||
|
||||
export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(true); // 默认收起状态
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(true);
|
||||
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 = () => {
|
||||
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 (
|
||||
<div className="min-h-screen bg-background">
|
||||
<TopBar collapsed={sidebarCollapsed} isDesktop={isDesktop} />
|
||||
{isDesktop && <Sidebar collapsed={sidebarCollapsed} onToggle={setSidebarCollapsed} />}
|
||||
<div
|
||||
className="h-[calc(100vh-4rem)] top-[4rem] fixed right-0 bottom-0 px-4"
|
||||
style={getLayoutStyles()}>
|
||||
className={`top-[4rem] fixed right-0 bottom-0 px-4 ${getMobileContainerClasses()}`}
|
||||
style={{
|
||||
...getLayoutStyles(),
|
||||
// 移动端使用动态高度计算
|
||||
height: (isMobile || isTablet)
|
||||
? 'calc(100dvh - 4rem)'
|
||||
: 'calc(100vh - 4rem)'
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2,16 +2,16 @@ import { useState, useEffect } from 'react';
|
||||
|
||||
// 定义设备类型枚举
|
||||
export enum DeviceType {
|
||||
MOBILE = 'mobile', // 手机
|
||||
TABLET = 'tablet', // 平板
|
||||
DESKTOP = 'desktop' // 桌面端
|
||||
MOBILE = 'mobile',
|
||||
TABLET = 'tablet',
|
||||
DESKTOP = 'desktop'
|
||||
}
|
||||
|
||||
// 定义屏幕断点
|
||||
const BREAKPOINTS = {
|
||||
MOBILE: 480, // 0-480px 为手机
|
||||
TABLET: 1024, // 481-1024px 为平板
|
||||
DESKTOP: 1025 // 1025px 及以上为桌面端
|
||||
MOBILE: 480,
|
||||
TABLET: 1024,
|
||||
DESKTOP: 1025
|
||||
};
|
||||
|
||||
export function useDeviceType() {
|
||||
@ -22,35 +22,39 @@ export function useDeviceType() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* 根据窗口宽度判断设备类型
|
||||
*/
|
||||
const getDeviceType = (width: number): DeviceType => {
|
||||
if (width <= BREAKPOINTS.MOBILE) return DeviceType.MOBILE;
|
||||
if (width <= BREAKPOINTS.TABLET) return DeviceType.TABLET;
|
||||
return DeviceType.DESKTOP;
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理窗口大小变化
|
||||
*/
|
||||
const handleResize = () => {
|
||||
const width = window.innerWidth;
|
||||
const height = window.innerHeight;
|
||||
|
||||
setWindowSize({ width, height });
|
||||
setDeviceType(getDeviceType(width));
|
||||
|
||||
// 移动端动态视口高度处理
|
||||
if (width <= BREAKPOINTS.TABLET) {
|
||||
const vh = height * 0.01;
|
||||
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化设备类型
|
||||
// 初始化
|
||||
handleResize();
|
||||
|
||||
// 添加窗口大小变化监听
|
||||
// 添加事件监听
|
||||
window.addEventListener('resize', handleResize);
|
||||
window.addEventListener('orientationchange', () => {
|
||||
// 延迟处理以确保获取正确的视口尺寸
|
||||
setTimeout(handleResize, 100);
|
||||
});
|
||||
|
||||
// 清理监听器
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
window.removeEventListener('orientationchange', handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@ -59,6 +63,8 @@ export function useDeviceType() {
|
||||
windowSize,
|
||||
isMobile: deviceType === DeviceType.MOBILE,
|
||||
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
64
hooks/useSafeArea.ts
Normal 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)`,
|
||||
})
|
||||
};
|
||||
}
|
||||
@ -90,6 +90,20 @@ module.exports = {
|
||||
'100': '100ms',
|
||||
'200': '200ms',
|
||||
'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))',
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user