From dc25d283d84187b6edcb56989f4b1769fbb8c1cb Mon Sep 17 00:00:00 2001 From: Azure <983547216@qq.com> Date: Wed, 10 Sep 2025 15:44:10 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=9D=E5=AD=98=E4=B8=80=E4=B8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 45 ++ src/core/utils/DraggableResizable.ts | 559 --------------------- src/core/utils/DraggableResizableWindow.ts | 73 ++- src/core/window/css/window-form.scss | 47 ++ src/core/window/impl/WindowFormImpl.ts | 89 ++-- tsconfig.app.json | 4 +- vite.config.ts | 8 +- 8 files changed, 212 insertions(+), 614 deletions(-) delete mode 100644 src/core/utils/DraggableResizable.ts create mode 100644 src/core/window/css/window-form.scss diff --git a/package.json b/package.json index a751a36..acba4d0 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ }, "dependencies": { "@vueuse/core": "^13.6.0", + "lit": "^3.3.1", "lodash": "^4.17.21", "pinia": "^3.0.3", "uuid": "^11.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index efaee6b..9eaf67d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@vueuse/core': specifier: ^13.6.0 version: 13.6.0(vue@3.5.18(typescript@5.8.3)) + lit: + specifier: ^3.3.1 + version: 3.3.1 lodash: specifier: ^4.17.21 version: 4.17.21 @@ -413,6 +416,12 @@ packages: '@juggle/resize-observer@3.4.0': resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} + '@lit-labs/ssr-dom-shim@1.4.0': + resolution: {integrity: sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==} + + '@lit/reactive-element@2.1.1': + resolution: {integrity: sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==} + '@parcel/watcher-android-arm64@2.5.1': resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} engines: {node: '>= 10.0.0'} @@ -649,6 +658,9 @@ packages: '@types/node@22.17.1': resolution: {integrity: sha512-y3tBaz+rjspDTylNjAX37jEC3TETEFGNJL6uQDxwF9/8GLLIjW1rvVHlynyuUKMnMr1Roq8jOv3vkopBjC4/VA==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/web-bluetooth@0.0.21': resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} @@ -1170,6 +1182,15 @@ packages: kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + lit-element@4.2.1: + resolution: {integrity: sha512-WGAWRGzirAgyphK2urmYOV72tlvnxw7YfyLDgQ+OZnM9vQQBQnumQ7jUJe6unEzwGU3ahFOjuz1iz1jjrpCPuw==} + + lit-html@3.3.1: + resolution: {integrity: sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA==} + + lit@3.3.1: + resolution: {integrity: sha512-Ksr/8L3PTapbdXJCk+EJVB78jDodUMaP54gD24W186zGRARvwrsPfS60wae/SSCTCNZVPd1chXqio1qHQmu4NA==} + local-pkg@1.1.1: resolution: {integrity: sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==} engines: {node: '>=14'} @@ -1920,6 +1941,12 @@ snapshots: '@juggle/resize-observer@3.4.0': {} + '@lit-labs/ssr-dom-shim@1.4.0': {} + + '@lit/reactive-element@2.1.1': + dependencies: + '@lit-labs/ssr-dom-shim': 1.4.0 + '@parcel/watcher-android-arm64@2.5.1': optional: true @@ -2071,6 +2098,8 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/trusted-types@2.0.7': {} + '@types/web-bluetooth@0.0.21': {} '@unocss/astro@66.4.2(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))': @@ -2679,6 +2708,22 @@ snapshots: kolorist@1.8.0: {} + lit-element@4.2.1: + dependencies: + '@lit-labs/ssr-dom-shim': 1.4.0 + '@lit/reactive-element': 2.1.1 + lit-html: 3.3.1 + + lit-html@3.3.1: + dependencies: + '@types/trusted-types': 2.0.7 + + lit@3.3.1: + dependencies: + '@lit/reactive-element': 2.1.1 + lit-element: 4.2.1 + lit-html: 3.3.1 + local-pkg@1.1.1: dependencies: mlly: 1.7.4 diff --git a/src/core/utils/DraggableResizable.ts b/src/core/utils/DraggableResizable.ts deleted file mode 100644 index d7ba518..0000000 --- a/src/core/utils/DraggableResizable.ts +++ /dev/null @@ -1,559 +0,0 @@ -/** 拖拽移动开始的回调 */ -type TDragStartCallback = (x: number, y: number) => void; -/** 拖拽移动中的回调 */ -type TDragMoveCallback = (x: number, y: number) => void; -/** 拖拽移动结束的回调 */ -type TDragEndCallback = (x: number, y: number) => void; - -/** 拖拽调整尺寸的方向 */ -type TResizeDirection = - | 'top' - | 'bottom' - | 'left' - | 'right' - | 'top-left' - | 'top-right' - | 'bottom-left' - | 'bottom-right'; - -/** 拖拽调整尺寸回调数据 */ -interface IResizeCallbackData { - /** 宽度 */ - width: number; - /** 高度 */ - height: number; - /** 顶点坐标(相对 offsetParent) */ - top: number; - /** 左点坐标(相对 offsetParent) */ - left: number; - /** 拖拽调整尺寸的方向 */ - direction: TResizeDirection; -} - -/** 拖拽/调整尺寸 参数 */ -interface IDraggableResizableOptions { - /** 拖拽/调整尺寸目标元素 */ - target: HTMLElement; - /** 拖拽句柄 */ - handle?: HTMLElement; - /** 拖拽模式 */ - mode?: 'transform' | 'position'; - /** 拖拽边界或容器元素 */ - boundary?: IBoundaryRect | HTMLElement; - /** 移动步进(网格吸附) */ - snapGrid?: number; - /** 关键点吸附阈值 */ - snapThreshold?: number; - /** 是否开启吸附动画 */ - snapAnimation?: boolean; - /** 拖拽结束吸附动画时长 */ - snapAnimationDuration?: number; - /** 是否允许超出边界 */ - allowOverflow?: boolean; - - /** 拖拽开始回调 */ - onDragStart?: TDragStartCallback; - /** 拖拽移动中的回调 */ - onDragMove?: TDragMoveCallback; - /** 拖拽结束回调 */ - onDragEnd?: TDragEndCallback; - - /** 调整尺寸的最小宽度 */ - minWidth?: number; - /** 调整尺寸的最小高度 */ - minHeight?: number; - /** 调整尺寸的最大宽度 */ - maxWidth?: number; - /** 调整尺寸的最大高度 */ - maxHeight?: number; - - /** 拖拽调整尺寸中的回调 */ - onResizeMove?: (data: IResizeCallbackData) => void; - /** 拖拽调整尺寸结束回调 */ - onResizeEnd?: (data: IResizeCallbackData) => void; -} - -/** 拖拽的范围边界 */ -interface IBoundaryRect { - /** 最小 X 坐标 */ - minX?: number; - /** 最大 X 坐标 */ - maxX?: number; - /** 最小 Y 坐标 */ - minY?: number; - /** 最大 Y 坐标 */ - maxY?: number; -} - -/** - * 拖拽 + 调整尺寸通用类 - */ -export class DraggableResizable { - private handle?: HTMLElement; - private target: HTMLElement; - private boundary?: HTMLElement | IBoundaryRect; - private mode: 'transform' | 'position'; - private snapGrid: number; - private snapThreshold: number; - private snapAnimation: boolean; - private snapAnimationDuration: number; - private allowOverflow: boolean; - - private onDragStart?: TDragStartCallback; - private onDragMove?: TDragMoveCallback; - private onDragEnd?: TDragEndCallback; - - private isDragging = false; - private startX = 0; - private startY = 0; - private offsetX = 0; - private offsetY = 0; - private currentX = 0; - private currentY = 0; - - private containerRect?: DOMRect; - private resizeObserver?: ResizeObserver; - private mutationObserver: MutationObserver; - private animationFrame?: number; - - private currentDirection: TResizeDirection | null = null; - private startWidth = 0; - private startHeight = 0; - private startTop = 0; - private startLeft = 0; - private minWidth: number; - private minHeight: number; - private maxWidth: number; - private maxHeight: number; - private onResizeMove?: (data: IResizeCallbackData) => void; - private onResizeEnd?: (data: IResizeCallbackData) => void; - - constructor(options: IDraggableResizableOptions) { - // Drag - this.handle = options.handle; - this.target = options.target; - this.boundary = options.boundary; - this.mode = options.mode ?? 'transform'; - this.snapGrid = options.snapGrid ?? 1; - this.snapThreshold = options.snapThreshold ?? 0; - this.snapAnimation = options.snapAnimation ?? false; - this.snapAnimationDuration = options.snapAnimationDuration ?? 200; - this.allowOverflow = options.allowOverflow ?? true; - - this.onDragStart = options.onDragStart; - this.onDragMove = options.onDragMove; - this.onDragEnd = options.onDragEnd; - - // Resize - this.minWidth = options.minWidth ?? 100; - this.minHeight = options.minHeight ?? 50; - this.maxWidth = options.maxWidth ?? window.innerWidth; - this.maxHeight = options.maxHeight ?? window.innerHeight; - this.onResizeMove = options.onResizeMove; - this.onResizeEnd = options.onResizeEnd; - - this.init(); - } - - /** 初始化事件 */ - private init() { - if (this.handle) { - this.handle.addEventListener('mousedown', this.onMouseDownDrag); - } - - this.target.addEventListener('mousedown', this.onMouseDownResize); - this.target.addEventListener('mouseleave', this.onMouseLeave); - document.addEventListener('mousemove', this.onDocumentMouseMoveCursor); - - if (this.boundary instanceof HTMLElement) { - this.observeResize(this.boundary); - } - - // 监听目标 DOM 是否被移除,自动销毁 - this.mutationObserver = new MutationObserver((mutations) => { - for (const mutation of mutations) { - mutation.removedNodes.forEach((node) => { - if (node === this.target) { - this.destroy(); - } - }); - } - }); - if (this.target.parentElement) { - this.mutationObserver.observe(this.target.parentElement, { childList: true }); - } - } - - private onMouseDownDrag = (e: MouseEvent) => { - if (this.getResizeDirection(e)) return; // 避免和 resize 冲突 - e.preventDefault(); - this.startDrag(e); - }; - - private startDrag(e: MouseEvent) { - this.isDragging = true; - this.startX = e.clientX; - this.startY = e.clientY; - - if (this.mode === 'position') { - const rect = this.target.getBoundingClientRect(); - const parentRect = this.target.offsetParent?.getBoundingClientRect() ?? { left: 0, top: 0 }; - this.offsetX = rect.left - parentRect.left; - this.offsetY = rect.top - parentRect.top; - } else { - this.offsetX = this.currentX; - this.offsetY = this.currentY; - } - - document.addEventListener('mousemove', this.onMouseMoveDrag); - document.addEventListener('mouseup', this.onMouseUpDrag); - - this.onDragStart?.(this.offsetX, this.offsetY); - } - - private onMouseMoveDrag = (e: MouseEvent) => { - if (!this.isDragging) return; - - const dx = e.clientX - this.startX; - const dy = e.clientY - this.startY; - - let newX = this.offsetX + dx; - let newY = this.offsetY + dy; - - if (this.snapGrid > 1) { - newX = Math.round(newX / this.snapGrid) * this.snapGrid; - newY = Math.round(newY / this.snapGrid) * this.snapGrid; - } - - this.applyPosition(newX, newY, false); - this.onDragMove?.(newX, newY); - }; - - private onMouseUpDrag = () => { - if (!this.isDragging) return; - this.isDragging = false; - - const snapped = this.applySnapping(this.currentX, this.currentY); - - if (this.snapAnimation) { - this.animateTo(snapped.x, snapped.y, this.snapAnimationDuration, () => { - this.onDragEnd?.(snapped.x, snapped.y); - }); - } else { - this.applyPosition(snapped.x, snapped.y, true); - this.onDragEnd?.(snapped.x, snapped.y); - } - - document.removeEventListener('mousemove', this.onMouseMoveDrag); - document.removeEventListener('mouseup', this.onMouseUpDrag); - }; - - private applyPosition(x: number, y: number, isFinal: boolean) { - this.currentX = x; - this.currentY = y; - - if (this.mode === 'position') { - this.target.style.left = `${x}px`; - this.target.style.top = `${y}px`; - } else { - this.target.style.transform = `translate(${x}px, ${y}px)`; - } - - if (isFinal) this.applyBoundary(); - } - - private onMouseDownResize = (e: MouseEvent) => { - const dir = this.getResizeDirection(e); - if (!dir) return; - e.preventDefault(); - this.startResize(e, dir); - }; - - private startResize(e: MouseEvent, dir: TResizeDirection) { - this.currentDirection = dir; - const rect = this.target.getBoundingClientRect(); - const parentRect = this.target.offsetParent?.getBoundingClientRect() ?? { left: 0, top: 0 }; - - this.startX = e.clientX; - this.startY = e.clientY; - this.startWidth = rect.width; - this.startHeight = rect.height; - this.startTop = rect.top - parentRect.top; - this.startLeft = rect.left - parentRect.left; - - document.addEventListener('mousemove', this.onResizeDrag); - document.addEventListener('mouseup', this.onResizeEndHandler); - } - - private onResizeDrag = (e: MouseEvent) => { - if (!this.currentDirection) return; - - let deltaX = e.clientX - this.startX; - let deltaY = e.clientY - this.startY; - - let newWidth = this.startWidth; - let newHeight = this.startHeight; - let newTop = this.startTop; - let newLeft = this.startLeft; - - switch (this.currentDirection) { - case 'right': - newWidth = this.startWidth + deltaX; - break; - case 'bottom': - newHeight = this.startHeight + deltaY; - break; - case 'bottom-right': - newWidth = this.startWidth + deltaX; - newHeight = this.startHeight + deltaY; - break; - case 'left': - newWidth = this.startWidth - deltaX; - newLeft = this.startLeft + deltaX; - break; - case 'top': - newHeight = this.startHeight - deltaY; - newTop = this.startTop + deltaY; - break; - case 'top-left': - newWidth = this.startWidth - deltaX; - newLeft = this.startLeft + deltaX; - newHeight = this.startHeight - deltaY; - newTop = this.startTop + deltaY; - break; - case 'top-right': - newWidth = this.startWidth + deltaX; - newHeight = this.startHeight - deltaY; - newTop = this.startTop + deltaY; - break; - case 'bottom-left': - newWidth = this.startWidth - deltaX; - newLeft = this.startLeft + deltaX; - newHeight = this.startHeight + deltaY; - break; - } - - newWidth = Math.max(this.minWidth, Math.min(this.maxWidth, newWidth)); - newHeight = Math.max(this.minHeight, Math.min(this.maxHeight, newHeight)); - - this.target.style.width = `${newWidth}px`; - this.target.style.height = `${newHeight}px`; - this.target.style.top = `${newTop}px`; - this.target.style.left = `${newLeft}px`; - - this.onResizeMove?.({ - width: newWidth, - height: newHeight, - top: newTop, - left: newLeft, - direction: this.currentDirection, - }); - }; - - private onResizeEndHandler = () => { - if (this.currentDirection) { - const rect = this.target.getBoundingClientRect(); - const parentRect = this.target.offsetParent?.getBoundingClientRect() ?? { left: 0, top: 0 }; - this.onResizeEnd?.({ - width: rect.width, - height: rect.height, - top: rect.top - parentRect.top, - left: rect.left - parentRect.left, - direction: this.currentDirection, - }); - } - - this.currentDirection = null; - this.updateCursor(null); - document.removeEventListener('mousemove', this.onResizeDrag); - document.removeEventListener('mouseup', this.onResizeEndHandler); - }; - - private getResizeDirection(e: MouseEvent): TResizeDirection | null { - const rect = this.target.getBoundingClientRect(); - const offset = 8; - const x = e.clientX; - const y = e.clientY; - - const top = y >= rect.top && y <= rect.top + offset; - const bottom = y >= rect.bottom - offset && y <= rect.bottom; - const left = x >= rect.left && x <= rect.left + offset; - const right = x >= rect.right - offset && x <= rect.right; - - if (top && left) return 'top-left'; - if (top && right) return 'top-right'; - if (bottom && left) return 'bottom-left'; - if (bottom && right) return 'bottom-right'; - if (top) return 'top'; - if (bottom) return 'bottom'; - if (left) return 'left'; - if (right) return 'right'; - - return null; - } - - private updateCursor(direction: TResizeDirection | null) { - if (!direction) { - this.target.style.cursor = 'default'; - return; - } - const cursorMap: Record = { - top: 'ns-resize', - bottom: 'ns-resize', - left: 'ew-resize', - right: 'ew-resize', - 'top-left': 'nwse-resize', - 'top-right': 'nesw-resize', - 'bottom-left': 'nesw-resize', - 'bottom-right': 'nwse-resize', - }; - this.target.style.cursor = cursorMap[direction]; - } - - private onDocumentMouseMoveCursor = (e: MouseEvent) => { - if (this.currentDirection || this.isDragging) return; - const dir = this.getResizeDirection(e); - this.updateCursor(dir); - }; - - private onMouseLeave = () => { - if (!this.currentDirection && !this.isDragging) this.updateCursor(null); - }; - - private animateTo(targetX: number, targetY: number, duration: number, onComplete?: () => void) { - if (this.animationFrame) cancelAnimationFrame(this.animationFrame); - - const startX = this.currentX; - const startY = this.currentY; - const deltaX = targetX - startX; - const deltaY = targetY - startY; - const startTime = performance.now(); - - const step = (now: number) => { - const elapsed = now - startTime; - const progress = Math.min(elapsed / duration, 1); - const ease = 1 - Math.pow(1 - progress, 3); - - const x = startX + deltaX * ease; - const y = startY + deltaY * ease; - - this.applyPosition(x, y, false); - this.onDragMove?.(x, y); - - if (progress < 1) { - this.animationFrame = requestAnimationFrame(step); - } else { - this.applyPosition(targetX, targetY, true); - this.onDragMove?.(targetX, targetY); - onComplete?.(); - } - }; - - this.animationFrame = requestAnimationFrame(step); - } - - private applyBoundary() { - if (!this.boundary || this.allowOverflow) return; - - let { x, y } = { x: this.currentX, y: this.currentY }; - - if (this.boundary instanceof HTMLElement && this.containerRect) { - const rect = this.target.getBoundingClientRect(); - const minX = 0; - const minY = 0; - const maxX = this.containerRect.width - rect.width; - const maxY = this.containerRect.height - rect.height; - - x = Math.min(Math.max(x, minX), maxX); - y = Math.min(Math.max(y, minY), maxY); - } else if (!(this.boundary instanceof HTMLElement)) { - if (this.boundary.minX !== undefined) x = Math.max(x, this.boundary.minX); - if (this.boundary.maxX !== undefined) x = Math.min(x, this.boundary.maxX); - if (this.boundary.minY !== undefined) y = Math.max(y, this.boundary.minY); - if (this.boundary.maxY !== undefined) y = Math.min(y, this.boundary.maxY); - } - - this.currentX = x; - this.currentY = y; - this.applyPosition(x, y, false); - } - - private applySnapping(x: number, y: number) { - let { x: snappedX, y: snappedY } = { x, y }; - - // 1. 容器吸附 - const containerSnap = this.getSnapPoints(); - if (this.snapThreshold > 0) { - for (const sx of containerSnap.x) { - if (Math.abs(x - sx) <= this.snapThreshold) { - snappedX = sx; - break; - } - } - for (const sy of containerSnap.y) { - if (Math.abs(y - sy) <= this.snapThreshold) { - snappedY = sy; - break; - } - } - } - - // 2. 窗口吸附 TODO - - return { x: snappedX, y: snappedY }; - } - - private getSnapPoints() { - const snapPoints = { x: [] as number[], y: [] as number[] }; - - if (this.boundary instanceof HTMLElement && this.containerRect) { - const rect = this.target.getBoundingClientRect(); - snapPoints.x = [0, this.containerRect.width - rect.width]; - snapPoints.y = [0, this.containerRect.height - rect.height]; - } else if (!(this.boundary instanceof HTMLElement) && this.boundary) { - if (this.boundary.minX !== undefined) snapPoints.x.push(this.boundary.minX); - if (this.boundary.maxX !== undefined) snapPoints.x.push(this.boundary.maxX); - if (this.boundary.minY !== undefined) snapPoints.y.push(this.boundary.minY); - if (this.boundary.maxY !== undefined) snapPoints.y.push(this.boundary.maxY); - } - - return snapPoints; - } - - private observeResize(container: HTMLElement) { - if (this.resizeObserver) this.resizeObserver.disconnect(); - this.resizeObserver = new ResizeObserver(() => { - this.containerRect = container.getBoundingClientRect(); - this.applyBoundary(); - }); - this.resizeObserver.observe(container); - this.containerRect = container.getBoundingClientRect(); - } - - /** 销毁实例 */ - public destroy() { - // 拖拽解绑:只在 handle 上解绑 - if (this.handle) { - this.handle.removeEventListener('mousedown', this.onMouseDownDrag); - } - - // 调整尺寸解绑 - this.target.removeEventListener('mousedown', this.onMouseDownResize); - this.target.removeEventListener('mouseleave', this.onMouseLeave); - - // 全局事件解绑 - document.removeEventListener('mousemove', this.onDocumentMouseMoveCursor); - document.removeEventListener('mousemove', this.onMouseMoveDrag); - document.removeEventListener('mouseup', this.onMouseUpDrag); - document.removeEventListener('mousemove', this.onResizeDrag); - document.removeEventListener('mouseup', this.onResizeEndHandler); - - // 观察器清理 - if (this.resizeObserver) this.resizeObserver.disconnect(); - if (this.mutationObserver) this.mutationObserver.disconnect(); - if (this.animationFrame) cancelAnimationFrame(this.animationFrame); - - // 所有属性置空,释放内存 - Object.keys(this).forEach(k => (this as any)[k] = null); - } -} diff --git a/src/core/utils/DraggableResizableWindow.ts b/src/core/utils/DraggableResizableWindow.ts index ebc686b..0798135 100644 --- a/src/core/utils/DraggableResizableWindow.ts +++ b/src/core/utils/DraggableResizableWindow.ts @@ -86,6 +86,9 @@ interface IDraggableResizableOptions { onResizeMove?: (data: IResizeCallbackData) => void; /** 拖拽调整尺寸结束回调 */ onResizeEnd?: (data: IResizeCallbackData) => void; + + /** 窗口状态改变回调 */ + onWindowStateChange?: (state: TWindowState) => void; } /** 拖拽的范围边界 */ @@ -121,8 +124,11 @@ export class DraggableResizableWindow { private onResizeMove?: (data: IResizeCallbackData) => void; private onResizeEnd?: (data: IResizeCallbackData) => void; + private onWindowStateChange?: (state: TWindowState) => void; + private isDragging = false; private currentDirection: TResizeDirection | null = null; + private dragThreshold = 2; // 拖拽阈值 超过才开始真正的拖拽 private startX = 0; private startY = 0; @@ -161,6 +167,10 @@ export class DraggableResizableWindow { private targetPreMaximizedBounds?: IElementRect; private taskbarElementId: string; + get windowState() { + return this.state; + } + constructor(options: IDraggableResizableOptions) { this.handle = options.handle; this.target = options.target; @@ -181,6 +191,7 @@ export class DraggableResizableWindow { this.maxHeight = options.maxHeight ?? window.innerHeight; this.onResizeMove = options.onResizeMove; this.onResizeEnd = options.onResizeEnd; + this.onWindowStateChange = options.onWindowStateChange; requestAnimationFrame(() => { this.targetBounds = { @@ -225,16 +236,49 @@ export class DraggableResizableWindow { private onMouseDownDrag = (e: MouseEvent) => { e.preventDefault(); - e.stopPropagation(); if (e.target !== this.handle) return; - if (this.getResizeDirection(e)) return; - if (this.state === 'maximized') { - this.restore(() => this.startDrag(e)); - } else { - this.startDrag(e); + this.startX = e.clientX; + this.startY = e.clientY; + + document.addEventListener('mousemove', this.checkDragStart); + document.addEventListener('mouseup', this.cancelPendingDrag); + } + + private checkDragStart = (e: MouseEvent) => { + const dx = e.clientX - this.startX; + const dy = e.clientY - this.startY; + + if (Math.abs(dx) > this.dragThreshold || Math.abs(dy) > this.dragThreshold) { + // 超过阈值,真正开始拖拽 + document.removeEventListener('mousemove', this.checkDragStart); + document.removeEventListener('mouseup', this.cancelPendingDrag); + + if (this.state === 'maximized') { + const preRect = this.targetPreMaximizedBounds!; + const rect = this.target.getBoundingClientRect(); + const relX = e.clientX / rect.width; + const relY = e.clientY / rect.height; + const newLeft = e.clientX - preRect.width * relX; + const newTop = e.clientY - preRect.height * relY; + this.targetPreMaximizedBounds = { + width: preRect.width, + height: preRect.height, + top: newTop, + left: newLeft, + }; + + this.restore(() => this.startDrag(e)); + } else { + this.startDrag(e); + } } + }; + + private cancelPendingDrag = () => { + document.removeEventListener('mousemove', this.checkDragStart); + document.removeEventListener('mouseup', this.cancelPendingDrag); } private startDrag = (e: MouseEvent) => { @@ -291,12 +335,12 @@ export class DraggableResizableWindow { if (this.snapAnimation) { this.animateTo(snapped.x, snapped.y, this.snapAnimationDuration, () => { this.onDragEnd?.(snapped.x, snapped.y); - this.updateDefaultBounds(snapped.x, snapped.y); + this.updateTargetBounds(snapped.x, snapped.y); }); } else { this.applyPosition(snapped.x, snapped.y, true); this.onDragEnd?.(snapped.x, snapped.y); - this.updateDefaultBounds(snapped.x, snapped.y); + this.updateTargetBounds(snapped.x, snapped.y); } document.removeEventListener('mousemove', this.onMouseMoveDragRAF); @@ -507,7 +551,7 @@ export class DraggableResizableWindow { top: this.currentY, direction: this.currentDirection, }); - this.updateDefaultBounds(this.currentX, this.currentY, this.target.offsetWidth, this.target.offsetHeight); + this.updateTargetBounds(this.currentX, this.currentY, this.target.offsetWidth, this.target.offsetHeight); this.currentDirection = null; this.updateCursor(null); document.removeEventListener('mousemove', this.onResizeDragRAF); @@ -575,11 +619,10 @@ export class DraggableResizableWindow { /** 最大化 */ public maximize() { if (this.state === 'maximized') return; - this.targetPreMinimizeBounds = { ...this.targetBounds } + this.targetPreMaximizedBounds = { ...this.targetBounds } this.state = 'maximized'; const rect = this.target.getBoundingClientRect(); - this.targetBounds = { width: rect.width, height: rect.height, left: rect.left, top: rect.top }; const startX = this.currentX; const startY = this.currentY; @@ -597,7 +640,6 @@ export class DraggableResizableWindow { /** 恢复到默认窗体状态 */ public restore(onComplete?: () => void) { if (this.state === 'default') return; - this.state = 'default'; let b: IElementRect; if ((this.state as TWindowState) === 'minimized' && this.targetPreMinimizeBounds) { // 最小化恢复,恢复到最小化前的状态 @@ -609,6 +651,8 @@ export class DraggableResizableWindow { b = this.targetBounds; } + this.state = 'default'; + this.target.style.display = 'block'; const startX = this.currentX; @@ -667,12 +711,13 @@ export class DraggableResizableWindow { this.target.style.height = `${targetH}px`; this.applyPosition(targetX, targetY, true); onComplete?.(); + this.onWindowStateChange?.(this.state); } }; requestAnimationFrame(step); } - private updateDefaultBounds(left: number, top: number, width?: number, height?: number) { + private updateTargetBounds(left: number, top: number, width?: number, height?: number) { this.targetBounds = { left, top, width: width ?? this.target.offsetWidth, @@ -699,6 +744,8 @@ export class DraggableResizableWindow { document.removeEventListener('mousemove', this.onResizeDragRAF); document.removeEventListener('mouseup', this.onResizeEndHandler); document.removeEventListener('mousemove', this.onDocumentMouseMoveCursor); + document.removeEventListener('mousemove', this.checkDragStart); + document.removeEventListener('mouseup', this.cancelPendingDrag); this.resizeObserver?.disconnect(); this.mutationObserver.disconnect(); cancelAnimationFrame(this.animationFrame ?? 0); diff --git a/src/core/window/css/window-form.scss b/src/core/window/css/window-form.scss new file mode 100644 index 0000000..5058af3 --- /dev/null +++ b/src/core/window/css/window-form.scss @@ -0,0 +1,47 @@ +/* 窗体容器 */ +.window { + width: 400px; + border: 1px solid #666; + box-shadow: 0 0 10px rgba(0,0,0,0.5); + background-color: #ffffff; + overflow: hidden; + border-radius: 5px; +} + +/* 标题栏 */ +.title-bar { + color: white; + display: flex; + justify-content: space-between; + align-items: center; + user-select: none; + + .window-controls { + display: flex; + gap: 2px; + flex-shrink: 0; + + .btn { + width: 40px; + height: 40px; + padding: 0; + cursor: pointer; + font-size: 24px; + color: black; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.3s ease; + + &:hover { + @apply bg-gray-200; + } + } + } +} + +/* 窗体内容 */ +.window-content { + padding: 15px; + background-color: #e0e0e0; +} \ No newline at end of file diff --git a/src/core/window/impl/WindowFormImpl.ts b/src/core/window/impl/WindowFormImpl.ts index 3e1fdcf..28bbfff 100644 --- a/src/core/window/impl/WindowFormImpl.ts +++ b/src/core/window/impl/WindowFormImpl.ts @@ -6,6 +6,7 @@ import type { IWindowFormConfig } from '@/core/window/types/IWindowFormConfig.ts import type { WindowFormPos } from '@/core/window/types/WindowFormTypes.ts' import { processManager } from '@/core/process/ProcessManager.ts' import { DraggableResizableWindow } from '@/core/utils/DraggableResizableWindow.ts' +import '../css/window-form.scss' export default class WindowFormImpl implements IWindowForm { private readonly _id: string = uuidV4(); @@ -40,49 +41,13 @@ export default class WindowFormImpl implements IWindowForm { } private createWindowFrom() { - this.dom = document.createElement('div'); + this.dom = this.windowFormEle(); this.dom.style.position = 'absolute'; this.dom.style.width = `${this.width}px`; this.dom.style.height = `${this.height}px`; - this.dom.style.zIndex = '100'; - this.dom.style.backgroundColor = 'white'; - this.dom.classList.add('flex', 'flex-col', 'rd-[4px]') - const header = document.createElement('div'); - header.style.width = '100%'; - header.style.height = '20px'; - header.style.backgroundColor = 'red'; - this.dom.appendChild(header) - const bt1 = document.createElement('button'); - bt1.innerText = '最小化'; - bt1.addEventListener('click', () => { - this.drw.minimize(); - setTimeout(() => { - this.drw.restore(); - }, 2000) - }) - header.appendChild(bt1) - const bt2 = document.createElement('button'); - bt2.innerText = '最大化'; - bt2.addEventListener('click', () => { - this.drw.maximize(); - }) - header.appendChild(bt2) - const bt3 = document.createElement('button'); - bt3.innerText = '关闭'; - bt3.addEventListener('click', () => { - this.closeWindowForm(); - }) - header.appendChild(bt3) - const bt4 = document.createElement('button'); - bt4.innerText = '恢复'; - bt4.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation() - this.drw.restore(); - }) - header.appendChild(bt4) - + this.dom.style.zIndex = '10'; + const header = this.dom.querySelector('.title-bar') as HTMLElement; this.drw = new DraggableResizableWindow({ target: this.dom, handle: header, @@ -90,6 +55,13 @@ export default class WindowFormImpl implements IWindowForm { snapThreshold: 20, boundary: document.body, taskbarElementId: '#taskbar', + onWindowStateChange: (state) => { + if (state === 'maximized') { + this.dom.style.borderRadius = '0px'; + } else { + this.dom.style.borderRadius = '5px'; + } + }, }) this.desktopRootDom.appendChild(this.dom); @@ -100,4 +72,43 @@ export default class WindowFormImpl implements IWindowForm { this.desktopRootDom.removeChild(this.dom); this.proc?.event.notifyEvent('onProcessWindowFormExit', this.id) } + + private windowFormEle() { + const template = document.createElement('template'); + template.innerHTML = ` +
+
+
我的窗口
+
+
-
+
+
×
+
+
+
+

这是一个纯静态的 Windows 风格窗体示例。

+

你可以在这里放置任何内容。

+
+
+ ` + const fragment = template.content.cloneNode(true) as DocumentFragment; + const windowElement = fragment.firstElementChild as HTMLElement + + windowElement.querySelector('.btn.minimize') + ?.addEventListener('click', () => this.drw.minimize()); + + windowElement.querySelector('.btn.maximize') + ?.addEventListener('click', () => { + if (this.drw.windowState === 'maximized') { + this.drw.restore() + } else { + this.drw.maximize() + } + }); + + windowElement.querySelector('.btn.close') + ?.addEventListener('click', () => this.closeWindowForm()); + + return windowElement + } } diff --git a/tsconfig.app.json b/tsconfig.app.json index e285f71..17c30d3 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -8,16 +8,16 @@ "paths": { "@/*": ["./src/*"] }, - "experimentalDecorators": true, "target": "es2021", "lib": ["es2021", "dom"], "module": "ESNext", "strict": true, // 严格模式检查 + "experimentalDecorators": true, "strictPropertyInitialization": false, // 严格属性初始化检查 "noUnusedLocals": false, // 检查未使用的局部变量 "noUnusedParameters": false, // 检查未使用的参数 "noImplicitReturns": true, // 检查函数所有路径是否都有返回值 "noImplicitOverride": true, // 检查子类是否正确覆盖了父类方法 - "allowSyntheticDefaultImports": true // 允许使用默认导入 + "allowSyntheticDefaultImports": true, // 允许使用默认导入 } } diff --git a/vite.config.ts b/vite.config.ts index f747d7b..4a99dc0 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -10,7 +10,13 @@ import UnoCSS from 'unocss/vite' // https://vite.dev/config/ export default defineConfig({ plugins: [ - vue(), + vue({ + template: { + compilerOptions: { + isCustomElement: tag => tag.endsWith('-element') // 忽略自定义元素 + } + } + }), vueJsx(), vueDevTools(), UnoCSS()