This commit is contained in:
2025-09-25 13:36:38 +08:00
parent d042520b14
commit d18a3d5279
6 changed files with 931 additions and 39 deletions

View File

@@ -1,6 +1,7 @@
import { ref, reactive } from 'vue'
import type { IEventBuilder, IEventMap } from '@/events/IEventBuilder'
import { v4 as uuidv4 } from 'uuid'
import type { ResizeDirection, ResizeState } from '@/ui/types/WindowFormTypes'
/**
* 窗体状态枚举
@@ -132,6 +133,10 @@ export interface WindowInstance {
* 窗体更新时间
*/
updatedAt: Date
/**
* 拖拽调整尺寸状态
*/
resizeState?: ResizeState
}
/**
@@ -144,6 +149,9 @@ export interface WindowEvents extends IEventMap {
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
}
/**
@@ -157,6 +165,7 @@ export class WindowService {
constructor(eventBus: IEventBuilder<WindowEvents>) {
this.eventBus = eventBus
this.setupGlobalResizeEvents()
}
/**
@@ -248,6 +257,9 @@ export class WindowService {
window.element.style.display = 'none'
}
// 发送窗体数据更新事件
this.notifyWindowFormDataUpdate(windowId)
return true
}
@@ -276,10 +288,15 @@ export class WindowService {
width: '100vw',
height: 'calc(100vh - 40px)', // 减去任务栏高度
display: 'block',
transform: 'none' // 确保移除transform
})
}
this.setActiveWindow(windowId)
// 发送窗体数据更新事件
this.notifyWindowFormDataUpdate(windowId)
return true
}
@@ -312,14 +329,22 @@ export class WindowService {
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%)',
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
}
@@ -341,6 +366,9 @@ export class WindowService {
}
}
// 发送窗体数据更新事件
this.notifyWindowFormDataUpdate(windowId)
return true
}
@@ -352,14 +380,8 @@ export class WindowService {
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),
)
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
@@ -371,6 +393,10 @@ export class WindowService {
}
this.eventBus.notifyEvent('onResize', windowId, finalWidth, finalHeight)
// 发送窗体数据更新事件
this.notifyWindowFormDataUpdate(windowId)
return true
}
@@ -410,6 +436,10 @@ export class WindowService {
}
this.eventBus.notifyEvent('onFocus', windowId)
// 发送窗体数据更新事件
this.notifyWindowFormDataUpdate(windowId)
return true
}
@@ -434,21 +464,36 @@ export class WindowService {
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: '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%)',
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)
@@ -496,6 +541,11 @@ export class WindowService {
windowElement.appendChild(contentArea)
// 添加拖拽调整尺寸功能
if (config.resizable !== false) {
this.addResizeFunctionality(windowElement, windowInstance)
}
// 添加到页面
document.body.appendChild(windowElement)
@@ -624,6 +674,17 @@ export class WindowService {
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
@@ -631,6 +692,17 @@ export class WindowService {
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
@@ -638,9 +710,15 @@ export class WindowService {
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
@@ -651,13 +729,15 @@ export class WindowService {
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)
// 发送窗体数据更新事件
this.notifyWindowFormDataUpdate(windowInstance.id)
})
document.addEventListener('mouseup', () => {
@@ -665,6 +745,382 @@ export class WindowService {
})
}
/**
* 添加窗体调整尺寸功能
*/
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)
}
/**
* 加载应用
*/
@@ -728,5 +1184,8 @@ export class WindowService {
// 所有状态变化都应该触发事件,这是正常的系统行为
console.log(`[WindowService] 窗体状态变化: ${windowId} ${oldState} -> ${newState}`)
this.eventBus.notifyEvent('onStateChange', windowId, newState, oldState)
// 发送窗体数据更新事件
this.notifyWindowFormDataUpdate(windowId)
}
}
}