This commit is contained in:
2025-09-25 13:36:38 +08:00
parent d042520b14
commit d18a3d5279
6 changed files with 931 additions and 39 deletions

View File

@@ -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 用户体验测试
- 拖拽流畅性测试
- 视觉反馈效果测试
- 不同设备兼容性测试

View File

@@ -10,49 +10,64 @@ export interface IWindowFormEvent extends IEventMap {
* 窗口最小化 * 窗口最小化
* @param id 窗口id * @param id 窗口id
*/ */
windowFormMinimize: (id: string) => void; windowFormMinimize: (id: string) => void
/** /**
* 窗口最大化 * 窗口最大化
* @param id 窗口id * @param id 窗口id
*/ */
windowFormMaximize: (id: string) => void; windowFormMaximize: (id: string) => void
/** /**
* 窗口还原 * 窗口还原
* @param id 窗口id * @param id 窗口id
*/ */
windowFormRestore: (id: string) => void; windowFormRestore: (id: string) => void
/** /**
* 窗口关闭 * 窗口关闭
* @param id 窗口id * @param id 窗口id
*/ */
windowFormClose: (id: string) => void; windowFormClose: (id: string) => void
/** /**
* 窗口聚焦 * 窗口聚焦
* @param id 窗口id * @param id 窗口id
*/ */
windowFormFocus: (id: string) => void; windowFormFocus: (id: string) => void
/** /**
* 窗口数据更新 * 窗口数据更新
* @param data 窗口数据 * @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 */
id: string; id: string
/** 窗口状态 */ /** 窗口状态 */
state: TWindowFormState, state: TWindowFormState
/** 窗口宽度 */ /** 窗口宽度 */
width: number, width: number
/** 窗口高度 */ /** 窗口高度 */
height: number, height: number
/** 窗口x坐标(左上角) */ /** 窗口x坐标(左上角) */
x: number, x: number
/** 窗口y坐标(左上角) */ /** 窗口y坐标(左上角) */
y: number y: number
} }

View File

@@ -508,7 +508,10 @@ export class EventCommunicationService {
if (subscribers.length === 0) { if (subscribers.length === 0) {
message.status = MessageStatus.FAILED 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 return
} }

View File

@@ -9,6 +9,7 @@ import { EventCommunicationService } from './EventCommunicationService'
import { ApplicationSandboxEngine } from './ApplicationSandboxEngine' import { ApplicationSandboxEngine } from './ApplicationSandboxEngine'
import { ApplicationLifecycleManager } from './ApplicationLifecycleManager' import { ApplicationLifecycleManager } from './ApplicationLifecycleManager'
import { externalAppDiscovery } from './ExternalAppDiscovery' 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( this.eventBus.addEventListener(
'onResourceQuotaExceeded', 'onResourceQuotaExceeded',
@@ -567,8 +626,18 @@ export class SystemServiceIntegration {
return this.windowService.setWindowSize(windowId, data.width, data.height) return this.windowService.setWindowSize(windowId, data.width, data.height)
case 'move': 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': case 'minimize':
return this.windowService.minimizeWindow(windowId) return this.windowService.minimizeWindow(windowId)
@@ -583,14 +652,14 @@ export class SystemServiceIntegration {
return this.lifecycleManager.stopApp(appId) return this.lifecycleManager.stopApp(appId)
case 'getState': case 'getState':
const window = this.windowService.getWindow(windowId) const windowInfo = this.windowService.getWindow(windowId)
return window?.state return windowInfo?.state
case 'getSize': case 'getSize':
const windowInfo = this.windowService.getWindow(windowId) const windowData = this.windowService.getWindow(windowId)
return { return {
width: windowInfo?.config.width, width: windowData?.config.width,
height: windowInfo?.config.height, height: windowData?.config.height,
} }
default: default:
@@ -870,4 +939,4 @@ export class SystemServiceIntegration {
console.log(`[SystemService] ${message}`, data) console.log(`[SystemService] ${message}`, data)
} }
} }
} }

View File

