From 3a2eaa1ecc0a7d9f35b38bb0d9e09edc60f40d54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=97=E6=9E=B3?= <7854742+wang_rumeng@user.noreply.gitee.com> Date: Thu, 25 Sep 2025 11:29:53 +0800 Subject: [PATCH] =?UTF-8?q?=E9=80=82=E9=85=8D=E6=89=8B=E6=9C=BA=E7=AB=AF?= =?UTF-8?q?=E5=BA=95=E9=83=A8=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/globals.css | 54 ++++++++++++++++++++-- app/layout.tsx | 2 +- components/layout/dashboard-layout.tsx | 44 ++++++++++++++++-- hooks/useDeviceType.ts | 38 ++++++++------- hooks/useSafeArea.ts | 64 ++++++++++++++++++++++++++ tailwind.config.js | 14 ++++++ 6 files changed, 189 insertions(+), 27 deletions(-) create mode 100644 hooks/useSafeArea.ts 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))', } }, },