diff --git a/app/globals.css b/app/globals.css
index f8f96bd..29fa569 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -50,7 +50,7 @@
*,
*:after,
*:before {
- box-sizing: border-box;
+ box-sizing: border-box;
}
:root {
@@ -93,10 +93,12 @@
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--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);
@@ -256,9 +262,47 @@ body {
.ant-switch.ant-switch-checked:hover {
background: rgb(146 78 173) !important;
-}
+}
.language-dropdown li {
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);
+ }
}
\ No newline at end of file
diff --git a/app/layout.tsx b/app/layout.tsx
index db5e91e..118ff61 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -43,7 +43,7 @@ export default function RootLayout({
MovieFlow - AI Movie Studio
-
+
diff --git a/components/layout/dashboard-layout.tsx b/components/layout/dashboard-layout.tsx
index 6008f28..0d77dd4 100644
--- a/components/layout/dashboard-layout.tsx
+++ b/components/layout/dashboard-layout.tsx
@@ -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 (
-
+
{isDesktop &&
}
+ className={`top-[4rem] fixed right-0 bottom-0 px-4 ${getMobileContainerClasses()}`}
+ style={{
+ ...getLayoutStyles(),
+ // 移动端使用动态高度计算
+ height: (isMobile || isTablet)
+ ? 'calc(100dvh - 4rem)'
+ : 'calc(100vh - 4rem)'
+ }}
+ >
{children}
diff --git a/hooks/useDeviceType.ts b/hooks/useDeviceType.ts
index a46721f..30bb631 100644
--- a/hooks/useDeviceType.ts
+++ b/hooks/useDeviceType.ts
@@ -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
};
}
\ No newline at end of file
diff --git a/hooks/useSafeArea.ts b/hooks/useSafeArea.ts
new file mode 100644
index 0000000..42cdb71
--- /dev/null
+++ b/hooks/useSafeArea.ts
@@ -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
({
+ 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)`,
+ })
+ };
+}
diff --git a/tailwind.config.js b/tailwind.config.js
index d01ae8a..fae3ec4 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -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))',
}
},
},