This commit is contained in:
2025-09-24 16:43:10 +08:00
parent 12f46e6f8e
commit 9dbc054483
130 changed files with 16474 additions and 4660 deletions

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