import { ref, reactive } from 'vue' import type { IEventBuilder, IEventMap } from '@/events/IEventBuilder' import { v4 as uuidv4 } from 'uuid' /** * 窗体状态枚举 */ export enum WindowState { CREATING = 'creating', LOADING = 'loading', ACTIVE = 'active', MINIMIZED = 'minimized', MAXIMIZED = 'maximized', CLOSING = 'closing', DESTROYED = 'destroyed', ERROR = 'error', } /** * 窗体配置接口 */ export interface WindowConfig { title: string width: number height: number minWidth?: number minHeight?: number maxWidth?: number maxHeight?: number resizable?: boolean movable?: boolean closable?: boolean minimizable?: boolean maximizable?: boolean modal?: boolean alwaysOnTop?: boolean x?: number y?: number } /** * 窗体实例接口 */ export interface WindowInstance { id: string appId: string config: WindowConfig state: WindowState element?: HTMLElement iframe?: HTMLIFrameElement zIndex: number createdAt: Date updatedAt: Date } /** * 窗体事件接口 */ 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 } /** * 窗体管理服务类 */ export class WindowService { private windows = reactive(new Map()) private activeWindowId = ref(null) private nextZIndex = 1000 private eventBus: IEventBuilder constructor(eventBus: IEventBuilder) { this.eventBus = eventBus } /** * 创建新窗体 */ async createWindow(appId: string, config: WindowConfig): Promise { 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 { 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' } 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', }) } this.setActiveWindow(windowId) 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`, left: originalX ? `${originalX}px` : '50%', top: originalY ? `${originalY}px` : '50%', transform: originalX && originalY ? 'none' : 'translate(-50%, -50%)', }) } } this.setActiveWindow(windowId) 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 } } return true } /** * 设置窗体尺寸 */ setWindowSize(windowId: string, width: number, height: number): boolean { const window = this.windows.get(windowId) 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), ) 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) 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) return true } /** * 创建窗体DOM元素 */ private async createWindowElement(windowInstance: WindowInstance): Promise { 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}` // 设置基本样式 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%)', zIndex: windowInstance.zIndex.toString(), backgroundColor: '#fff', border: '1px solid #ccc', borderRadius: '8px', boxShadow: '0 4px 20px rgba(0,0,0,0.15)', overflow: 'hidden', }) // 创建窗体标题栏 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) // 添加到页面 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) => { if (!windowInstance.element) return isDragging = true startX = e.clientX startY = e.clientY const rect = windowInstance.element.getBoundingClientRect() startLeft = rect.left startTop = rect.top // 设置为活跃窗体 this.setActiveWindow(windowInstance.id) e.preventDefault() }) document.addEventListener('mousemove', (e) => { 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.element.style.transform = 'none' // 更新配置 windowInstance.config.x = newLeft windowInstance.config.y = newTop this.eventBus.notifyEvent('onMove', windowInstance.id, newLeft, newTop) }) document.addEventListener('mouseup', () => { isDragging = false }) } /** * 加载应用 */ private async loadApplication(windowInstance: WindowInstance): Promise { // 动态导入 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 = `

应用: ${windowInstance.config.title}

应用ID: ${windowInstance.appId}

窗体ID: ${windowInstance.id}

这是一个示例应用内容。

` } } 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) } }