Files
vue-desktop/src/services/WindowService.ts

1191 lines
33 KiB
TypeScript
Raw Normal View History

2025-09-24 16:43:10 +08:00
import { ref, reactive } from 'vue'
import type { IEventBuilder, IEventMap } from '@/events/IEventBuilder'
import { v4 as uuidv4 } from 'uuid'
2025-09-25 13:36:38 +08:00
import type { ResizeDirection, ResizeState } from '@/ui/types/WindowFormTypes'
2025-09-24 16:43:10 +08:00
/**
*
*/
export enum WindowState {
CREATING = 'creating',
LOADING = 'loading',
ACTIVE = 'active',
MINIMIZED = 'minimized',
MAXIMIZED = 'maximized',
CLOSING = 'closing',
DESTROYED = 'destroyed',
ERROR = 'error',
}
2025-09-25 12:48:29 +08:00
/**
*
*/
2025-09-24 16:43:10 +08:00
/**
*
*/
export interface WindowConfig {
2025-09-25 12:48:29 +08:00
/**
*
*/
2025-09-24 16:43:10 +08:00
title: string
2025-09-25 12:48:29 +08:00
/**
* ()
*/
2025-09-24 16:43:10 +08:00
width: number
2025-09-25 12:48:29 +08:00
/**
* ()
*/
2025-09-24 16:43:10 +08:00
height: number
2025-09-25 12:48:29 +08:00
/**
* ()
*/
2025-09-24 16:43:10 +08:00
minWidth?: number
2025-09-25 12:48:29 +08:00
/**
* ()
*/
2025-09-24 16:43:10 +08:00
minHeight?: number
2025-09-25 12:48:29 +08:00
/**
* ()
*/
2025-09-24 16:43:10 +08:00
maxWidth?: number
2025-09-25 12:48:29 +08:00
/**
* ()
*/
2025-09-24 16:43:10 +08:00
maxHeight?: number
2025-09-25 12:48:29 +08:00
/**
*
*/
2025-09-24 16:43:10 +08:00
resizable?: boolean
2025-09-25 12:48:29 +08:00
/**
*
*/
2025-09-24 16:43:10 +08:00
movable?: boolean
2025-09-25 12:48:29 +08:00
/**
*
*/
2025-09-24 16:43:10 +08:00
closable?: boolean
2025-09-25 12:48:29 +08:00
/**
*
*/
2025-09-24 16:43:10 +08:00
minimizable?: boolean
2025-09-25 12:48:29 +08:00
/**
*
*/
2025-09-24 16:43:10 +08:00
maximizable?: boolean
2025-09-25 12:48:29 +08:00
/**
*
*/
2025-09-24 16:43:10 +08:00
modal?: boolean
2025-09-25 12:48:29 +08:00
/**
*
*/
2025-09-24 16:43:10 +08:00
alwaysOnTop?: boolean
2025-09-25 12:48:29 +08:00
/**
* X坐标位置
*/
2025-09-24 16:43:10 +08:00
x?: number
2025-09-25 12:48:29 +08:00
/**
* Y坐标位置
*/
2025-09-24 16:43:10 +08:00
y?: number
}
2025-09-25 12:48:29 +08:00
/**
*
*/
2025-09-24 16:43:10 +08:00
/**
*
*/
export interface WindowInstance {
2025-09-25 12:48:29 +08:00
/**
*
*/
2025-09-24 16:43:10 +08:00
id: string
2025-09-25 12:48:29 +08:00
/**
*
*/
2025-09-24 16:43:10 +08:00
appId: string
2025-09-25 12:48:29 +08:00
/**
*
*/
2025-09-24 16:43:10 +08:00
config: WindowConfig
2025-09-25 12:48:29 +08:00
/**
*
*/
2025-09-24 16:43:10 +08:00
state: WindowState
2025-09-25 12:48:29 +08:00
/**
* DOM元素
*/
2025-09-24 16:43:10 +08:00
element?: HTMLElement
2025-09-25 12:48:29 +08:00
/**
* iframe元素
*/
2025-09-24 16:43:10 +08:00
iframe?: HTMLIFrameElement
2025-09-25 12:48:29 +08:00
/**
*
*/
2025-09-24 16:43:10 +08:00
zIndex: number
2025-09-25 12:48:29 +08:00
/**
*
*/
2025-09-24 16:43:10 +08:00
createdAt: Date
2025-09-25 12:48:29 +08:00
/**
*
*/
2025-09-24 16:43:10 +08:00
updatedAt: Date
2025-09-25 13:36:38 +08:00
/**
*
*/
resizeState?: ResizeState
2025-09-24 16:43:10 +08:00
}
/**
*
*/
export interface WindowEvents extends IEventMap {
onStateChange: (windowId: string, newState: WindowState, oldState: WindowState) => void
onResize: (windowId: string, width: number, height: number) => void
onMove: (windowId: string, x: number, y: number) => void
onFocus: (windowId: string) => void
onBlur: (windowId: string) => void
onClose: (windowId: string) => void
2025-09-25 13:36:38 +08:00
onResizeStart: (windowId: string) => void
onResizing: (windowId: string, width: number, height: number) => void
onResizeEnd: (windowId: string) => void
2025-09-24 16:43:10 +08:00
}
/**
*
*/
export class WindowService {
private windows = reactive(new Map<string, WindowInstance>())
private activeWindowId = ref<string | null>(null)
private nextZIndex = 1000
private eventBus: IEventBuilder<WindowEvents>
constructor(eventBus: IEventBuilder<WindowEvents>) {
this.eventBus = eventBus
2025-09-25 13:36:38 +08:00
this.setupGlobalResizeEvents()
2025-09-24 16:43:10 +08:00
}
/**
*
*/
async createWindow(appId: string, config: WindowConfig): Promise<WindowInstance> {
const windowId = uuidv4()
const now = new Date()
const windowInstance: WindowInstance = {
id: windowId,
appId,
config,
state: WindowState.CREATING,
zIndex: this.nextZIndex++,
createdAt: now,
updatedAt: now,
}
this.windows.set(windowId, windowInstance)
try {
// 创建窗体DOM元素
await this.createWindowElement(windowInstance)
// 更新状态为加载中
this.updateWindowState(windowId, WindowState.LOADING)
// 模拟应用加载过程
await this.loadApplication(windowInstance)
// 激活窗体
this.updateWindowState(windowId, WindowState.ACTIVE)
this.setActiveWindow(windowId)
return windowInstance
} catch (error) {
this.updateWindowState(windowId, WindowState.ERROR)
throw error
}
}
/**
*
*/
async destroyWindow(windowId: string): Promise<boolean> {
const window = this.windows.get(windowId)
if (!window) return false
try {
this.updateWindowState(windowId, WindowState.CLOSING)
// 清理DOM元素
if (window.element) {
window.element.remove()
}
// 从集合中移除
this.windows.delete(windowId)
// 更新活跃窗体
if (this.activeWindowId.value === windowId) {
this.activeWindowId.value = null
// 激活最后一个窗体
const lastWindow = Array.from(this.windows.values()).pop()
if (lastWindow) {
this.setActiveWindow(lastWindow.id)
}
}
this.eventBus.notifyEvent('onClose', windowId)
return true
} catch (error) {
console.error('销毁窗体失败:', error)
return false
}
}
/**
*
*/
minimizeWindow(windowId: string): boolean {
const window = this.windows.get(windowId)
if (!window || window.state === WindowState.MINIMIZED) return false
this.updateWindowState(windowId, WindowState.MINIMIZED)
if (window.element) {
window.element.style.display = 'none'
}
2025-09-25 13:36:38 +08:00
// 发送窗体数据更新事件
this.notifyWindowFormDataUpdate(windowId)
2025-09-24 16:43:10 +08:00
return true
}
/**
*
*/
maximizeWindow(windowId: string): boolean {
const window = this.windows.get(windowId)
if (!window || window.state === WindowState.MAXIMIZED) return false
const oldState = window.state
this.updateWindowState(windowId, WindowState.MAXIMIZED)
if (window.element) {
// 保存原始尺寸和位置
window.element.dataset.originalWidth = window.config.width.toString()
window.element.dataset.originalHeight = window.config.height.toString()
window.element.dataset.originalX = (window.config.x || 0).toString()
window.element.dataset.originalY = (window.config.y || 0).toString()
// 设置最大化样式
Object.assign(window.element.style, {
position: 'fixed',
top: '0',
left: '0',
width: '100vw',
height: 'calc(100vh - 40px)', // 减去任务栏高度
display: 'block',
2025-09-25 13:36:38 +08:00
transform: 'none' // 确保移除transform
2025-09-24 16:43:10 +08:00
})
}
this.setActiveWindow(windowId)
2025-09-25 13:36:38 +08:00
// 发送窗体数据更新事件
this.notifyWindowFormDataUpdate(windowId)
2025-09-24 16:43:10 +08:00
return true
}
/**
*
*/
restoreWindow(windowId: string): boolean {
const window = this.windows.get(windowId)
if (!window) return false
const targetState =
window.state === WindowState.MINIMIZED
? WindowState.ACTIVE
: window.state === WindowState.MAXIMIZED
? WindowState.ACTIVE
: window.state
this.updateWindowState(windowId, targetState)
if (window.element) {
if (window.state === WindowState.MINIMIZED) {
window.element.style.display = 'block'
} else if (window.state === WindowState.MAXIMIZED) {
// 恢复原始尺寸和位置
const originalWidth = window.element.dataset.originalWidth
const originalHeight = window.element.dataset.originalHeight
const originalX = window.element.dataset.originalX
const originalY = window.element.dataset.originalY
Object.assign(window.element.style, {
width: originalWidth ? `${originalWidth}px` : `${window.config.width}px`,
height: originalHeight ? `${originalHeight}px` : `${window.config.height}px`,
2025-09-25 13:36:38 +08:00
left: originalX ? `${originalX}px` : '0px',
top: originalY ? `${originalY}px` : '0px',
transform: 'none' // 确保移除transform
2025-09-24 16:43:10 +08:00
})
2025-09-25 13:36:38 +08:00
// 更新配置中的位置
if (originalX) window.config.x = parseFloat(originalX);
if (originalY) window.config.y = parseFloat(originalY);
2025-09-24 16:43:10 +08:00
}
}
this.setActiveWindow(windowId)
2025-09-25 13:36:38 +08:00
// 发送窗体数据更新事件
this.notifyWindowFormDataUpdate(windowId)
2025-09-24 16:43:10 +08:00
return true
}
/**
*
*/
setWindowTitle(windowId: string, title: string): boolean {
const window = this.windows.get(windowId)
if (!window) return false
window.config.title = title
window.updatedAt = new Date()
// 更新DOM元素标题
if (window.element) {
const titleElement = window.element.querySelector('.window-title')
if (titleElement) {
titleElement.textContent = title
}
}
2025-09-25 13:36:38 +08:00
// 发送窗体数据更新事件
this.notifyWindowFormDataUpdate(windowId)
2025-09-24 16:43:10 +08:00
return true
}
/**
*
*/
setWindowSize(windowId: string, width: number, height: number): boolean {
const window = this.windows.get(windowId)
if (!window) return false
// 检查尺寸限制
2025-09-25 13:36:38 +08:00
const finalWidth = this.clampDimension(width, window.config.minWidth, window.config.maxWidth)
const finalHeight = this.clampDimension(height, window.config.minHeight, window.config.maxHeight)
2025-09-24 16:43:10 +08:00
window.config.width = finalWidth
window.config.height = finalHeight
window.updatedAt = new Date()
if (window.element) {
window.element.style.width = `${finalWidth}px`
window.element.style.height = `${finalHeight}px`
}
this.eventBus.notifyEvent('onResize', windowId, finalWidth, finalHeight)
2025-09-25 13:36:38 +08:00
// 发送窗体数据更新事件
this.notifyWindowFormDataUpdate(windowId)
2025-09-24 16:43:10 +08:00
return true
}
/**
*
*/
getWindow(windowId: string): WindowInstance | undefined {
return this.windows.get(windowId)
}
/**
*
*/
getAllWindows(): WindowInstance[] {
return Array.from(this.windows.values())
}
/**
* ID
*/
getActiveWindowId(): string | null {
return this.activeWindowId.value
}
/**
*
*/
setActiveWindow(windowId: string): boolean {
const window = this.windows.get(windowId)
if (!window) return false
this.activeWindowId.value = windowId
window.zIndex = this.nextZIndex++
if (window.element) {
window.element.style.zIndex = window.zIndex.toString()
}
this.eventBus.notifyEvent('onFocus', windowId)
2025-09-25 13:36:38 +08:00
// 发送窗体数据更新事件
this.notifyWindowFormDataUpdate(windowId)
2025-09-24 16:43:10 +08:00
return true
}
/**
* DOM元素
*/
private async createWindowElement(windowInstance: WindowInstance): Promise<void> {
const { id, config, appId } = windowInstance
// 检查是否为内置应用
let isBuiltInApp = false
try {
const { AppRegistry } = await import('../apps/AppRegistry')
const appRegistry = AppRegistry.getInstance()
isBuiltInApp = appRegistry.hasApp(appId)
} catch (error) {
console.warn('无法导入 AppRegistry')
}
// 创建窗体容器
const windowElement = document.createElement('div')
windowElement.className = 'system-window'
windowElement.id = `window-${id}`
2025-09-25 13:36:38 +08:00
// 计算初始位置
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;
}
2025-09-24 16:43:10 +08:00
// 设置基本样式
Object.assign(windowElement.style, {
position: 'fixed',
width: `${config.width}px`,
height: `${config.height}px`,
2025-09-25 13:36:38 +08:00
left: `${left}px`,
top: `${top}px`,
2025-09-24 16:43:10 +08:00
zIndex: windowInstance.zIndex.toString(),
backgroundColor: '#fff',
border: '1px solid #ccc',
borderRadius: '8px',
boxShadow: '0 4px 20px rgba(0,0,0,0.15)',
overflow: 'hidden',
2025-09-25 13:36:38 +08:00
});
// 保存初始位置到配置中
windowInstance.config.x = left;
windowInstance.config.y = top;
2025-09-24 16:43:10 +08:00
// 创建窗体标题栏
const titleBar = this.createTitleBar(windowInstance)
windowElement.appendChild(titleBar)
// 创建窗体内容区域
const contentArea = document.createElement('div')
contentArea.className = 'window-content'
contentArea.style.cssText = `
width: 100%;
height: calc(100% - 40px);
overflow: hidden;
`
if (isBuiltInApp) {
// 内置应用:创建普通 div 容器AppRenderer 组件会在这里渲染内容
const appContainer = document.createElement('div')
appContainer.className = 'built-in-app-container'
appContainer.id = `app-container-${appId}`
appContainer.style.cssText = `
width: 100%;
height: 100%;
background: #fff;
`
contentArea.appendChild(appContainer)
console.log(`[WindowService] 为内置应用 ${appId} 创建了普通容器`)
} else {
// 外部应用:创建 iframe 容器
const iframe = document.createElement('iframe')
iframe.style.cssText = `
width: 100%;
height: 100%;
border: none;
background: #fff;
`
iframe.sandbox = 'allow-scripts allow-forms allow-popups' // 移除allow-same-origin以提高安全性
contentArea.appendChild(iframe)
// 保存 iframe 引用(仅对外部应用)
windowInstance.iframe = iframe
console.log(`[WindowService] 为外部应用 ${appId} 创建了 iframe 容器`)
}
windowElement.appendChild(contentArea)
2025-09-25 13:36:38 +08:00
// 添加拖拽调整尺寸功能
if (config.resizable !== false) {
this.addResizeFunctionality(windowElement, windowInstance)
}
2025-09-24 16:43:10 +08:00
// 添加到页面
document.body.appendChild(windowElement)
// 保存引用
windowInstance.element = windowElement
}
/**
*
*/
private createTitleBar(windowInstance: WindowInstance): HTMLElement {
const titleBar = document.createElement('div')
titleBar.className = 'window-title-bar'
titleBar.style.cssText = `
height: 40px;
background: linear-gradient(to bottom, #f8f9fa, #e9ecef);
border-bottom: 1px solid #dee2e6;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 12px;
cursor: move;
user-select: none;
`
// 窗体标题
const title = document.createElement('span')
title.className = 'window-title'
title.textContent = windowInstance.config.title
title.style.cssText = `
font-size: 14px;
font-weight: 500;
color: #333;
`
// 控制按钮组
const controls = document.createElement('div')
controls.className = 'window-controls'
controls.style.cssText = `
display: flex;
gap: 8px;
`
// 最小化按钮
if (windowInstance.config.minimizable !== false) {
const minimizeBtn = this.createControlButton('', () => {
this.minimizeWindow(windowInstance.id)
})
controls.appendChild(minimizeBtn)
}
// 最大化按钮
if (windowInstance.config.maximizable !== false) {
const maximizeBtn = this.createControlButton('□', () => {
if (windowInstance.state === WindowState.MAXIMIZED) {
this.restoreWindow(windowInstance.id)
} else {
this.maximizeWindow(windowInstance.id)
}
})
controls.appendChild(maximizeBtn)
}
// 关闭按钮
if (windowInstance.config.closable !== false) {
const closeBtn = this.createControlButton('×', () => {
this.destroyWindow(windowInstance.id)
})
closeBtn.style.color = '#dc3545'
controls.appendChild(closeBtn)
}
titleBar.appendChild(title)
titleBar.appendChild(controls)
// 添加拖拽功能
if (windowInstance.config.movable !== false) {
this.addDragFunctionality(titleBar, windowInstance)
}
return titleBar
}
/**
*
*/
private createControlButton(text: string, onClick: () => void): HTMLElement {
const button = document.createElement('button')
button.textContent = text
button.style.cssText = `
width: 24px;
height: 24px;
border: none;
background: transparent;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
font-size: 16px;
line-height: 1;
`
button.addEventListener('click', onClick)
// 添加悬停效果
button.addEventListener('mouseenter', () => {
button.style.backgroundColor = '#e9ecef'
})
button.addEventListener('mouseleave', () => {
button.style.backgroundColor = 'transparent'
})
return button
}
/**
*
*/
private addDragFunctionality(titleBar: HTMLElement, windowInstance: WindowInstance): void {
let isDragging = false
let startX = 0
let startY = 0
let startLeft = 0
let startTop = 0
titleBar.addEventListener('mousedown', (e) => {
2025-09-25 13:36:38 +08:00
// 检查是否正在调整尺寸,如果是则不处理拖拽
if (windowInstance.resizeState?.isResizing) {
return;
}
// 检查是否点击在调整尺寸手柄上,如果是则不处理拖拽
const target = e.target as HTMLElement;
if (target.classList.contains('resize-handle')) {
return;
}
2025-09-24 16:43:10 +08:00
if (!windowInstance.element) return
isDragging = true
startX = e.clientX
startY = e.clientY
const rect = windowInstance.element.getBoundingClientRect()
2025-09-25 13:36:38 +08:00
// 如果使用了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`;
}
2025-09-24 16:43:10 +08:00
startLeft = rect.left
startTop = rect.top
// 设置为活跃窗体
this.setActiveWindow(windowInstance.id)
e.preventDefault()
2025-09-25 13:36:38 +08:00
e.stopPropagation()
2025-09-24 16:43:10 +08:00
})
document.addEventListener('mousemove', (e) => {
2025-09-25 13:36:38 +08:00
// 检查是否正在调整尺寸,如果是则不处理拖拽
if (windowInstance.resizeState?.isResizing) {
return;
}
2025-09-24 16:43:10 +08:00
if (!isDragging || !windowInstance.element) return
const deltaX = e.clientX - startX
const deltaY = e.clientY - startY
const newLeft = startLeft + deltaX
const newTop = startTop + deltaY
windowInstance.element.style.left = `${newLeft}px`
windowInstance.element.style.top = `${newTop}px`
// 更新配置
windowInstance.config.x = newLeft
windowInstance.config.y = newTop
this.eventBus.notifyEvent('onMove', windowInstance.id, newLeft, newTop)
2025-09-25 13:36:38 +08:00
// 发送窗体数据更新事件
this.notifyWindowFormDataUpdate(windowInstance.id)
2025-09-24 16:43:10 +08:00
})
document.addEventListener('mouseup', () => {
isDragging = false
})
}
2025-09-25 13:36:38 +08:00
/**
*
*/
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)
}
2025-09-24 16:43:10 +08:00
/**
*
*/
private async loadApplication(windowInstance: WindowInstance): Promise<void> {
// 动态导入 AppRegistry 检查是否为内置应用
try {
const { AppRegistry } = await import('../apps/AppRegistry')
const appRegistry = AppRegistry.getInstance()
// 如果是内置应用,直接返回,不需要等待
if (appRegistry.hasApp(windowInstance.appId)) {
console.log(`[WindowService] 内置应用 ${windowInstance.appId} 无需等待加载`)
return Promise.resolve()
}
} catch (error) {
console.warn('无法导入 AppRegistry使用传统加载方式')
}
// 对于外部应用,保持原有的加载逻辑
return new Promise((resolve) => {
console.log(`[WindowService] 开始加载外部应用 ${windowInstance.appId}`)
setTimeout(() => {
if (windowInstance.iframe) {
// 这里可以设置 iframe 的 src 来加载具体应用
windowInstance.iframe.src = 'about:blank'
// 添加一些示例内容
const doc = windowInstance.iframe.contentDocument
if (doc) {
doc.body.innerHTML = `
<div style="padding: 20px; font-family: sans-serif;">
<h2>应用: ${windowInstance.config.title}</h2>
<p>应用ID: ${windowInstance.appId}</p>
<p>窗体ID: ${windowInstance.id}</p>
<p></p>
</div>
`
}
}
console.log(`[WindowService] 外部应用 ${windowInstance.appId} 加载完成`)
resolve()
}, 200) // 改为200ms即使是外部应用也不需要这么长的时间
})
}
/**
*
*/
private updateWindowState(windowId: string, newState: WindowState): void {
const window = this.windows.get(windowId)
if (!window) return
const oldState = window.state
// 只有状态真正发生变化时才触发事件
if (oldState === newState) return
window.state = newState
window.updatedAt = new Date()
// 所有状态变化都应该触发事件,这是正常的系统行为
console.log(`[WindowService] 窗体状态变化: ${windowId} ${oldState} -> ${newState}`)
this.eventBus.notifyEvent('onStateChange', windowId, newState, oldState)
2025-09-25 13:36:38 +08:00
// 发送窗体数据更新事件
this.notifyWindowFormDataUpdate(windowId)
2025-09-24 16:43:10 +08:00
}
2025-09-25 13:36:38 +08:00
}