update
This commit is contained in:
@@ -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', '关闭')">×</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>
|
||||
|
||||
Reference in New Issue
Block a user