feat: 实现智能屏幕适配功能

新功能:
- 根据用户屏幕尺寸自动调整窗口大小
- 支持小屏幕、中等屏幕、大屏幕、超宽屏的智能适配
- 提供屏幕适配设置页面,用户可自定义配置
- 应用启动时自动应用屏幕适配

技术实现:
- 创建ScreenAdaptationService服务类
- 支持动态获取屏幕信息和显示器配置
- 提供智能适配和手动配置两种模式
- 集成到应用设置页面,提供友好的UI界面

配置选项:
- 默认宽度/高度比例可调节
- 最小窗口尺寸限制
- 最大窗口尺寸比例
- 支持不同屏幕类型的预设配置

用户体验:
- 应用启动时自动适配屏幕
- 设置页面提供实时预览
- 支持一键智能适配和手动微调
- 窗口居中显示,提升视觉体验

适配策略:
- 小屏幕(<1366x768): 95%宽度, 90%高度
- 中等屏幕(1366-1920): 85%宽度, 85%高度
- 大屏幕(>=1920x1080): 75%宽度, 80%高度
- 超宽屏(21:9+): 70%宽度, 85%高度

解决了固定窗口尺寸在不同屏幕上显示不佳的问题
This commit is contained in:
imeepos 2025-07-23 20:55:51 +08:00
parent 3d24f5b908
commit 78f27983a6
6 changed files with 674 additions and 5 deletions

View File

@ -15,8 +15,8 @@
"title": "MixVideo Desktop",
"width": 1200,
"height": 900,
"minWidth": 800,
"minHeight": 600,
"minWidth": 1200,
"minHeight": 900,
"center": true,
"resizable": true,
"maximizable": true,

View File

