This commit is contained in:
Baobhan Sith
2025-04-17 21:40:50 +08:00
parent 636ed5a2bb
commit 1c4ae3a3e1
17 changed files with 959 additions and 357 deletions
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, computed, watch, type Ref } from 'vue';
import { ref, computed, watch, onMounted, onUnmounted, type Ref, reactive } from 'vue'; // 导入 reactive
import { useI18n } from 'vue-i18n';
import { useLayoutStore, type LayoutNode, type PaneName } from '../stores/layout.store';
import draggable from 'vuedraggable';
@@ -26,18 +26,67 @@ const localLayoutTree: Ref<LayoutNode | null> = ref(null);
// 标记是否有更改未保存
const hasChanges = ref(false);
// --- Resizing State ---
const dialogRef = ref<HTMLElement | null>(null); // 对话框元素的引用
const initialDialogState = { width: 800, height: 600 }; // 初始/默认尺寸 (px)
const dialogStyle = reactive({
width: `${initialDialogState.width}px`,
height: `${initialDialogState.height}px`,
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)', // 初始居中
position: 'absolute' as 'absolute', // 显式设置定位
});
const isResizing = ref(false);
const resizeHandle = ref<string | null>(null); // 记录当前拖拽的手柄 ('t', 'b', 'l', 'r', 'tl', 'tr', 'bl', 'br')
const startDragPos = { x: 0, y: 0 };
const startDialogRect = { width: 0, height: 0, top: 0, left: 0 };
const minDialogSize = { width: 400, height: 300 }; // 最小尺寸限制
// --- Watchers ---
// 当弹窗可见时,从 store 加载当前布局到本地副本
// 当弹窗可见时,从 store 加载当前布局并计算初始位置
watch(() => props.isVisible, (newValue) => {
if (newValue && layoutStore.layoutTree) {
// 深拷贝以避免直接修改 store 状态
localLayoutTree.value = JSON.parse(JSON.stringify(layoutStore.layoutTree));
hasChanges.value = false; // 重置更改状态
if (newValue) {
// 加载布局
if (layoutStore.layoutTree) {
localLayoutTree.value = JSON.parse(JSON.stringify(layoutStore.layoutTree));
}
hasChanges.value = false;
console.log('[LayoutConfigurator] 弹窗打开,已加载当前布局到本地副本。');
// 重置/计算初始位置和大小 (确保在 DOM 更新后执行,或给个延迟)
requestAnimationFrame(() => {
if (dialogRef.value) {
// 尝试读取当前计算出的尺寸,如果失败则使用默认值
const currentRect = dialogRef.value.getBoundingClientRect();
const initialWidth = currentRect.width > minDialogSize.width ? currentRect.width : initialDialogState.width;
const initialHeight = currentRect.height > minDialogSize.height ? currentRect.height : initialDialogState.height;
dialogStyle.width = `${initialWidth}px`;
dialogStyle.height = `${initialHeight}px`;
dialogStyle.left = `${(window.innerWidth - initialWidth) / 2}px`;
dialogStyle.top = `${(window.innerHeight - initialHeight) / 2}px`;
dialogStyle.transform = 'none'; // 移除 transform 居中
dialogStyle.position = 'absolute';
console.log('[LayoutConfigurator] Dialog initial position calculated:', dialogStyle);
} else {
// Fallback if ref not ready (less accurate centering)
dialogStyle.width = `${initialDialogState.width}px`;
dialogStyle.height = `${initialDialogState.height}px`;
dialogStyle.left = '50%';
dialogStyle.top = '50%';
dialogStyle.transform = 'translate(-50%, -50%)';
dialogStyle.position = 'absolute';
}
});
} else {
localLayoutTree.value = null; // 关闭时清空本地副本
isResizing.value = false; // 确保关闭时重置拖拽状态
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
}
});
}, { immediate: true }); // immediate: true 可能导致初始计算问题,先去掉试试
// 监听本地布局树的变化,标记有未保存更改
watch(localLayoutTree, (newValue, oldValue) => {
@@ -125,7 +174,99 @@ const resetToDefault = () => {
}
};
// --- Drag & Drop Methods ---
// --- Resizing Methods ---
const handleMouseDown = (event: MouseEvent, handle: string) => {
if (!dialogRef.value) return;
event.preventDefault(); // 阻止默认行为,如文本选择
event.stopPropagation();
isResizing.value = true;
resizeHandle.value = handle;
startDragPos.x = event.clientX;
startDragPos.y = event.clientY;
// 解析当前的 px 值
const currentWidth = parseFloat(dialogStyle.width);
const currentHeight = parseFloat(dialogStyle.height);
const currentTop = parseFloat(dialogStyle.top);
const currentLeft = parseFloat(dialogStyle.left);
startDialogRect.width = isNaN(currentWidth) ? initialDialogState.width : currentWidth;
startDialogRect.height = isNaN(currentHeight) ? initialDialogState.height : currentHeight;
startDialogRect.top = isNaN(currentTop) ? (window.innerHeight - startDialogRect.height) / 2 : currentTop;
startDialogRect.left = isNaN(currentLeft) ? (window.innerWidth - startDialogRect.width) / 2 : currentLeft;
console.log(`[LayoutConfigurator] MouseDown on handle ${handle}. Start rect:`, { ...startDialogRect });
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
};
const handleMouseMove = (event: MouseEvent) => {
if (!isResizing.value || !resizeHandle.value) return;
const deltaX = event.clientX - startDragPos.x;
const deltaY = event.clientY - startDragPos.y;
let newWidth = startDialogRect.width;
let newHeight = startDialogRect.height;
let newTop = startDialogRect.top;
let newLeft = startDialogRect.left;
// 根据拖拽的手柄计算新的尺寸和位置 (中心缩放)
if (resizeHandle.value.includes('r')) {
newWidth = startDialogRect.width + deltaX * 2;
newLeft = startDialogRect.left - deltaX;
}
if (resizeHandle.value.includes('l')) {
newWidth = startDialogRect.width - deltaX * 2;
newLeft = startDialogRect.left + deltaX;
}
if (resizeHandle.value.includes('b')) {
newHeight = startDialogRect.height + deltaY * 2;
newTop = startDialogRect.top - deltaY;
}
if (resizeHandle.value.includes('t')) {
newHeight = startDialogRect.height - deltaY * 2;
newTop = startDialogRect.top + deltaY;
}
// 应用最小尺寸限制
if (newWidth < minDialogSize.width) {
const diff = minDialogSize.width - newWidth;
newWidth = minDialogSize.width;
// 根据拖拽方向调整 left 以保持中心
if (resizeHandle.value.includes('r')) newLeft += diff / 2;
if (resizeHandle.value.includes('l')) newLeft -= diff / 2;
}
if (newHeight < minDialogSize.height) {
const diff = minDialogSize.height - newHeight;
newHeight = minDialogSize.height;
// 根据拖拽方向调整 top 以保持中心
if (resizeHandle.value.includes('b')) newTop += diff / 2;
if (resizeHandle.value.includes('t')) newTop -= diff / 2;
}
// 更新样式
dialogStyle.width = `${newWidth}px`;
dialogStyle.height = `${newHeight}px`;
dialogStyle.top = `${newTop}px`;
dialogStyle.left = `${newLeft}px`;
};
const handleMouseUp = () => {
if (isResizing.value) {
console.log(`[LayoutConfigurator] MouseUp. Final rect:`, { width: dialogStyle.width, height: dialogStyle.height, top: dialogStyle.top, left: dialogStyle.left });
isResizing.value = false;
resizeHandle.value = null;
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
}
};
// --- Drag & Drop Methods (for panes, unchanged) ---
// 克隆函数:当从可用列表拖拽时,创建新的 LayoutNode 对象
const clonePane = (paneName: PaneName): LayoutNode => {
console.log(`[LayoutConfigurator] 克隆面板: ${paneName}`);
@@ -204,7 +345,18 @@ const handleNodeRemove = (payload: { parentNodeId: string | undefined; nodeIndex
<template>
<div v-if="isVisible" class="layout-configurator-overlay" @click.self="closeDialog">
<div class="layout-configurator-dialog">
<!-- 应用动态样式并添加 ref -->
<div ref="dialogRef" class="layout-configurator-dialog" :style="dialogStyle">
<!-- Resize Handles -->
<div class="resize-handle top" @mousedown.prevent="handleMouseDown($event, 't')"></div>
<div class="resize-handle bottom" @mousedown.prevent="handleMouseDown($event, 'b')"></div>
<div class="resize-handle left" @mousedown.prevent="handleMouseDown($event, 'l')"></div>
<div class="resize-handle right" @mousedown.prevent="handleMouseDown($event, 'r')"></div>
<div class="resize-handle top-left" @mousedown.prevent="handleMouseDown($event, 'tl')"></div>
<div class="resize-handle top-right" @mousedown.prevent="handleMouseDown($event, 'tr')"></div>
<div class="resize-handle bottom-left" @mousedown.prevent="handleMouseDown($event, 'bl')"></div>
<div class="resize-handle bottom-right" @mousedown.prevent="handleMouseDown($event, 'br')"></div>
<header class="dialog-header">
<h2>{{ t('layoutConfigurator.title', '配置工作区布局') }}</h2>
<button class="close-button" @click="closeDialog" :title="t('common.close', '关闭')">&times;</button>
@@ -287,20 +439,32 @@ const handleNodeRemove = (payload: { parentNodeId: string | undefined; nodeIndex
background-color: rgba(0, 0, 0, 0.6);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000; /* 低于 TerminalTabBar 的弹出窗口?可能需要调整 */
align-items: flex-start; /* 改为 flex-start 以配合 absolute 定位 */
/* 移除 justify-content: center; */
z-index: 1000;
/* 添加 pointer-events none 以允许点击穿透覆盖层,除非点在 dialog 上 */
pointer-events: none;
}
.layout-configurator-dialog {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
width: 80vw;
max-width: 900px; /* 增加最大宽度 */
max-height: 85vh;
/* width, height, top, left 由 dialogStyle 控制 */
/* 移除 max-width, max-height,由拖拽逻辑和 minSize 控制 */
display: flex;
flex-direction: column;
overflow: hidden; /* 防止内容溢出 */
position: absolute; /* 确保是 absolute */
/* 允许 dialog 接收点击事件 */
pointer-events: auto;
/* 添加 resize 光标,当悬停在 dialog 上但不在 handle 上时 */
cursor: default;
}
/* 当正在调整大小时,给 body 添加样式以覆盖其他元素的鼠标指针 */
body.resizing * {
cursor: grabbing !important; /* 或者根据 handle 方向设置不同的 cursor */
}
.dialog-header {
@@ -462,4 +626,44 @@ h3 {
background-color: #dee2e6;
}
/* --- Resize Handle Styles --- */
.resize-handle {
position: absolute;
background: transparent; /* 使手柄不可见,但可交互 */
z-index: 10; /* 确保在内容之上 */
}
.resize-handle.top, .resize-handle.bottom {
left: 10px; /* 调整手柄区域 */
right: 10px;
height: 10px; /* 手柄厚度 */
cursor: ns-resize;
}
.resize-handle.left, .resize-handle.right {
top: 10px;
bottom: 10px;
width: 10px;
cursor: ew-resize;
}
.resize-handle.top { top: -5px; }
.resize-handle.bottom { bottom: -5px; }
.resize-handle.left { left: -5px; }
.resize-handle.right { right: -5px; }
.resize-handle.top-left, .resize-handle.top-right, .resize-handle.bottom-left, .resize-handle.bottom-right {
width: 16px; /* 角落手柄稍大 */
height: 16px;
}
.resize-handle.top-left { top: -8px; left: -8px; cursor: nwse-resize; }
.resize-handle.top-right { top: -8px; right: -8px; cursor: nesw-resize; }
.resize-handle.bottom-left { bottom: -8px; left: -8px; cursor: nesw-resize; }
.resize-handle.bottom-right { bottom: -8px; right: -8px; cursor: nwse-resize; }
/* 可选:添加一个细微的边框或背景色以便调试 */
/*
.resize-handle {
border: 1px dotted rgba(255, 0, 0, 0.5);
}
*/
</style>