1215 lines
33 KiB
TypeScript
1215 lines
33 KiB
TypeScript
import { reactive, ref } from 'vue'
|
||
import type { IEventBuilder, IEventMap } from '@/events/IEventBuilder'
|
||
import { v4 as uuidv4 } from 'uuid'
|
||
import type { ResizeDirection, ResizeState } from '@/ui/types/WindowFormTypes'
|
||
import { appRegistry } from '@/apps/AppRegistry'
|
||
|
||
/**
|
||
* 窗体状态枚举
|
||
*/
|
||
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坐标位置
|
||
*/
|
||
x?: number
|
||
/**
|
||
* 窗体Y坐标位置
|
||
*/
|
||
y?: number
|
||
}
|
||
|
||
/**
|
||
* 窗体实例接口
|
||
*/
|
||
export interface WindowInstance {
|
||
/**
|
||
* 窗体唯一标识符
|
||
*/
|
||
id: string
|
||
/**
|
||
* 关联应用标识符
|
||
*/
|
||
appId: string
|
||
/**
|
||
* 窗体配置信息
|
||
*/
|
||
config: WindowConfig
|
||
/**
|
||
* 窗体当前状态
|
||
*/
|
||
state: WindowState
|
||
/**
|
||
* 窗体DOM元素
|
||
*/
|
||
element?: HTMLElement
|
||
/**
|
||
* 窗体内嵌iframe元素
|
||
*/
|
||
iframe?: HTMLIFrameElement
|
||
/**
|
||
* 窗体层级索引
|
||
*/
|
||
zIndex: number
|
||
/**
|
||
* 窗体创建时间
|
||
*/
|
||
createdAt: Date
|
||
/**
|
||
* 窗体更新时间
|
||
*/
|
||
updatedAt: Date
|
||
/**
|
||
* 拖拽调整尺寸状态
|
||
*/
|
||
resizeState?: ResizeState
|
||
}
|
||
|
||
/**
|
||
* 窗体事件接口
|
||
*/
|
||
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
|
||
onResizeStart: (windowId: string) => void
|
||
onResizing: (windowId: string, width: number, height: number) => void
|
||
onResizeEnd: (windowId: string) => void
|
||
}
|
||
|
||
/**
|
||
* 窗体管理服务类
|
||
*/
|
||
export class WindowFormService {
|
||
private windowsForm = 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
|
||
this.setupGlobalResizeEvents()
|
||
}
|
||
|
||
/**
|
||
* 创建新窗体
|
||
*/
|
||
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.windowsForm.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.windowsForm.get(windowId)
|
||
if (!window) return false
|
||
|
||
try {
|
||
this.updateWindowState(windowId, WindowState.CLOSING)
|
||
|
||
// 清理DOM元素
|
||
if (window.element) {
|
||
window.element.remove()
|
||
}
|
||
|
||
// 从集合中移除
|
||
this.windowsForm.delete(windowId)
|
||
|
||
// 更新活跃窗体
|
||
if (this.activeWindowId.value === windowId) {
|
||
this.activeWindowId.value = null
|
||
// 激活最后一个窗体
|
||
const lastWindow = Array.from(this.windowsForm.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.windowsForm.get(windowId)
|
||
if (!window || window.state === WindowState.MINIMIZED) return false
|
||
|
||
this.updateWindowState(windowId, WindowState.MINIMIZED)
|
||
|
||
if (window.element) {
|
||
window.element.style.display = 'none'
|
||
}
|
||
|
||
// 发送窗体数据更新事件
|
||
this.notifyWindowFormDataUpdate(windowId)
|
||
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* 最大化窗体
|
||
*/
|
||
maximizeWindow(windowId: string): boolean {
|
||
const window = this.windowsForm.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',
|
||
transform: 'none' // 确保移除transform
|
||
})
|
||
}
|
||
|
||
this.setActiveWindow(windowId)
|
||
|
||
// 发送窗体数据更新事件
|
||
this.notifyWindowFormDataUpdate(windowId)
|
||
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* 还原窗体
|
||
*/
|
||
restoreWindow(windowId: string): boolean {
|
||
const window = this.windowsForm.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` : '0px',
|
||
top: originalY ? `${originalY}px` : '0px',
|
||
transform: 'none' // 确保移除transform
|
||
})
|
||
|
||
// 更新配置中的位置
|
||
if (originalX) window.config.x = parseFloat(originalX)
|
||
if (originalY) window.config.y = parseFloat(originalY)
|
||
}
|
||
}
|
||
|
||
this.setActiveWindow(windowId)
|
||
|
||
// 发送窗体数据更新事件
|
||
this.notifyWindowFormDataUpdate(windowId)
|
||
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* 设置窗体标题
|
||
*/
|
||
setWindowTitle(windowId: string, title: string): boolean {
|
||
const window = this.windowsForm.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
|
||
}
|
||
}
|
||
|
||
// 发送窗体数据更新事件
|
||
this.notifyWindowFormDataUpdate(windowId)
|
||
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* 设置窗体尺寸
|
||
*/
|
||
setWindowSize(windowId: string, width: number, height: number): boolean {
|
||
const window = this.windowsForm.get(windowId)
|
||
if (!window) return false
|
||
|
||
// 检查尺寸限制
|
||
const finalWidth = this.clampDimension(width, window.config.minWidth, window.config.maxWidth)
|
||
const finalHeight = this.clampDimension(
|
||
height,
|
||
window.config.minHeight,
|
||
window.config.maxHeight
|
||
)
|
||
|
||
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)
|
||
|
||
// 发送窗体数据更新事件
|
||
this.notifyWindowFormDataUpdate(windowId)
|
||
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* 获取窗体实例
|
||
*/
|
||
getWindow(windowId: string): WindowInstance | undefined {
|
||
return this.windowsForm.get(windowId)
|
||
}
|
||
|
||
/**
|
||
* 获取所有窗体
|
||
*/
|
||
getAllWindows(): WindowInstance[] {
|
||
return Array.from(this.windowsForm.values())
|
||
}
|
||
|
||
/**
|
||
* 获取活跃窗体ID
|
||
*/
|
||
getActiveWindowId(): string | null {
|
||
return this.activeWindowId.value
|
||
}
|
||
|
||
/**
|
||
* 设置活跃窗体
|
||
*/
|
||
setActiveWindow(windowId: string): boolean {
|
||
const window = this.windowsForm.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)
|
||
|
||
// 发送窗体数据更新事件
|
||
this.notifyWindowFormDataUpdate(windowId)
|
||
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* 创建窗体DOM元素
|
||
*/
|
||
private async createWindowElement(windowInstance: WindowInstance): Promise<void> {
|
||
const { id, config, appId } = windowInstance
|
||
|
||
// 创建窗体容器
|
||
const windowElement = document.createElement('div')
|
||
windowElement.className = 'system-window'
|
||
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, {
|
||
position: 'absolute',
|
||
width: `${config.width}px`,
|
||
height: `${config.height}px`,
|
||
left: `${left}px`,
|
||
top: `${top}px`,
|
||
zIndex: windowInstance.zIndex.toString(),
|
||
backgroundColor: '#fff',
|
||
border: '1px solid #ccc',
|
||
borderRadius: '8px',
|
||
boxShadow: '0 4px 20px rgba(0,0,0,0.15)',
|
||
overflow: 'hidden'
|
||
})
|
||
|
||
// 保存初始位置到配置中
|
||
windowInstance.config.x = left
|
||
windowInstance.config.y = top
|
||
|
||
// 创建窗体标题栏
|
||
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 (appRegistry.hasApp(appId)) {
|
||
// 内置应用:创建普通 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)
|
||
|
||
// 添加拖拽调整尺寸功能
|
||
if (config.resizable !== false) {
|
||
this.addResizeFunctionality(windowElement, windowInstance)
|
||
}
|
||
|
||
// 添加到页面
|
||
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.resizeState?.isResizing) {
|
||
return
|
||
}
|
||
|
||
// 检查是否点击在调整尺寸手柄上,如果是则不处理拖拽
|
||
const target = e.target as HTMLElement
|
||
if (target.classList.contains('resize-handle')) {
|
||
return
|
||
}
|
||
|
||
if (!windowInstance.element) return
|
||
|
||
isDragging = true
|
||
startX = e.clientX
|
||
startY = e.clientY
|
||
|
||
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
|
||
startTop = rect.top
|
||
|
||
// 设置为活跃窗体
|
||
this.setActiveWindow(windowInstance.id)
|
||
|
||
e.preventDefault()
|
||
e.stopPropagation()
|
||
})
|
||
|
||
document.addEventListener('mousemove', (e) => {
|
||
// 检查是否正在调整尺寸,如果是则不处理拖拽
|
||
if (windowInstance.resizeState?.isResizing) {
|
||
return
|
||
}
|
||
|
||
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)
|
||
|
||
// 发送窗体数据更新事件
|
||
this.notifyWindowFormDataUpdate(windowInstance.id)
|
||
})
|
||
|
||
document.addEventListener('mouseup', () => {
|
||
isDragging = false
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 添加窗体调整尺寸功能
|
||
*/
|
||
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.windowsForm.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.windowsForm.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.windowsForm.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)
|
||
}
|
||
|
||
/**
|
||
* 加载应用
|
||
*/
|
||
private async loadApplication(windowInstance: WindowInstance): Promise<void> {
|
||
// 动态导入 AppRegistry 检查是否为内置应用
|
||
try {
|
||
// 如果是内置应用,直接返回,不需要等待
|
||
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)
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 更新窗体状态
|
||
*/
|
||
private updateWindowState(windowId: string, newState: WindowState): void {
|
||
const window = this.windowsForm.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)
|
||
|
||
// 发送窗体数据更新事件
|
||
this.notifyWindowFormDataUpdate(windowId)
|
||
}
|
||
}
|