保存
This commit is contained in:
651
src/services/WindowService.ts
Normal file
651
src/services/WindowService.ts
Normal file
@@ -0,0 +1,651 @@
|
||||
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<string, WindowInstance>())
|
||||
private activeWindowId = ref<string | null>(null)
|
||||
private nextZIndex = 1000
|
||||
private eventBus: IEventBuilder<WindowEvents>
|
||||
|
||||
constructor(eventBus: IEventBuilder<WindowEvents>) {
|
||||
this.eventBus = eventBus
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新窗体
|
||||
*/
|
||||
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'
|
||||
}
|
||||
|
||||
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<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}`
|
||||
|
||||
// 设置基本样式
|
||||
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<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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user