From d18a3d52791511d4600305e8bac7862740a606dd Mon Sep 17 00:00:00 2001 From: Azure <983547216@qq.com> Date: Thu, 25 Sep 2025 13:36:38 +0800 Subject: [PATCH] 1 --- .qoder/quests/window-drag-resize-control.md | 314 +++++++++++++ src/events/WindowFormEventManager.ts | 41 +- src/services/EventCommunicationService.ts | 5 +- src/services/SystemServiceIntegration.ts | 85 +++- src/services/WindowService.ts | 493 +++++++++++++++++++- src/ui/types/WindowFormTypes.ts | 32 ++ 6 files changed, 931 insertions(+), 39 deletions(-) create mode 100644 .qoder/quests/window-drag-resize-control.md diff --git a/.qoder/quests/window-drag-resize-control.md b/.qoder/quests/window-drag-resize-control.md new file mode 100644 index 0000000..c6f8c92 --- /dev/null +++ b/.qoder/quests/window-drag-resize-control.md @@ -0,0 +1,314 @@ +# 窗体功能增强设计文档 + +## 1. 概述 + +### 1.1 功能目标 + +1. 为项目中的窗体组件添加8个方向的拖拽调整尺寸功能,增强用户交互体验 +2. 修复窗体最大化、最小化、还原功能存在的问题 +3. 完善窗体状态管理和事件通知机制 + +### 1.2 当前系统分析 + +通过代码分析发现,当前系统已具备以下基础功能: + +- 窗体创建、销毁、移动功能 +- 窗体最大化、最小化、还原功能 +- 基本的事件管理系统 +- 窗体状态管理和配置 + +但现有功能存在以下问题: + +1. 缺少用户通过鼠标拖拽调整窗体尺寸的功能 +2. 窗体最大化、最小化、还原功能实现不完整,缺少与事件系统的完整集成 +3. 窗体状态变更时未正确触发`windowFormDataUpdate`事件通知 + +## 2. 架构设计 + +### 2.1 整体架构 + +```mermaid +graph TD + A[用户操作] --> B[窗体功能处理器] + B --> C[窗体状态管理] + C --> D[UI更新引擎] + D --> E[界面重渲染] + C --> F[事件通知系统] + + G[窗体服务] --> C + H[事件管理器] --> F +``` + +### 2.2 核心组件 + +| 组件名称 | 职责 | 说明 | +| --------------------- | ------------------ | -------------------------------------- | +| WindowOperationHandler | 窗体操作处理核心 | 处理最大化、最小化、还原和拖拽调整尺寸操作 | +| WindowManager | 窗体状态管理 | 管理窗体状态变更和配置更新 | +| WindowRenderer | UI更新引擎 | 应用新状态到窗体界面 | +| WindowEventManager | 事件管理器 | 发布窗体状态变更事件 | + +## 3. 功能设计 + +### 3.1 窗体状态操作 + +窗体支持以下状态操作: + +| 操作 | 状态变更 | 事件触发 | 说明 | +| ------ | ---------------------------- | ------------------------------------------ | ------------------------------------------ | +| 最大化 | default/minimized → maximized | windowFormMaximize, windowFormDataUpdate | 窗体占据除任务栏外的整个屏幕空间 | +| 最小化 | default/maximized → minimized | windowFormMinimize | 窗体隐藏,仅在任务栏保留图标 | +| 还原 | minimized/maximized → default | windowFormRestore, windowFormDataUpdate | 窗体恢复到正常尺寸和位置 | + +### 3.2 拖拽调整尺寸方向定义 + +窗体支持8个方向的拖拽调整尺寸,具体如下: + +```mermaid +graph TD + subgraph 窗体 + direction LR + A[↖] --- B[↑] --- C[↗] + D[←] --- E[窗体内容] --- F[→] + G[↙] --- H[↓] --- I[↘] + end + + classDef corner fill:#f9f,stroke:#333; + classDef edge fill:#bbf,stroke:#333; + + class A,C,G,I,corner + class B,D,F,H,edge +``` + +| 方向 | 标识符 | 影响属性 | 说明 | +| ------ | ----------- | ------------------- | ------------------ | +| 左上角 | topLeft | width, height, x, y | 同时调整宽高和位置 | +| 上边缘 | top | height, y | 调整高度和垂直位置 | +| 右上角 | topRight | width, height, y | 调整宽高和垂直位置 | +| 右边缘 | right | width | 仅调整宽度 | +| 右下角 | bottomRight | width, height | 仅调整宽高 | +| 下边缘 | bottom | height | 仅调整高度 | +| 左下角 | bottomLeft | width, height, x | 调整宽高和水平位置 | +| 左边缘 | left | width, x | 调整宽度和水平位置 | + +### 3.3 交互流程 + +```mermaid +sequenceDiagram + participant U as 用户 + participant WH as 窗体句柄 + participant WOH as 操作处理器 + participant WM as 窗体管理器 + participant WR as 窗体渲染器 + participant EM as 事件管理器 + + U->>WH: 执行操作(最大化/最小化/还原/拖拽) + WH->>WOH: 处理操作请求 + WOH->>WM: 更新窗体状态 + WM->>WR: 应用新状态 + WM->>EM: 发布状态变更事件 + EM->>EM: 发布windowFormDataUpdate事件 + WR->>U: 更新界面显示 +``` + +## 4. 数据模型 + +### 4.1 窗体状态数据结构 + +```typescript +interface IWindowFormDataUpdateParams { + /** 窗口id */ + id: string; + /** 窗口状态 */ + state: TWindowFormState; + /** 窗口宽度 */ + width: number; + /** 窗口高度 */ + height: number; + /** 窗口x坐标(左上角) */ + x: number; + /** 窗口y坐标(左上角) */ + y: number; +} + +type TWindowFormState = 'default' | 'minimized' | 'maximized'; +``` + +### 4.2 窗体尺寸配置扩展 + +在现有`WindowConfig`接口基础上增加最小/最大尺寸限制: + +| 字段 | 类型 | 必填 | 说明 | +| --------- | ------- | ---- | -------------------------- | +| minWidth | number | 可选 | 窗体最小宽度(像素) | +| minHeight | number | 可选 | 窗体最小高度(像素) | +| maxWidth | number | 可选 | 窗体最大宽度(像素) | +| maxHeight | number | 可选 | 窗体最大高度(像素) | +| resizable | boolean | 可选 | 是否可调整尺寸,默认为true | + +### 4.3 拖拽状态数据结构 + +```typescript +interface ResizeState { + /** 是否正在调整尺寸 */ + isResizing: boolean; + /** 调整方向 */ + direction: ResizeDirection; + /** 起始鼠标位置 */ + startX: number; + /** 起始鼠标位置 */ + startY: number; + /** 起始窗体宽度 */ + startWidth: number; + /** 起始窗体高度 */ + startHeight: number; + /** 起始窗体X坐标 */ + startXPosition: number; + /** 起始窗体Y坐标 */ + startYPosition: number; +} + +type ResizeDirection = + 'topLeft' | 'top' | 'topRight' | + 'right' | 'bottomRight' | 'bottom' | + 'bottomLeft' | 'left' | 'none'; +``` + +## 5. 事件系统设计 + +### 5.1 修复事件触发问题 + +当前窗体状态变更时未正确触发`windowFormDataUpdate`事件,需要修复以下问题: + +1. 在最大化操作完成后,应触发`windowFormDataUpdate`事件,携带窗体新状态和尺寸信息 +2. 在最小化操作完成后,应触发`windowFormDataUpdate`事件,携带窗体新状态信息 +3. 在还原操作完成后,应触发`windowFormDataUpdate`事件,携带窗体新状态和尺寸信息 + +### 5.2 新增事件定义 + +在现有`IWindowFormEvent`接口中添加以下事件: + +| 事件名称 | 参数类型 | 触发时机 | +| --------------------- | --------------------------- | ---------------- | +| windowFormResizeStart | string | 开始调整窗体尺寸 | +| windowFormResizing | IWindowFormDataUpdateParams | 调整尺寸过程中 | +| windowFormResizeEnd | string | 完成窗体尺寸调整 | + +### 5.3 事件触发流程 + +```mermaid +graph TD + A[窗体状态变更] --> B{是否需要更新数据?} + B -->|是| C[构造更新数据] + C --> D[触发windowFormDataUpdate事件] + B -->|否| E[直接触发状态事件] + D --> F[通知监听组件] + E --> F +``` + +## 6. 核心逻辑设计 + +### 6.1 窗体状态变更逻辑修复 + +当前窗体状态变更逻辑存在以下问题需要修复: + +1. **最大化逻辑问题**: + - 未正确保存原始窗体尺寸和位置信息 + - 未触发`windowFormDataUpdate`事件通知 + +2. **最小化逻辑问题**: + - 未触发`windowFormDataUpdate`事件通知 + +3. **还原逻辑问题**: + - 未正确恢复窗体尺寸和位置 + - 未触发`windowFormDataUpdate`事件通知 + +### 6.2 边缘检测算法 + +窗体边缘需要划分8个可拖拽区域,每个区域宽度为8px: + +```mermaid +graph TD + subgraph 边缘区域划分 + direction LR + A[8px↖] --- B[8px↑] --- C[8px↗] + D[8px←] --- E[窗体内容] --- F[8px→] + G[8px↙] --- H[8px↓] --- I[8px↘] + end +``` + +### 6.3 尺寸计算规则 + +1. **最小尺寸限制**:窗体宽度不能小于minWidth,高度不能小于minHeight +2. **最大尺寸限制**:窗体宽度不能大于maxWidth,高度不能大于maxHeight +3. **位置边界**:窗体不能被拖拽到屏幕外 + +### 6.4 拖拽处理流程 + +1. 鼠标按下时检测是否在边缘区域 +2. 根据鼠标位置确定拖拽方向 +3. 记录初始状态数据 +4. 鼠标移动时实时计算新尺寸 +5. 应用新尺寸并触发重渲染 +6. 鼠标释放时结束拖拽状态 + +## 7. UI/UX设计 + +### 7.1 视觉反馈 + +- 鼠标悬停在可拖拽边缘时,光标应变为对应方向的调整光标 +- 拖拽过程中窗体应有半透明遮罩效果 +- 拖拽完成后应有平滑的过渡动画 + +### 7.2 响应式适配 + +- 在不同屏幕分辨率下保持边缘区域的可点击性 +- 支持触屏设备的拖拽操作 +- 在窗体尺寸接近最小/最大限制时提供视觉提示 + +## 8. 性能优化 + +### 8.1 事件处理优化 + +- 使用防抖机制减少高频事件触发 +- 在拖拽过程中暂停不必要的重渲染 +- 使用requestAnimationFrame优化动画性能 + +### 8.2 内存管理 + +- 及时清理事件监听器 +- 复用计算对象避免频繁创建 +- 在窗体销毁时清理所有相关资源 + +## 9. 安全考虑 + +### 9.1 边界检查 + +- 确保窗体不能被调整到超出屏幕边界 +- 验证尺寸参数的有效性 +- 防止负值或异常大值的输入 + +### 9.2 权限控制 + +- 只有具有调整权限的窗体才能被调整尺寸 +- 外部应用的窗体尺寸调整需通过SDK接口 + +## 10. 测试策略 + +### 10.1 单元测试 + +- 边缘检测算法准确性测试 +- 尺寸计算边界条件测试 +- 事件发布/订阅机制测试 + +### 10.2 集成测试 + +- 不同方向拖拽功能测试 +- 尺寸限制功能测试 +- 与现有窗体功能集成测试 + +### 10.3 用户体验测试 + +- 拖拽流畅性测试 +- 视觉反馈效果测试 +- 不同设备兼容性测试 diff --git a/src/events/WindowFormEventManager.ts b/src/events/WindowFormEventManager.ts index 0da2044..06a41eb 100644 --- a/src/events/WindowFormEventManager.ts +++ b/src/events/WindowFormEventManager.ts @@ -10,49 +10,64 @@ export interface IWindowFormEvent extends IEventMap { * 窗口最小化 * @param id 窗口id */ - windowFormMinimize: (id: string) => void; + windowFormMinimize: (id: string) => void /** * 窗口最大化 * @param id 窗口id */ - windowFormMaximize: (id: string) => void; + windowFormMaximize: (id: string) => void /** * 窗口还原 * @param id 窗口id */ - windowFormRestore: (id: string) => void; + windowFormRestore: (id: string) => void /** * 窗口关闭 * @param id 窗口id */ - windowFormClose: (id: string) => void; + windowFormClose: (id: string) => void /** * 窗口聚焦 * @param id 窗口id */ - windowFormFocus: (id: string) => void; + windowFormFocus: (id: string) => void /** * 窗口数据更新 * @param data 窗口数据 */ - windowFormDataUpdate: (data: IWindowFormDataUpdateParams) => void; + windowFormDataUpdate: (data: IWindowFormDataUpdateParams) => void /** * 窗口创建完成 */ - windowFormCreated: () => void; + windowFormCreated: () => void + /** + * 开始调整窗体尺寸 + * @param id 窗口id + */ + windowFormResizeStart: (id: string) => void + /** + * 调整尺寸过程中 + * @param data 窗口数据 + */ + windowFormResizing: (data: IWindowFormDataUpdateParams) => void + /** + * 完成窗体尺寸调整 + * @param id 窗口id + */ + windowFormResizeEnd: (id: string) => void } -interface IWindowFormDataUpdateParams { +export interface IWindowFormDataUpdateParams { /** 窗口id */ - id: string; + id: string /** 窗口状态 */ - state: TWindowFormState, + state: TWindowFormState /** 窗口宽度 */ - width: number, + width: number /** 窗口高度 */ - height: number, + height: number /** 窗口x坐标(左上角) */ - x: number, + x: number /** 窗口y坐标(左上角) */ y: number } diff --git a/src/services/EventCommunicationService.ts b/src/services/EventCommunicationService.ts index 383c278..ec0687c 100644 --- a/src/services/EventCommunicationService.ts +++ b/src/services/EventCommunicationService.ts @@ -508,7 +508,10 @@ export class EventCommunicationService { if (subscribers.length === 0) { message.status = MessageStatus.FAILED - console.warn(`[EventCommunication] 没有找到频道 ${message.channel} 的订阅者[消息 ID: ${message.id}]`) + // 只对非系统频道显示警告信息 + if (message.channel !== 'system') { + console.warn(`[EventCommunication] 没有找到频道 ${message.channel} 的订阅者[消息 ID: ${message.id}]`) + } return } diff --git a/src/services/SystemServiceIntegration.ts b/src/services/SystemServiceIntegration.ts index 1d4d738..f41a8ff 100644 --- a/src/services/SystemServiceIntegration.ts +++ b/src/services/SystemServiceIntegration.ts @@ -9,6 +9,7 @@ import { EventCommunicationService } from './EventCommunicationService' import { ApplicationSandboxEngine } from './ApplicationSandboxEngine' import { ApplicationLifecycleManager } from './ApplicationLifecycleManager' import { externalAppDiscovery } from './ExternalAppDiscovery' +import type { IWindowFormDataUpdateParams } from '@/events/WindowFormEventManager' /** * 系统服务配置接口 @@ -404,6 +405,64 @@ export class SystemServiceIntegration { } }) + // 监听窗体数据更新事件 + this.eventBus.addEventListener( + 'onWindowFormDataUpdate', + (data: IWindowFormDataUpdateParams) => { + console.log(`[SystemIntegration] 接收到窗体数据更新事件:`, data) + // 只有在有订阅者时才发送消息 + if (this.eventService.getChannelSubscriberCount('window-form-data-update') > 0) { + this.eventService.sendMessage('system', 'window-form-data-update', data) + console.log(`[SystemIntegration] 已发送 window-form-data-update 消息到事件通信服务`) + } else { + console.log(`[SystemIntegration] 无订阅者,跳过发送 window-form-data-update 消息`) + } + }, + ) + + // 监听窗体调整尺寸开始事件 + this.eventBus.addEventListener('onResizeStart', (windowId: string) => { + console.log(`[SystemIntegration] 接收到窗体调整尺寸开始事件: ${windowId}`) + // 只有在有订阅者时才发送消息 + if (this.eventService.getChannelSubscriberCount('window-form-resize-start') > 0) { + this.eventService.sendMessage('system', 'window-form-resize-start', { windowId }) + } else { + console.log(`[SystemIntegration] 无订阅者,跳过发送 window-form-resize-start 消息`) + } + }) + + // 监听窗体调整尺寸过程中事件 + this.eventBus.addEventListener( + 'onResizing', + (windowId: string, width: number, height: number) => { + console.log(`[SystemIntegration] 接收到窗体调整尺寸过程中事件: ${windowId}`, { + width, + height, + }) + // 只有在有订阅者时才发送消息 + if (this.eventService.getChannelSubscriberCount('window-form-resizing') > 0) { + this.eventService.sendMessage('system', 'window-form-resizing', { + windowId, + width, + height, + }) + } else { + console.log(`[SystemIntegration] 无订阅者,跳过发送 window-form-resizing 消息`) + } + }, + ) + + // 监听窗体调整尺寸结束事件 + this.eventBus.addEventListener('onResizeEnd', (windowId: string) => { + console.log(`[SystemIntegration] 接收到窗体调整尺寸结束事件: ${windowId}`) + // 只有在有订阅者时才发送消息 + if (this.eventService.getChannelSubscriberCount('window-form-resize-end') > 0) { + this.eventService.sendMessage('system', 'window-form-resize-end', { windowId }) + } else { + console.log(`[SystemIntegration] 无订阅者,跳过发送 window-form-resize-end 消息`) + } + }) + // 监听资源配额超出 this.eventBus.addEventListener( 'onResourceQuotaExceeded', @@ -567,8 +626,18 @@ export class SystemServiceIntegration { return this.windowService.setWindowSize(windowId, data.width, data.height) case 'move': - // 需要实现窗体移动功能 - return true + // 实现窗体移动功能 + const window = this.windowService.getWindow(windowId); + if (window && window.element) { + // 更新窗体位置 + window.config.x = data.x; + window.config.y = data.y; + window.element.style.left = `${data.x}px`; + window.element.style.top = `${data.y}px`; + window.element.style.transform = 'none'; // 确保移除transform + return true; + } + return false; case 'minimize': return this.windowService.minimizeWindow(windowId) @@ -583,14 +652,14 @@ export class SystemServiceIntegration { return this.lifecycleManager.stopApp(appId) case 'getState': - const window = this.windowService.getWindow(windowId) - return window?.state + const windowInfo = this.windowService.getWindow(windowId) + return windowInfo?.state case 'getSize': - const windowInfo = this.windowService.getWindow(windowId) + const windowData = this.windowService.getWindow(windowId) return { - width: windowInfo?.config.width, - height: windowInfo?.config.height, + width: windowData?.config.width, + height: windowData?.config.height, } default: @@ -870,4 +939,4 @@ export class SystemServiceIntegration { console.log(`[SystemService] ${message}`, data) } } -} +} \ No newline at end of file diff --git a/src/services/WindowService.ts b/src/services/WindowService.ts index b9a6b2a..728c510 100644 --- a/src/services/WindowService.ts +++ b/src/services/WindowService.ts @@ -1,6 +1,7 @@ import { ref, reactive } from 'vue' import type { IEventBuilder, IEventMap } from '@/events/IEventBuilder' import { v4 as uuidv4 } from 'uuid' +import type { ResizeDirection, ResizeState } from '@/ui/types/WindowFormTypes' /** * 窗体状态枚举 @@ -132,6 +133,10 @@ export interface WindowInstance { * 窗体更新时间 */ updatedAt: Date + /** + * 拖拽调整尺寸状态 + */ + resizeState?: ResizeState } /** @@ -144,6 +149,9 @@ export interface WindowEvents extends IEventMap { onFocus: (windowId: string) => void onBlur: (windowId: string) => void onClose: (windowId: string) => void + onResizeStart: (windowId: string) => void + onResizing: (windowId: string, width: number, height: number) => void + onResizeEnd: (windowId: string) => void } /** @@ -157,6 +165,7 @@ export class WindowService { constructor(eventBus: IEventBuilder) { this.eventBus = eventBus + this.setupGlobalResizeEvents() } /** @@ -248,6 +257,9 @@ export class WindowService { window.element.style.display = 'none' } + // 发送窗体数据更新事件 + this.notifyWindowFormDataUpdate(windowId) + return true } @@ -276,10 +288,15 @@ export class WindowService { width: '100vw', height: 'calc(100vh - 40px)', // 减去任务栏高度 display: 'block', + transform: 'none' // 确保移除transform }) } this.setActiveWindow(windowId) + + // 发送窗体数据更新事件 + this.notifyWindowFormDataUpdate(windowId) + return true } @@ -312,14 +329,22 @@ export class WindowService { Object.assign(window.element.style, { width: originalWidth ? `${originalWidth}px` : `${window.config.width}px`, height: originalHeight ? `${originalHeight}px` : `${window.config.height}px`, - left: originalX ? `${originalX}px` : '50%', - top: originalY ? `${originalY}px` : '50%', - transform: originalX && originalY ? 'none' : 'translate(-50%, -50%)', + left: originalX ? `${originalX}px` : '0px', + top: originalY ? `${originalY}px` : '0px', + transform: 'none' // 确保移除transform }) + + // 更新配置中的位置 + if (originalX) window.config.x = parseFloat(originalX); + if (originalY) window.config.y = parseFloat(originalY); } } this.setActiveWindow(windowId) + + // 发送窗体数据更新事件 + this.notifyWindowFormDataUpdate(windowId) + return true } @@ -341,6 +366,9 @@ export class WindowService { } } + // 发送窗体数据更新事件 + this.notifyWindowFormDataUpdate(windowId) + return true } @@ -352,14 +380,8 @@ export class WindowService { if (!window) return false // 检查尺寸限制 - const finalWidth = Math.max( - window.config.minWidth || 200, - Math.min(window.config.maxWidth || Infinity, width), - ) - const finalHeight = Math.max( - window.config.minHeight || 150, - Math.min(window.config.maxHeight || Infinity, height), - ) + const finalWidth = this.clampDimension(width, window.config.minWidth, window.config.maxWidth) + const finalHeight = this.clampDimension(height, window.config.minHeight, window.config.maxHeight) window.config.width = finalWidth window.config.height = finalHeight @@ -371,6 +393,10 @@ export class WindowService { } this.eventBus.notifyEvent('onResize', windowId, finalWidth, finalHeight) + + // 发送窗体数据更新事件 + this.notifyWindowFormDataUpdate(windowId) + return true } @@ -410,6 +436,10 @@ export class WindowService { } this.eventBus.notifyEvent('onFocus', windowId) + + // 发送窗体数据更新事件 + this.notifyWindowFormDataUpdate(windowId) + return true } @@ -434,21 +464,36 @@ export class WindowService { windowElement.className = 'system-window' windowElement.id = `window-${id}` + // 计算初始位置 + let left = config.x; + let top = config.y; + + // 如果没有指定位置,则居中显示 + if (left === undefined || top === undefined) { + const centerX = Math.max(0, (window.innerWidth - config.width) / 2); + const centerY = Math.max(0, (window.innerHeight - config.height) / 2); + left = left !== undefined ? left : centerX; + top = top !== undefined ? top : centerY; + } + // 设置基本样式 Object.assign(windowElement.style, { position: 'fixed', width: `${config.width}px`, height: `${config.height}px`, - left: config.x ? `${config.x}px` : '50%', - top: config.y ? `${config.y}px` : '50%', - transform: config.x && config.y ? 'none' : 'translate(-50%, -50%)', + left: `${left}px`, + top: `${top}px`, zIndex: windowInstance.zIndex.toString(), backgroundColor: '#fff', border: '1px solid #ccc', borderRadius: '8px', boxShadow: '0 4px 20px rgba(0,0,0,0.15)', overflow: 'hidden', - }) + }); + + // 保存初始位置到配置中 + windowInstance.config.x = left; + windowInstance.config.y = top; // 创建窗体标题栏 const titleBar = this.createTitleBar(windowInstance) @@ -496,6 +541,11 @@ export class WindowService { windowElement.appendChild(contentArea) + // 添加拖拽调整尺寸功能 + if (config.resizable !== false) { + this.addResizeFunctionality(windowElement, windowInstance) + } + // 添加到页面 document.body.appendChild(windowElement) @@ -624,6 +674,17 @@ export class WindowService { let startTop = 0 titleBar.addEventListener('mousedown', (e) => { + // 检查是否正在调整尺寸,如果是则不处理拖拽 + if (windowInstance.resizeState?.isResizing) { + return; + } + + // 检查是否点击在调整尺寸手柄上,如果是则不处理拖拽 + const target = e.target as HTMLElement; + if (target.classList.contains('resize-handle')) { + return; + } + if (!windowInstance.element) return isDragging = true @@ -631,6 +692,17 @@ export class WindowService { startY = e.clientY const rect = windowInstance.element.getBoundingClientRect() + + // 如果使用了transform,需要转换为实际坐标 + if (windowInstance.element.style.transform && windowInstance.element.style.transform.includes('translate')) { + // 移除transform并设置实际的left/top值 + windowInstance.element.style.transform = 'none'; + windowInstance.config.x = rect.left; + windowInstance.config.y = rect.top; + windowInstance.element.style.left = `${rect.left}px`; + windowInstance.element.style.top = `${rect.top}px`; + } + startLeft = rect.left startTop = rect.top @@ -638,9 +710,15 @@ export class WindowService { this.setActiveWindow(windowInstance.id) e.preventDefault() + e.stopPropagation() }) document.addEventListener('mousemove', (e) => { + // 检查是否正在调整尺寸,如果是则不处理拖拽 + if (windowInstance.resizeState?.isResizing) { + return; + } + if (!isDragging || !windowInstance.element) return const deltaX = e.clientX - startX @@ -651,13 +729,15 @@ export class WindowService { windowInstance.element.style.left = `${newLeft}px` windowInstance.element.style.top = `${newTop}px` - windowInstance.element.style.transform = 'none' // 更新配置 windowInstance.config.x = newLeft windowInstance.config.y = newTop this.eventBus.notifyEvent('onMove', windowInstance.id, newLeft, newTop) + + // 发送窗体数据更新事件 + this.notifyWindowFormDataUpdate(windowInstance.id) }) document.addEventListener('mouseup', () => { @@ -665,6 +745,382 @@ export class WindowService { }) } + /** + * 添加窗体调整尺寸功能 + */ + private addResizeFunctionality(windowElement: HTMLElement, windowInstance: WindowInstance): void { + // 初始化调整尺寸状态 + windowInstance.resizeState = { + isResizing: false, + direction: 'none', + startX: 0, + startY: 0, + startWidth: 0, + startHeight: 0, + startXPosition: 0, + startYPosition: 0 + } + + // 创建8个调整尺寸的手柄 + const resizeHandles = this.createResizeHandles(windowElement) + + // 添加鼠标事件监听器 + resizeHandles.forEach(handle => { + this.addResizeHandleEvents(handle, windowElement, windowInstance) + }) + + // 添加窗口边缘检测 + windowElement.addEventListener('mousemove', (e) => { + if (!windowInstance.resizeState || windowInstance.resizeState.isResizing) return + this.updateCursorForResize(e, windowElement, windowInstance) + }) + + windowElement.addEventListener('mouseleave', () => { + if (!windowInstance.resizeState || windowInstance.resizeState.isResizing) return + windowElement.style.cursor = 'default' + }) + } + + /** + * 创建调整尺寸的手柄 + */ + private createResizeHandles(windowElement: HTMLElement): HTMLElement[] { + const handles: HTMLElement[] = [] + const directions: ResizeDirection[] = [ + 'topLeft', 'top', 'topRight', + 'right', 'bottomRight', 'bottom', + 'bottomLeft', 'left' + ] + + directions.forEach(direction => { + const handle = document.createElement('div') + handle.className = `resize-handle resize-handle-${direction}` + + // 设置手柄样式 + handle.style.position = 'absolute' + handle.style.zIndex = '1001' + + // 根据方向设置位置和光标 + switch (direction) { + case 'topLeft': + handle.style.top = '-6px' + handle.style.left = '-6px' + handle.style.cursor = 'nw-resize' + break + case 'top': + handle.style.top = '-4px' + handle.style.left = '6px' + handle.style.right = '6px' + handle.style.cursor = 'n-resize' + break + case 'topRight': + handle.style.top = '-6px' + handle.style.right = '-6px' + handle.style.cursor = 'ne-resize' + break + case 'right': + handle.style.top = '6px' + handle.style.bottom = '6px' + handle.style.right = '-4px' + handle.style.cursor = 'e-resize' + break + case 'bottomRight': + handle.style.bottom = '-6px' + handle.style.right = '-6px' + handle.style.cursor = 'se-resize' + break + case 'bottom': + handle.style.bottom = '-4px' + handle.style.left = '6px' + handle.style.right = '6px' + handle.style.cursor = 's-resize' + break + case 'bottomLeft': + handle.style.bottom = '-6px' + handle.style.left = '-6px' + handle.style.cursor = 'sw-resize' + break + case 'left': + handle.style.top = '6px' + handle.style.bottom = '6px' + handle.style.left = '-4px' + handle.style.cursor = 'w-resize' + break + } + + // 设置手柄尺寸 + if (direction === 'top' || direction === 'bottom') { + handle.style.height = '8px' + } else if (direction === 'left' || direction === 'right') { + handle.style.width = '8px' + } else { + // 对角方向的手柄需要更大的点击区域 + handle.style.width = '12px' + handle.style.height = '12px' + } + + windowElement.appendChild(handle) + handles.push(handle) + }) + + return handles + } + + /** + * 添加调整尺寸手柄的事件监听器 + */ + private addResizeHandleEvents( + handle: HTMLElement, + windowElement: HTMLElement, + windowInstance: WindowInstance + ): void { + const direction = handle.className.split(' ').find(cls => cls.startsWith('resize-handle-'))?.split('-')[2] as ResizeDirection + + handle.addEventListener('mousedown', (e) => { + if (!windowInstance.resizeState) return + + e.preventDefault() + e.stopPropagation() + + // 确保窗体位置是最新的 + if (windowInstance.element) { + const rect = windowInstance.element.getBoundingClientRect(); + // 如果使用了transform,需要转换为实际坐标 + if (windowInstance.element.style.transform && windowInstance.element.style.transform.includes('translate')) { + // 移除transform并设置实际的left/top值 + windowInstance.element.style.transform = 'none'; + windowInstance.config.x = rect.left; + windowInstance.config.y = rect.top; + windowInstance.element.style.left = `${rect.left}px`; + windowInstance.element.style.top = `${rect.top}px`; + } + } + + // 开始调整尺寸 + windowInstance.resizeState.isResizing = true + windowInstance.resizeState.direction = direction + windowInstance.resizeState.startX = e.clientX + windowInstance.resizeState.startY = e.clientY + windowInstance.resizeState.startWidth = windowInstance.config.width + windowInstance.resizeState.startHeight = windowInstance.config.height + windowInstance.resizeState.startXPosition = windowInstance.config.x || 0 + windowInstance.resizeState.startYPosition = windowInstance.config.y || 0 + + // 添加半透明遮罩效果 + windowElement.style.opacity = '0.8' + + // 触发开始调整尺寸事件 + this.eventBus.notifyEvent('onResizeStart', windowInstance.id) + + e.preventDefault() + e.stopPropagation() + }) + } + + /** + * 根据鼠标位置更新光标样式 + */ + private updateCursorForResize( + e: MouseEvent, + windowElement: HTMLElement, + windowInstance: WindowInstance + ): void { + if (!windowInstance.resizeState) return + + const rect = windowElement.getBoundingClientRect() + const x = e.clientX - rect.left + const y = e.clientY - rect.top + const edgeSize = 8 + + // 检查鼠标位置确定调整方向 + let direction: ResizeDirection = 'none' + + // 优先检查角落区域,使用更精确的检测 + if (x >= 0 && x < edgeSize && y >= 0 && y < edgeSize) { + direction = 'topLeft' + } else if (x > rect.width - edgeSize && x <= rect.width && y >= 0 && y < edgeSize) { + direction = 'topRight' + } else if (x >= 0 && x < edgeSize && y > rect.height - edgeSize && y <= rect.height) { + direction = 'bottomLeft' + } else if (x > rect.width - edgeSize && x <= rect.width && y > rect.height - edgeSize && y <= rect.height) { + direction = 'bottomRight' + } + // 然后检查边缘区域 + else if (x >= 0 && x < edgeSize && y >= edgeSize && y <= rect.height - edgeSize) { + direction = 'left' + } else if (x > rect.width - edgeSize && x <= rect.width && y >= edgeSize && y <= rect.height - edgeSize) { + direction = 'right' + } else if (y >= 0 && y < edgeSize && x >= edgeSize && x <= rect.width - edgeSize) { + direction = 'top' + } else if (y > rect.height - edgeSize && y <= rect.height && x >= edgeSize && x <= rect.width - edgeSize) { + direction = 'bottom' + } + + // 更新光标样式 + windowElement.style.cursor = direction === 'none' ? 'default' : `${direction.replace(/([A-Z])/g, '-$1').toLowerCase()}-resize` + } + + /** + * 设置全局调整尺寸事件监听器 + */ + private setupGlobalResizeEvents(): void { + document.addEventListener('mousemove', (e) => { + // 处理调整尺寸过程中的鼠标移动 + this.handleResizeMouseMove(e) + }) + + document.addEventListener('mouseup', () => { + // 处理调整尺寸结束 + this.handleResizeMouseUp() + }) + } + + /** + * 处理调整尺寸过程中的鼠标移动 + */ + private handleResizeMouseMove(e: MouseEvent): void { + // 找到正在调整尺寸的窗体 + const resizingWindow = Array.from(this.windows.values()).find( + window => window.resizeState?.isResizing + ) + + // 如果没有正在调整尺寸的窗体,直接返回 + if (!resizingWindow || !resizingWindow.resizeState || !resizingWindow.element) return + + const { + direction, + startX, + startY, + startWidth, + startHeight, + startXPosition, + startYPosition + } = resizingWindow.resizeState + + const deltaX = e.clientX - startX + const deltaY = e.clientY - startY + + let newWidth = startWidth + let newHeight = startHeight + let newX = startXPosition + let newY = startYPosition + + // 根据调整方向计算新尺寸和位置 + switch (direction) { + case 'topLeft': + newWidth = Math.max(200, startWidth - deltaX) + newHeight = Math.max(150, startHeight - deltaY) + newX = startXPosition + (startWidth - newWidth) + newY = startYPosition + (startHeight - newHeight) + break + case 'top': + newHeight = Math.max(150, startHeight - deltaY) + newY = startYPosition + (startHeight - newHeight) + break + case 'topRight': + newWidth = Math.max(200, startWidth + deltaX) + newHeight = Math.max(150, startHeight - deltaY) + newY = startYPosition + (startHeight - newHeight) + break + case 'right': + newWidth = Math.max(200, startWidth + deltaX) + break + case 'bottomRight': + newWidth = Math.max(200, startWidth + deltaX) + newHeight = Math.max(150, startHeight + deltaY) + break + case 'bottom': + newHeight = Math.max(150, startHeight + deltaY) + break + case 'bottomLeft': + newWidth = Math.max(200, startWidth - deltaX) + newHeight = Math.max(150, startHeight + deltaY) + newX = startXPosition + (startWidth - newWidth) + break + case 'left': + newWidth = Math.max(200, startWidth - deltaX) + newX = startXPosition + (startWidth - newWidth) + break + } + + // 应用尺寸限制 + newWidth = this.clampDimension(newWidth, resizingWindow.config.minWidth, resizingWindow.config.maxWidth) + newHeight = this.clampDimension(newHeight, resizingWindow.config.minHeight, resizingWindow.config.maxHeight) + + // 应用新尺寸和位置 + resizingWindow.config.width = newWidth + resizingWindow.config.height = newHeight + resizingWindow.config.x = newX + resizingWindow.config.y = newY + + if (resizingWindow.element) { + resizingWindow.element.style.width = `${newWidth}px` + resizingWindow.element.style.height = `${newHeight}px` + resizingWindow.element.style.left = `${newX}px` + resizingWindow.element.style.top = `${newY}px` + resizingWindow.element.style.transform = 'none' + } + + // 触发调整尺寸事件 + this.eventBus.notifyEvent('onResizing', resizingWindow.id, newWidth, newHeight) + + // 发送窗体数据更新事件 + this.notifyWindowFormDataUpdate(resizingWindow.id) + } + + /** + * 处理调整尺寸结束 + */ + private handleResizeMouseUp(): void { + // 找到正在调整尺寸的窗体 + const resizingWindow = Array.from(this.windows.values()).find( + window => window.resizeState?.isResizing + ) + + if (!resizingWindow || !resizingWindow.resizeState || !resizingWindow.element) return + + // 结束调整尺寸 + resizingWindow.resizeState.isResizing = false + + // 移除半透明遮罩效果 + resizingWindow.element.style.opacity = '1' + + // 触发调整尺寸结束事件 + this.eventBus.notifyEvent('onResizeEnd', resizingWindow.id) + + // 发送窗体数据更新事件 + this.notifyWindowFormDataUpdate(resizingWindow.id) + } + + /** + * 限制尺寸在最小值和最大值之间 + */ + private clampDimension(value: number, min: number = 0, max: number = Infinity): number { + return Math.max(min, Math.min(max, value)) + } + + /** + * 发送窗体数据更新事件 + */ + private notifyWindowFormDataUpdate(windowId: string): void { + const window = this.windows.get(windowId) + if (!window || !window.element) return + + // 获取窗体数据 + const rect = window.element.getBoundingClientRect() + const data = { + id: windowId, + state: window.state, + width: window.config.width, + height: window.config.height, + x: window.config.x !== undefined ? window.config.x : rect.left, + y: window.config.y !== undefined ? window.config.y : rect.top + } + + // 发送事件到事件总线 + this.eventBus.notifyEvent('onWindowFormDataUpdate', data) + } + /** * 加载应用 */ @@ -728,5 +1184,8 @@ export class WindowService { // 所有状态变化都应该触发事件,这是正常的系统行为 console.log(`[WindowService] 窗体状态变化: ${windowId} ${oldState} -> ${newState}`) this.eventBus.notifyEvent('onStateChange', windowId, newState, oldState) + + // 发送窗体数据更新事件 + this.notifyWindowFormDataUpdate(windowId) } -} +} \ No newline at end of file diff --git a/src/ui/types/WindowFormTypes.ts b/src/ui/types/WindowFormTypes.ts index 494c7f8..77c4673 100644 --- a/src/ui/types/WindowFormTypes.ts +++ b/src/ui/types/WindowFormTypes.ts @@ -8,3 +8,35 @@ export interface WindowFormPos { /** 窗口状态 */ export type TWindowFormState = 'default' | 'minimized' | 'maximized'; + +/** 拖拽方向 */ +export type ResizeDirection = + | 'topLeft' + | 'top' + | 'topRight' + | 'right' + | 'bottomRight' + | 'bottom' + | 'bottomLeft' + | 'left' + | 'none' + +/** 拖拽状态 */ +export interface ResizeState { + /** 是否正在调整尺寸 */ + isResizing: boolean + /** 调整方向 */ + direction: ResizeDirection + /** 起始鼠标位置 */ + startX: number + /** 起始鼠标位置 */ + startY: number + /** 起始窗体宽度 */ + startWidth: number + /** 起始窗体高度 */ + startHeight: number + /** 起始窗体X坐标 */ + startXPosition: number + /** 起始窗体Y坐标 */ + startYPosition: number +} \ No newline at end of file