@ -1,4 +1,5 @@
import { useEffect } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { ProjectList } from './components/ProjectList';
import { ProjectForm } from './components/ProjectForm';
@ -16,6 +17,7 @@ import DebugPanelTool from './pages/tools/DebugPanelTool';
import ChatTool from './pages/tools/ChatTool';
import ChatTestPage from './pages/tools/ChatTestPage';
import WatermarkTool from './pages/tools/WatermarkTool';
import Settings from './pages/Settings';
// import BatchThumbnailGenerator from './pages/tools/BatchThumbnailGenerator';
import Navigation from './components/Navigation';
@ -23,6 +25,7 @@ import { NotificationSystem, useNotifications } from './components/NotificationS
import { useProjectStore } from './store/projectStore';
import { useUIStore } from './store/uiStore';
import { CreateProjectRequest, UpdateProjectRequest } from './types/project';
import { screenAdaptationService } from './services/screenAdaptationService';
import "./App.css";
import './styles/design-system.css';
import './styles/animations.css';
@ -47,6 +50,20 @@ function App() {
// 通知系统
const { notifications, removeNotification, success, error } = useNotifications();
// 屏幕适配
useEffect(() => {
const initScreenAdaptation = async () => {
try {
await screenAdaptationService.applySmartAdaptation();
console.log('屏幕适配初始化完成');
} catch (error) {
console.error('屏幕适配初始化失败:', error);
}
};
initScreenAdaptation();
}, []);
// 处理创建项目
const handleCreateProject = async (data: CreateProjectRequest) => {
try {
@ -82,8 +99,8 @@ function App() {
<Navigation />
{/* 可滚动的主要内容区域 */}
<main className="flex-1 overflow-y-auto smooth-scroll">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-4 sm:py-6 lg:py-8 max-w-7xl min-h-full">
<main className="flex-1 relative">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-4 sm:py-6 lg:py-8 overflow-hidden max-w-full absolute top-0 left-0 right-0 bottom-0">
<Routes>
<Route path="/" element={<ProjectList />} />
<Route path="/project/:id" element={<ProjectDetails />} />
@ -102,6 +119,7 @@ function App() {
<Route path="/tools/ai-chat" element={<ChatTool />} />
<Route path="/tools/chat-test" element={<ChatTestPage />} />
<Route path="/tools/watermark" element={<WatermarkTool />} />
<Route path="/settings" element={<Settings />} />
{/* <Route path="/tools/batch-thumbnail-generator" element={<BatchThumbnailGenerator />} /> */}
</Routes>
</div>

View File

@ -7,7 +7,8 @@ import {
DocumentDuplicateIcon,
LinkIcon,
WrenchScrewdriverIcon,
SparklesIcon
SparklesIcon,
CogIcon
} from '@heroicons/react/24/outline';
const Navigation: React.FC = () => {
@ -55,6 +56,12 @@ const Navigation: React.FC = () => {
href: '/tools',
icon: WrenchScrewdriverIcon,
description: 'AI检索图片/数据清洗工具'
},
{
name: '应用设置',
href: '/settings',
icon: CogIcon,
description: '屏幕适配和应用配置'
}
];

View File

@ -0,0 +1,239 @@
import React, { useState, useEffect } from 'react';
import { screenAdaptationService } from '../services/screenAdaptationService';
import { getCurrentWindow } from '@tauri-apps/api/window';
interface ScreenAdaptationSettingsProps {
onClose?: () => void;
}
/**
*
*
*/
export const ScreenAdaptationSettings: React.FC<ScreenAdaptationSettingsProps> = ({
onClose
}) => {
const [config, setConfig] = useState(screenAdaptationService.getConfig());
const [screenInfo, setScreenInfo] = useState<{
width: number;
height: number;
type: string;
} | null>(null);
const [currentSize, setCurrentSize] = useState<{
width: number;
height: number;
} | null>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
loadScreenInfo();
loadCurrentSize();
}, []);
const loadScreenInfo = async () => {
try {
const { type } = await screenAdaptationService.getScreenTypeConfig();
// 模拟获取屏幕信息
const info = {
width: (globalThis as any).screen?.availWidth || 1920,
height: (globalThis as any).screen?.availHeight || 1080,
type: type
};
setScreenInfo(info);
} catch (error) {
console.error('获取屏幕信息失败:', error);
}
};
const loadCurrentSize = async () => {
try {
const window = getCurrentWindow();
const size = await window.innerSize();
setCurrentSize({
width: size.width,
height: size.height
});
} catch (error) {
console.error('获取当前窗口尺寸失败:', error);
}
};
const handleConfigChange = (key: string, value: number) => {
const newConfig = { ...config, [key]: value };
setConfig(newConfig);
screenAdaptationService.updateConfig(newConfig);
};
const handleApplyAdaptation = async () => {
setLoading(true);
try {
await screenAdaptationService.applyScreenAdaptation();
await loadCurrentSize();
} catch (error) {
console.error('应用屏幕适配失败:', error);
} finally {
setLoading(false);
}
};
const handleSmartAdaptation = async () => {
setLoading(true);
try {
await screenAdaptationService.applySmartAdaptation();
setConfig(screenAdaptationService.getConfig());
await loadCurrentSize();
} catch (error) {
console.error('智能适配失败:', error);
} finally {
setLoading(false);
}
};
const previewSize = screenInfo ? {
width: Math.floor(screenInfo.width * config.defaultWidthRatio),
height: Math.floor(screenInfo.height * config.defaultHeightRatio)
} : null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
<div className="p-6">
<div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-semibold text-gray-900"></h2>
{onClose && (
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 transition-colors"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
</div>
{/* 屏幕信息 */}
{screenInfo && (
<div className="mb-6 p-4 bg-blue-50 rounded-lg">
<h3 className="text-sm font-medium text-blue-900 mb-2"></h3>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-blue-700">:</span>
<span className="ml-2 font-mono">{screenInfo.width} × {screenInfo.height}</span>
</div>
<div>
<span className="text-blue-700">:</span>
<span className="ml-2 capitalize">{screenInfo.type}</span>
</div>
{currentSize && (
<div>
<span className="text-blue-700">:</span>
<span className="ml-2 font-mono">{currentSize.width} × {currentSize.height}</span>
</div>
)}
{previewSize && (
<div>
<span className="text-blue-700">:</span>
<span className="ml-2 font-mono">{previewSize.width} × {previewSize.height}</span>
</div>
)}
</div>
</div>
)}
{/* 配置选项 */}
<div className="space-y-6">
<div>
<h3 className="text-lg font-medium text-gray-900 mb-4"></h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
({Math.round(config.defaultWidthRatio * 100)}%)
</label>
<input
type="range"
min="0.5"
max="1"
step="0.05"
value={config.defaultWidthRatio}
onChange={(e) => handleConfigChange('defaultWidthRatio', parseFloat(e.target.value))}
className="w-full"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
({Math.round(config.defaultHeightRatio * 100)}%)
</label>
<input
type="range"
min="0.5"
max="1"
step="0.05"
value={config.defaultHeightRatio}
onChange={(e) => handleConfigChange('defaultHeightRatio', parseFloat(e.target.value))}
className="w-full"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
(px)
</label>
<input
type="number"
min="600"
max="1200"
value={config.minWidth}
onChange={(e) => handleConfigChange('minWidth', parseInt(e.target.value))}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
(px)
</label>
<input
type="number"
min="400"
max="900"
value={config.minHeight}
onChange={(e) => handleConfigChange('minHeight', parseInt(e.target.value))}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
</div>
</div>
{/* 操作按钮 */}
<div className="flex space-x-4 pt-4 border-t border-gray-200">
<button
onClick={handleSmartAdaptation}
disabled={loading}
className="flex-1 bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{loading ? '应用中...' : '智能适配'}
</button>
<button
onClick={handleApplyAdaptation}
disabled={loading}
className="flex-1 bg-green-600 text-white px-4 py-2 rounded-md hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{loading ? '应用中...' : '应用设置'}
</button>
</div>
<div className="text-xs text-gray-500 space-y-1">
<p> <strong></strong></p>
<p> <strong></strong>使</p>
<p> </p>
</div>
</div>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,152 @@
import React, { useState } from 'react';
import { ScreenAdaptationSettings } from '../components/ScreenAdaptationSettings';
/**
*
*
*/
const Settings: React.FC = () => {
const [showScreenSettings, setShowScreenSettings] = useState(false);
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-4xl mx-auto py-8 px-4">
<div className="bg-white rounded-lg shadow-sm">
<div className="px-6 py-4 border-b border-gray-200">
<h1 className="text-2xl font-semibold text-gray-900"></h1>
<p className="text-gray-600 mt-1"></p>
</div>
<div className="p-6">
<div className="space-y-6">
{/* 显示设置 */}
<div className="border border-gray-200 rounded-lg p-4">
<h2 className="text-lg font-medium text-gray-900 mb-4"></h2>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-gray-900"></h3>
<p className="text-sm text-gray-500"></p>
</div>
<button
onClick={() => setShowScreenSettings(true)}
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition-colors text-sm"
>
</button>
</div>
</div>
</div>
{/* 性能设置 */}
<div className="border border-gray-200 rounded-lg p-4">
<h2 className="text-lg font-medium text-gray-900 mb-4"></h2>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-gray-900"></h3>
<p className="text-sm text-gray-500">GPU加速以提升性能</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" className="sr-only peer" defaultChecked />
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
</label>
</div>
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-gray-900"></h3>
<p className="text-sm text-gray-500"></p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" className="sr-only peer" defaultChecked />
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
</label>
</div>
</div>
</div>
{/* 通知设置 */}
<div className="border border-gray-200 rounded-lg p-4">
<h2 className="text-lg font-medium text-gray-900 mb-4"></h2>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-gray-900"></h3>
<p className="text-sm text-gray-500"></p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" className="sr-only peer" defaultChecked />
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
</label>
</div>
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-gray-900"></h3>
<p className="text-sm text-gray-500"></p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" className="sr-only peer" />
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
</label>
</div>
</div>
</div>
{/* 数据设置 */}
<div className="border border-gray-200 rounded-lg p-4">
<h2 className="text-lg font-medium text-gray-900 mb-4"></h2>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-gray-900"></h3>
<p className="text-sm text-gray-500"></p>
</div>
<button className="bg-green-600 text-white px-4 py-2 rounded-md hover:bg-green-700 transition-colors text-sm">
</button>
</div>
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-gray-900"></h3>
<p className="text-sm text-gray-500"></p>
</div>
<button className="bg-orange-600 text-white px-4 py-2 rounded-md hover:bg-orange-700 transition-colors text-sm">
</button>
</div>
</div>
</div>
{/* 关于信息 */}
<div className="border border-gray-200 rounded-lg p-4">
<h2 className="text-lg font-medium text-gray-900 mb-4"></h2>
<div className="space-y-2 text-sm text-gray-600">
<p><strong>:</strong> 0.2.1</p>
<p><strong>:</strong> {new Date().toLocaleDateString()}</p>
<p><strong>:</strong> Tauri + React + TypeScript</p>
</div>
</div>
</div>
</div>
</div>
</div>
{/* 屏幕适配设置弹窗 */}
{showScreenSettings && (
<ScreenAdaptationSettings
onClose={() => setShowScreenSettings(false)}
/>
)}
</div>
);
};
export default Settings;

View File

@ -0,0 +1,253 @@
import { getCurrentWindow } from '@tauri-apps/api/window';
import { LogicalSize } from '@tauri-apps/api/window';
/**
*
*/
interface ScreenAdaptationConfig {
/** 最小宽度 */
minWidth: number;
/** 最小高度 */
minHeight: number;
/** 最大宽度比例(相对于屏幕宽度) */
maxWidthRatio: number;
/** 最大高度比例(相对于屏幕高度) */
maxHeightRatio: number;
/** 默认宽度比例 */
defaultWidthRatio: number;
/** 默认高度比例 */
defaultHeightRatio: number;
}
/**
*
*/
interface ScreenInfo {
width: number;
height: number;
scaleFactor: number;
}
/**
*
*/
interface WindowSize {
width: number;
height: number;
}
/**
*
*
*/
export class ScreenAdaptationService {
private static instance: ScreenAdaptationService;
private config: ScreenAdaptationConfig;
private constructor() {
this.config = {
minWidth: 800,
minHeight: 600,
maxWidthRatio: 0.9,
maxHeightRatio: 0.9,
defaultWidthRatio: 0.75,
defaultHeightRatio: 0.8,
};
}
public static getInstance(): ScreenAdaptationService {
if (!ScreenAdaptationService.instance) {
ScreenAdaptationService.instance = new ScreenAdaptationService();
}
return ScreenAdaptationService.instance;
}
/**
*
*/
private async getScreenInfo(): Promise<ScreenInfo> {
try {
// 获取主显示器信息
const { availableMonitors } = await import('@tauri-apps/api/window');
const monitors = await availableMonitors();
const primaryMonitor = monitors.find(m => m.name === 'primary') || monitors[0];
if (primaryMonitor) {
return {
width: primaryMonitor.size.width,
height: primaryMonitor.size.height,
scaleFactor: primaryMonitor.scaleFactor,
};
}
} catch (error) {
console.warn('无法获取显示器信息,使用默认值:', error);
}
// 回退到浏览器API如果可用
if (typeof globalThis !== 'undefined' && 'screen' in globalThis) {
const screen = (globalThis as any).screen;
return {
width: screen.availWidth || 1920,
height: screen.availHeight || 1080,
scaleFactor: (globalThis as any).devicePixelRatio || 1,
};
}
// 默认值
return {
width: 1920,
height: 1080,
scaleFactor: 1,
};
}
/**
*
*/
private calculateOptimalSize(screenInfo: ScreenInfo): WindowSize {
const { width: screenWidth, height: screenHeight } = screenInfo;
// 计算基于比例的尺寸
let width = Math.floor(screenWidth * this.config.defaultWidthRatio);
let height = Math.floor(screenHeight * this.config.defaultHeightRatio);
// 应用最小尺寸限制
width = Math.max(width, this.config.minWidth);
height = Math.max(height, this.config.minHeight);
// 应用最大尺寸限制
const maxWidth = Math.floor(screenWidth * this.config.maxWidthRatio);
const maxHeight = Math.floor(screenHeight * this.config.maxHeightRatio);
width = Math.min(width, maxWidth);
height = Math.min(height, maxHeight);
return { width, height };
}
/**
*
*/
public async applyScreenAdaptation(): Promise<void> {
try {
const window = getCurrentWindow();
const screenInfo = await this.getScreenInfo();
const optimalSize = this.calculateOptimalSize(screenInfo);
console.log('屏幕信息:', screenInfo);
console.log('计算的最佳窗口尺寸:', optimalSize);
// 设置窗口尺寸
await window.setSize(new LogicalSize(optimalSize.width, optimalSize.height));
// 居中显示
await window.center();
// 设置最小尺寸
await window.setMinSize(new LogicalSize(this.config.minWidth, this.config.minHeight));
console.log('屏幕适配完成');
} catch (error) {
console.error('屏幕适配失败:', error);
}
}
/**
*
*/
public async getScreenTypeConfig(): Promise<{
type: 'small' | 'medium' | 'large' | 'ultrawide';
config: Partial<ScreenAdaptationConfig>;
}> {
const screenInfo = await this.getScreenInfo();
const { width, height } = screenInfo;
const aspectRatio = width / height;
// 小屏幕 (< 1366x768)
if (width < 1366 || height < 768) {
return {
type: 'small',
config: {
defaultWidthRatio: 0.95,
defaultHeightRatio: 0.9,
minWidth: 800,
minHeight: 600,
},
};
}
// 超宽屏 (21:9 或更宽)
if (aspectRatio >= 2.3) {
return {
type: 'ultrawide',
config: {
defaultWidthRatio: 0.7,
defaultHeightRatio: 0.85,
minWidth: 1200,
minHeight: 700,
},
};
}
// 大屏幕 (>= 1920x1080)
if (width >= 1920 && height >= 1080) {
return {
type: 'large',
config: {
defaultWidthRatio: 0.75,
defaultHeightRatio: 0.8,
minWidth: 1000,
minHeight: 700,
},
};
}
// 中等屏幕
return {
type: 'medium',
config: {
defaultWidthRatio: 0.85,
defaultHeightRatio: 0.85,
minWidth: 900,
minHeight: 650,
},
};
}
/**
*
*/
public async applySmartAdaptation(): Promise<void> {
try {
const { type, config } = await this.getScreenTypeConfig();
// 更新配置
this.config = { ...this.config, ...config };
console.log(`检测到屏幕类型: ${type}`);
console.log('应用配置:', config);
// 应用适配
await this.applyScreenAdaptation();
} catch (error) {
console.error('智能适配失败:', error);
}
}
/**
*
*/
public updateConfig(newConfig: Partial<ScreenAdaptationConfig>): void {
this.config = { ...this.config, ...newConfig };
}
/**
*
*/
public getConfig(): ScreenAdaptationConfig {
return { ...this.config };
}
}
// 导出单例实例
export const screenAdaptationService = ScreenAdaptationService.getInstance();