保存一下

This commit is contained in:
2025-09-03 11:17:32 +08:00
parent 926ac698cc
commit 53fc32de24
3 changed files with 148 additions and 77 deletions

View File

@@ -13,7 +13,9 @@
/> />
</div> </div>
</div> </div>
<div class="task-bar"></div> <div class="task-bar">
<div id="taskbar" class="w-[80px] h-full flex justify-center items-center bg-blue">测试</div>
</div>
</div> </div>
</n-config-provider> </n-config-provider>
</template> </template>
@@ -70,7 +72,7 @@ $taskBarHeight: 40px;
} }
.task-bar { .task-bar {
@apply w-full bg-gray-200; @apply w-full bg-gray-200 flex justify-center items-center;
height: $taskBarHeight; height: $taskBarHeight;
flex-shrink: 0; flex-shrink: 0;
} }

View File

@@ -19,12 +19,7 @@ type TResizeDirection =
/** 窗口状态 */ /** 窗口状态 */
type WindowState = 'default' | 'minimized' | 'maximized'; type WindowState = 'default' | 'minimized' | 'maximized';
interface TaskbarOptions { /** 元素边界 */
/** 任务栏图标 DOM 元素或目标位置 {x, y} */
element?: HTMLElement;
position?: { x: number; y: number };
}
interface IElementRect { interface IElementRect {
/** 宽度 */ /** 宽度 */
width: number; width: number;
@@ -70,6 +65,8 @@ interface IDraggableResizableOptions {
snapAnimationDuration?: number; snapAnimationDuration?: number;
/** 是否允许超出边界 */ /** 是否允许超出边界 */
allowOverflow?: boolean; allowOverflow?: boolean;
/** 最小化任务栏位置的元素ID */
taskbarElementId: string;
/** 拖拽开始回调 */ /** 拖拽开始回调 */
onDragStart?: TDragStartCallback; onDragStart?: TDragStartCallback;
@@ -106,7 +103,7 @@ interface IBoundaryRect {
} }
/** /**
* 拖拽 + 调整尺寸通用类 * 拖拽 + 调整尺寸 + 最大最小化 通用类
*/ */
export class DraggableResizableWindow { export class DraggableResizableWindow {
private handle?: HTMLElement; private handle?: HTMLElement;
@@ -153,10 +150,10 @@ export class DraggableResizableWindow {
private targetDefaultBounds: IElementRect; private targetDefaultBounds: IElementRect;
/** 最大化前保存 bounds */ /** 最大化前保存 bounds */
private maximizedBounds?: IElementRect; private maximizedBounds?: IElementRect;
/** 任务栏相关配置 */ /** 最小化任务栏位置的元素ID */
private taskbar?: TaskbarOptions; private taskbarElementId: string;
constructor(options: IDraggableResizableOptions & { taskbar?: TaskbarOptions }) { constructor(options: IDraggableResizableOptions) {
// Drag // Drag
this.handle = options.handle; this.handle = options.handle;
this.target = options.target; this.target = options.target;
@@ -181,7 +178,7 @@ export class DraggableResizableWindow {
this.onResizeEnd = options.onResizeEnd; this.onResizeEnd = options.onResizeEnd;
this.targetDefaultBounds = { width: this.target.offsetWidth, height: this.target.offsetHeight, top: this.target.offsetTop, left: this.target.offsetLeft }; this.targetDefaultBounds = { width: this.target.offsetWidth, height: this.target.offsetHeight, top: this.target.offsetTop, left: this.target.offsetLeft };
this.taskbar = options.taskbar; this.taskbarElementId = options.taskbarElementId;
this.init(); this.init();
} }
@@ -600,53 +597,131 @@ export class DraggableResizableWindow {
if (this.state === 'minimized') return; if (this.state === 'minimized') return;
this.state = 'minimized'; this.state = 'minimized';
// 获取目标任务栏位置 // 获取任务栏位置
let targetX = 50, targetY = window.innerHeight - 40; const taskbarElement = document.querySelector(this.taskbarElementId)
if (this.taskbar?.element) { if (!taskbarElement) throw new Error('任务栏元素未找到');
const rect = this.taskbar.element.getBoundingClientRect(); const rect = taskbarElement.getBoundingClientRect()
targetX = rect.left; const targetX = rect.left;
targetY = rect.top; const targetY = rect.top;
} else if (this.taskbar?.position) { const targetWidth = rect.width;
targetX = this.taskbar.position.x; const targetHeight = rect.height;
targetY = this.taskbar.position.y;
const startX = this.currentX;
const startY = this.currentY;
const startWidth = this.target.offsetWidth;
const startHeight = this.target.offsetHeight;
const deltaX = targetX - startX;
const deltaY = targetY - startY;
const deltaW = targetWidth - startWidth;
const deltaH = targetHeight - startHeight;
const duration = 400; // Windows 风格稍慢
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;
const w = startWidth + deltaW * ease;
const h = startHeight + deltaH * ease;
this.target.style.width = `${w}px`;
this.target.style.height = `${h}px`;
this.applyPosition(x, y, false);
if (progress < 1) {
requestAnimationFrame(step);
} else {
// 最终值
this.target.style.width = `${targetWidth}px`;
this.target.style.height = `${targetHeight}px`;
this.applyPosition(targetX, targetY, true);
}
};
requestAnimationFrame(step);
} }
this.animateTo(targetX, targetY, 300, () => { /** 最大化窗口(带动画) */
this.target.style.width = '0px'; public maximize(withAnimation = true) {
this.target.style.height = '0px';
});
}
/** 最大化 */
public maximize() {
if (this.state === 'maximized') return; if (this.state === 'maximized') return;
this.state = 'maximized'; this.state = 'maximized';
// 保存原始 bounds
const rect = this.target.getBoundingClientRect(); const rect = this.target.getBoundingClientRect();
this.targetDefaultBounds = { width: rect.width, height: rect.height, top: rect.top, left: rect.left }; this.targetDefaultBounds = { width: rect.width, height: rect.height, top: rect.top, left: rect.left };
this.maximizedBounds = { ...this.targetDefaultBounds }; const startWidth = rect.width;
const startHeight = rect.height;
const startX = this.currentX;
const startY = this.currentY;
const width = this.containerRect?.width ?? window.innerWidth; const targetWidth = this.containerRect?.width ?? window.innerWidth;
const height = this.containerRect?.height ?? window.innerHeight; const targetHeight = this.containerRect?.height ?? window.innerHeight;
const targetX = 0;
const targetY = 0;
this.target.style.width = `${width}px`; if (!withAnimation) {
this.target.style.height = `${height}px`; this.target.style.width = `${targetWidth}px`;
this.applyPosition(0, 0, true); this.target.style.height = `${targetHeight}px`;
this.applyPosition(targetX, targetY, true);
return;
} }
/** 恢复到默认状态 */ // 动画过渡
const deltaX = targetX - startX;
const deltaY = targetY - startY;
const deltaW = targetWidth - startWidth;
const deltaH = targetHeight - startHeight;
const duration = 300;
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;
const w = startWidth + deltaW * ease;
const h = startHeight + deltaH * ease;
this.target.style.width = `${w}px`;
this.target.style.height = `${h}px`;
this.applyPosition(x, y, false);
if (progress < 1) {
requestAnimationFrame(step);
} else {
this.target.style.width = `${targetWidth}px`;
this.target.style.height = `${targetHeight}px`;
this.applyPosition(targetX, targetY, true);
}
};
requestAnimationFrame(step);
}
/** 恢复窗口(带动画,从最大化或最小化) */
public restore(withAnimation = true) { public restore(withAnimation = true) {
if (this.state === 'default') return; if (this.state === 'default') return;
this.state = 'default'; this.state = 'default';
const b = this.targetDefaultBounds; const b = this.targetDefaultBounds;
if (withAnimation) {
// 从当前位置(可能是任务栏位置或 0 尺寸)动画过渡到 targetDefaultBounds
const startWidth = this.target.offsetWidth || 0; const startWidth = this.target.offsetWidth || 0;
const startHeight = this.target.offsetHeight || 0; const startHeight = this.target.offsetHeight || 0;
const startLeft = this.currentX; const startLeft = this.currentX;
const startTop = this.currentY; const startTop = this.currentY;
if (!withAnimation) {
this.target.style.width = `${b.width}px`;
this.target.style.height = `${b.height}px`;
this.applyPosition(b.left, b.top, true);
return;
}
const deltaX = b.left - startLeft; const deltaX = b.left - startLeft;
const deltaY = b.top - startTop; const deltaY = b.top - startTop;
const deltaW = b.width - startWidth; const deltaW = b.width - startWidth;
@@ -657,7 +732,7 @@ export class DraggableResizableWindow {
const step = (now: number) => { const step = (now: number) => {
const elapsed = now - startTime; const elapsed = now - startTime;
const progress = Math.min(elapsed / duration, 1); const progress = Math.min(elapsed / duration, 1);
const ease = 1 - Math.pow(1 - progress, 3); // 缓动函数 const ease = 1 - Math.pow(1 - progress, 3);
const x = startLeft + deltaX * ease; const x = startLeft + deltaX * ease;
const y = startTop + deltaY * ease; const y = startTop + deltaY * ease;
@@ -671,7 +746,6 @@ export class DraggableResizableWindow {
if (progress < 1) { if (progress < 1) {
requestAnimationFrame(step); requestAnimationFrame(step);
} else { } else {
// 最终值
this.target.style.width = `${b.width}px`; this.target.style.width = `${b.width}px`;
this.target.style.height = `${b.height}px`; this.target.style.height = `${b.height}px`;
this.applyPosition(b.left, b.top, true); this.applyPosition(b.left, b.top, true);
@@ -679,14 +753,9 @@ export class DraggableResizableWindow {
}; };
requestAnimationFrame(step); requestAnimationFrame(step);
} else {
// 不动画直接恢复
this.target.style.width = `${b.width}px`;
this.target.style.height = `${b.height}px`;
this.applyPosition(b.left, b.top, true);
}
} }
/** 更新默认 bounds */ /** 更新默认 bounds */
private updateDefaultBounds(x?: number, y?: number, width?: number, height?: number) { private updateDefaultBounds(x?: number, y?: number, width?: number, height?: number) {
const rect = this.target.getBoundingClientRect(); const rect = this.target.getBoundingClientRect();

View File

@@ -80,7 +80,7 @@ export default class WindowFormImpl implements IWindowForm {
mode: 'position', mode: 'position',
snapThreshold: 20, snapThreshold: 20,
boundary: document.body, boundary: document.body,
taskbar: { position: { x: 50, y: window.innerHeight - 40 } }, taskbarElementId: '#taskbar',
}) })
this.desktopRootDom.appendChild(dom); this.desktopRootDom.appendChild(dom);