@@ -1,6 +1,7 @@
import { ref, reactive } from 'vue' import { ref, reactive } from 'vue'
import type { IEventBuilder, IEventMap } from '@/events/IEventBuilder' import type { IEventBuilder, IEventMap } from '@/events/IEventBuilder'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import type { ResizeDirection, ResizeState } from '@/ui/types/WindowFormTypes'
/** /**
* 窗体状态枚举 * 窗体状态枚举
@@ -132,6 +133,10 @@ export interface WindowInstance {
* 窗体更新时间 * 窗体更新时间
*/ */
updatedAt: Date updatedAt: Date
/**
* 拖拽调整尺寸状态
*/
resizeState?: ResizeState
} }
/** /**
@@ -144,6 +149,9 @@ export interface WindowEvents extends IEventMap {
onFocus: (windowId: string) => void onFocus: (windowId: string) => void
onBlur: (windowId: string) => void onBlur: (windowId: string) => void
onClose: (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<WindowEvents>) { constructor(eventBus: IEventBuilder<WindowEvents>) {
this.eventBus = eventBus this.eventBus = eventBus
this.setupGlobalResizeEvents()
} }
/** /**
@@ -248,6 +257,9 @@ export class WindowService {
window.element.style.display = 'none' window.element.style.display = 'none'
} }
// 发送窗体数据更新事件
this.notifyWindowFormDataUpdate(windowId)
return true return true
} }
@@ -276,10 +288,15 @@ export class WindowService {
width: '100vw', width: '100vw',
height: 'calc(100vh - 40px)', // 减去任务栏高度 height: 'calc(100vh - 40px)', // 减去任务栏高度
display: 'block', display: 'block',
transform: 'none' // 确保移除transform
}) })
} }
this.setActiveWindow(windowId) this.setActiveWindow(windowId)
// 发送窗体数据更新事件
this.notifyWindowFormDataUpdate(windowId)
return true return true
} }
@@ -312,14 +329,22 @@ export class WindowService {
Object.assign(window.element.style, { Object.assign(window.element.style, {
width: originalWidth ? `${originalWidth}px` : `${window.config.width}px`, width: originalWidth ? `${originalWidth}px` : `${window.config.width}px`,
height: originalHeight ? `${originalHeight}px` : `${window.config.height}px`, height: originalHeight ? `${originalHeight}px` : `${window.config.height}px`,
left: originalX ? `${originalX}px` : '50%', left: originalX ? `${originalX}px` : '0px',
top: originalY ? `${originalY}px` : '50%', top: originalY ? `${originalY}px` : '0px',
transform: originalX && originalY ? 'none' : 'translate(-50%, -50%)', transform: 'none' // 确保移除transform
}) })
// 更新配置中的位置
if (originalX) window.config.x = parseFloat(originalX);
if (originalY) window.config.y = parseFloat(originalY);
} }
} }
this.setActiveWindow(windowId) this.setActiveWindow(windowId)
// 发送窗体数据更新事件
this.notifyWindowFormDataUpdate(windowId)
return true return true
} }
@@ -341,6 +366,9 @@ export class WindowService {
} }
} }
// 发送窗体数据更新事件
this.notifyWindowFormDataUpdate(windowId)
return true return true
} }
@@ -352,14 +380,8 @@ export class WindowService {
if (!window) return false if (!window) return false
// 检查尺寸限制 // 检查尺寸限制
const finalWidth = Math.max( const finalWidth = this.clampDimension(width, window.config.minWidth, window.config.maxWidth)
window.config.minWidth || 200, const finalHeight = this.clampDimension(height, window.config.minHeight, window.config.maxHeight)
Math.min(window.config.maxWidth || Infinity, width),
)
const finalHeight = Math.max(
window.config.minHeight || 150,
Math.min(window.config.maxHeight || Infinity, height),
)
window.config.width = finalWidth window.config.width = finalWidth
window.config.height = finalHeight window.config.height = finalHeight
@@ -371,6 +393,10 @@ export class WindowService {
} }
this.eventBus.notifyEvent('onResize', windowId, finalWidth, finalHeight) this.eventBus.notifyEvent('onResize', windowId, finalWidth, finalHeight)
// 发送窗体数据更新事件
this.notifyWindowFormDataUpdate(windowId)
return true return true
} }
@@ -410,6 +436,10 @@ export class WindowService {
} }
this.eventBus.notifyEvent('onFocus', windowId) this.eventBus.notifyEvent('onFocus', windowId)
// 发送窗体数据更新事件
this.notifyWindowFormDataUpdate(windowId)
return true return true
} }
@@ -434,21 +464,36 @@ export class WindowService {
windowElement.className = 'system-window' windowElement.className = 'system-window'
windowElement.id = `window-${id}` 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, { Object.assign(windowElement.style, {
position: 'fixed', position: 'fixed',
width: `${config.width}px`, width: `${config.width}px`,
height: `${config.height}px`, height: `${config.height}px`,
left: config.x ? `${config.x}px` : '50%', left: `${left}px`,
top: config.y ? `${config.y}px` : '50%', top: `${top}px`,
transform: config.x && config.y ? 'none' : 'translate(-50%, -50%)',
zIndex: windowInstance.zIndex.toString(), zIndex: windowInstance.zIndex.toString(),
backgroundColor: '#fff', backgroundColor: '#fff',
border: '1px solid #ccc', border: '1px solid #ccc',
borderRadius: '8px', borderRadius: '8px',
boxShadow: '0 4px 20px rgba(0,0,0,0.15)', boxShadow: '0 4px 20px rgba(0,0,0,0.15)',
overflow: 'hidden', overflow: 'hidden',
}) });
// 保存初始位置到配置中
windowInstance.config.x = left;
windowInstance.config.y = top;
// 创建窗体标题栏 // 创建窗体标题栏
const titleBar = this.createTitleBar(windowInstance) const titleBar = this.createTitleBar(windowInstance)
@@ -496,6 +541,11 @@ export class WindowService {
windowElement.appendChild(contentArea) windowElement.appendChild(contentArea)
// 添加拖拽调整尺寸功能
if (config.resizable !== false) {
this.addResizeFunctionality(windowElement, windowInstance)
}
// 添加到页面 // 添加到页面
document.body.appendChild(windowElement) document.body.appendChild(windowElement)
@@ -624,6 +674,17 @@ export class WindowService {
let startTop = 0 let startTop = 0
titleBar.addEventListener('mousedown', (e) => { 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 if (!windowInstance.element) return
isDragging = true isDragging = true
@@ -631,6 +692,17 @@ export class WindowService {
startY = e.clientY startY = e.clientY
const rect = windowInstance.element.getBoundingClientRect() 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 startLeft = rect.left
startTop = rect.top startTop = rect.top
@@ -638,9 +710,15 @@ export class WindowService {
this.setActiveWindow(windowInstance.id) this.setActiveWindow(windowInstance.id)
e.preventDefault() e.preventDefault()
e.stopPropagation()
}) })
document.addEventListener('mousemove', (e) => { document.addEventListener('mousemove', (e) => {
// 检查是否正在调整尺寸,如果是则不处理拖拽
if (windowInstance.resizeState?.isResizing) {
return;
}
if (!isDragging || !windowInstance.element) return if (!isDragging || !windowInstance.element) return
const deltaX = e.clientX - startX const deltaX = e.clientX - startX
@@ -651,13 +729,15 @@ export class WindowService {
windowInstance.element.style.left = `${newLeft}px` windowInstance.element.style.left = `${newLeft}px`
windowInstance.element.style.top = `${newTop}px` windowInstance.element.style.top = `${newTop}px`
windowInstance.element.style.transform = 'none'
// 更新配置 // 更新配置
windowInstance.config.x = newLeft windowInstance.config.x = newLeft
windowInstance.config.y = newTop windowInstance.config.y = newTop
this.eventBus.notifyEvent('onMove', windowInstance.id, newLeft, newTop) this.eventBus.notifyEvent('onMove', windowInstance.id, newLeft, newTop)
// 发送窗体数据更新事件
this.notifyWindowFormDataUpdate(windowInstance.id)
}) })
document.addEventListener('mouseup', () => { 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}`) console.log(`[WindowService] 窗体状态变化: ${windowId} ${oldState} -> ${newState}`)
this.eventBus.notifyEvent('onStateChange', windowId, newState, oldState) this.eventBus.notifyEvent('onStateChange', windowId, newState, oldState)
// 发送窗体数据更新事件
this.notifyWindowFormDataUpdate(windowId)
} }
} }

View File

@@ -8,3 +8,35 @@ export interface WindowFormPos {
/** 窗口状态 */ /** 窗口状态 */
export type TWindowFormState = 'default' | 'minimized' | 'maximized'; 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
}