Files
JoyD/AutoRobot/Windows/Robot/Web/src/components/FloatingWindow.vue

506 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div
class="floating-window"
:style="windowStyle"
:data-window-id="window.id"
@click="handleWindowActivate"
>
<!-- 标题栏 -->
<div
class="floating-window-titlebar"
:style="titleBarStyle"
@mousedown="handleTitleBarMouseDown"
>
<span v-if="window.icon" class="window-icon">{{ window.icon }}</span>
<span class="window-title">{{ window.title }}</span>
<!-- 窗口操作按钮 -->
<div class="window-controls">
<button
class="window-btn window-btn-dock"
@click.stop="handleDock"
title="停靠窗口"
>
📌
</button>
<button
class="window-btn window-btn-minimize"
@click.stop="handleMinimize"
title="最小化窗口"
>
</button>
<button
class="window-btn window-btn-maximize"
@click.stop="handleMaximize"
:title="window.maximized ? '还原窗口' : '最大化窗口'"
>
{{ window.maximized ? '□' : '◻' }}
</button>
<button
class="window-btn window-btn-close"
@click.stop="handleClose"
title="关闭窗口"
>
×
</button>
</div>
</div>
<!-- 内容区 -->
<div
class="floating-window-content"
:style="contentStyle"
>
<!-- 渲染窗口内容 -->
<template v-if="window.component">
<component
:is="window.component"
v-bind="window.props || {}"
:style="{ height: '100%', width: '100%', overflow: 'auto' }"
/>
</template>
<template v-else-if="window.content">
<div class="window-default-content">
{{ window.content }}
</div>
</template>
<template v-else>
<div class="window-empty-content">
无内容
</div>
</template>
</div>
<!-- 调整大小手柄 -->
<template v-if="!window.maximized">
<!-- 左上 -->
<div
class="resize-handle resize-tl"
@mousedown="startResize({ top: true, left: true }, $event)"
/>
<!-- 右上 -->
<div
class="resize-handle resize-tr"
@mousedown="startResize({ top: true, right: true }, $event)"
/>
<!-- 左下 -->
<div
class="resize-handle resize-bl"
@mousedown="startResize({ bottom: true, left: true }, $event)"
/>
<!-- 右下 -->
<div
class="resize-handle resize-br"
@mousedown="startResize({ bottom: true, right: true }, $event)"
/>
<!-- -->
<div
class="resize-handle resize-t"
@mousedown="startResize({ top: true }, $event)"
/>
<!-- -->
<div
class="resize-handle resize-b"
@mousedown="startResize({ bottom: true }, $event)"
/>
<!-- -->
<div
class="resize-handle resize-l"
@mousedown="startResize({ left: true }, $event)"
/>
<!-- -->
<div
class="resize-handle resize-r"
@mousedown="startResize({ right: true }, $event)"
/>
</template>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue';
// 定义组件属性
const props = defineProps({
// 窗口数据
window: {
type: Object,
required: true
}
});
// 定义事件
const emit = defineEmits([
'close', // 关闭窗口
'dock', // 停靠窗口
'move', // 移动窗口
'minimize', // 最小化窗口
'maximize' // 最大化窗口
]);
// 窗口位置和尺寸状态
const position = ref({
x: props.window.x || 100,
y: props.window.y || 100
});
const size = ref({
width: props.window.width || 400,
height: props.window.height || 300
});
// 窗口状态
const isDragging = ref(false);
const dragStartPos = ref({ x: 0, y: 0 });
const isResizing = ref(false);
const resizeStartPos = ref({ x: 0, y: 0 });
const resizeEdges = ref({});
// 计算窗口样式
const windowStyle = computed(() => {
const style = {
position: 'fixed',
top: `${position.value.y}px`,
left: `${position.value.x}px`,
width: `${size.value.width}px`,
height: `${size.value.height}px`,
backgroundColor: 'white',
border: '1px solid #e5e7eb',
borderRadius: '4px',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
zIndex: props.window.zIndex || 50,
display: props.window.minimized ? 'none' : 'flex',
flexDirection: 'column',
overflow: 'hidden',
transition: 'box-shadow 0.2s ease'
};
// 最大化状态样式
if (props.window.maximized) {
style.top = '0px';
style.left = '0px';
style.width = '100vw';
style.height = '100vh';
style.borderRadius = '0px';
style.zIndex = 100;
}
return style;
});
// 计算标题栏样式
const titleBarStyle = computed(() => {
return {
display: 'flex',
alignItems: 'center',
padding: '0 12px',
height: '36px',
backgroundColor: '#f3f4f6',
borderBottom: '1px solid #e5e7eb',
cursor: 'move',
userSelect: 'none',
fontSize: '13px',
fontWeight: '500'
};
});
// 计算内容区样式
const contentStyle = computed(() => {
return {
flex: 1,
overflow: 'hidden',
position: 'relative'
};
});
// 处理标题栏鼠标按下事件
const handleTitleBarMouseDown = (event) => {
if (event.target.tagName.toLowerCase() === 'button') {
return; // 如果点击的是按钮,不触发拖拽
}
isDragging.value = true;
dragStartPos.value = {
x: event.clientX - position.value.x,
y: event.clientY - position.value.y
};
// 提升窗口层级
if (!props.window.maximized) {
props.window.zIndex = Math.max(...Array.from(document.querySelectorAll('.floating-window')).map(el => parseInt(el.style.zIndex) || 50)) + 1;
}
// 阻止默认行为
event.preventDefault();
};
// 处理窗口关闭
const handleClose = () => {
emit('close', props.window.id);
};
// 处理窗口最小化
const handleMinimize = () => {
emit('minimize', props.window.id);
};
// 处理窗口最大化/还原
const handleMaximize = () => {
emit('maximize', props.window.id);
};
// 处理窗口停靠
const handleDock = () => {
emit('dock', props.window.id);
};
// 开始调整大小
const startResize = (edges, event) => {
isResizing.value = true;
resizeStartPos.value = {
x: event.clientX,
y: event.clientY
};
resizeEdges.value = edges;
// 阻止默认行为
event.preventDefault();
};
// 处理鼠标移动
const handleMouseMove = (event) => {
if (isDragging.value) {
// 处理窗口移动
const newX = event.clientX - dragStartPos.value.x;
const newY = event.clientY - dragStartPos.value.y;
position.value = {
x: newX,
y: newY
};
emit('move', props.window.id, newX, newY);
} else if (isResizing.value) {
// 处理窗口调整大小
const deltaX = event.clientX - resizeStartPos.value.x;
const deltaY = event.clientY - resizeStartPos.value.y;
let newWidth = size.value.width;
let newHeight = size.value.height;
let newX = position.value.x;
let newY = position.value.y;
// 根据边缘调整尺寸和位置
if (resizeEdges.value.left) {
newWidth = Math.max(200, size.value.width - deltaX);
newX = position.value.x + deltaX;
}
if (resizeEdges.value.right) {
newWidth = Math.max(200, size.value.width + deltaX);
}
if (resizeEdges.value.top) {
newHeight = Math.max(150, size.value.height - deltaY);
newY = position.value.y + deltaY;
}
if (resizeEdges.value.bottom) {
newHeight = Math.max(150, size.value.height + deltaY);
}
// 更新窗口状态
size.value = {
width: newWidth,
height: newHeight
};
position.value = {
x: newX,
y: newY
};
// 通知父组件
emit('move', props.window.id, newX, newY);
// 更新拖拽起点
resizeStartPos.value = {
x: event.clientX,
y: event.clientY
};
}
};
// 处理鼠标松开
const handleMouseUp = () => {
isDragging.value = false;
isResizing.value = false;
};
// 处理窗口激活
const handleWindowActivate = () => {
// 提升窗口层级
if (!props.window.maximized) {
props.window.zIndex = Math.max(...Array.from(document.querySelectorAll('.floating-window')).map(el => parseInt(el.style.zIndex) || 50)) + 1;
}
};
// 生命周期钩子
onMounted(() => {
// 同步外部窗口状态
if (props.window.x !== undefined) position.value.x = props.window.x;
if (props.window.y !== undefined) position.value.y = props.window.y;
if (props.window.width !== undefined) size.value.width = props.window.width;
if (props.window.height !== undefined) size.value.height = props.window.height;
// 添加全局事件监听
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
});
onUnmounted(() => {
// 清理全局事件监听
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
});
</script>
<style scoped>
/* 窗口图标样式 */
.window-icon {
margin-right: 8px;
font-size: 14px;
display: inline-flex;
align-items: center;
justify-content: center;
}
/* 窗口标题样式 */
.window-title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 窗口控制按钮组样式 */
.window-controls {
display: flex;
align-items: center;
}
/* 窗口按钮通用样式 */
.window-btn {
background: none;
border: none;
cursor: pointer;
padding: 4px 6px;
border-radius: 2px;
color: #6b7280;
font-size: 12px;
margin-left: 4px;
transition: all 0.2s ease;
}
.window-btn:hover {
background-color: #e5e7eb;
color: #111827;
}
/* 关闭按钮特殊样式 */
.window-btn-close:hover {
background-color: #ef4444 !important;
color: white !important;
}
/* 默认内容样式 */
.window-default-content {
padding: 16px;
height: 100%;
width: 100%;
overflow: auto;
}
/* 空内容样式 */
.window-empty-content {
padding: 16px;
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #9ca3af;
font-size: 14px;
}
/* 调整大小手柄样式 */
.resize-handle {
position: absolute;
background-color: transparent;
z-index: 10;
}
/* 四角手柄 */
.resize-tl {
top: -4px;
left: -4px;
width: 8px;
height: 8px;
cursor: nwse-resize;
}
.resize-tr {
top: -4px;
right: -4px;
width: 8px;
height: 8px;
cursor: nesw-resize;
}
.resize-bl {
bottom: -4px;
left: -4px;
width: 8px;
height: 8px;
cursor: nesw-resize;
}
.resize-br {
bottom: -4px;
right: -4px;
width: 8px;
height: 8px;
cursor: nwse-resize;
}
/* 四边手柄 */
.resize-t {
top: -4px;
left: 8px;
right: 8px;
height: 8px;
cursor: ns-resize;
}
.resize-b {
bottom: -4px;
left: 8px;
right: 8px;
height: 8px;
cursor: ns-resize;
}
.resize-l {
left: -4px;
top: 8px;
bottom: 8px;
width: 8px;
cursor: ew-resize;
}
.resize-r {
right: -4px;
top: 8px;
bottom: 8px;
width: 8px;
cursor: ew-resize;
}
</style>