Add AutoRobot directory with Windows line endings
This commit is contained in:
506
AutoRobot/Windows/Robot/Web/src/components/FloatingWindow.vue
Normal file
506
AutoRobot/Windows/Robot/Web/src/components/FloatingWindow.vue
Normal file
@@ -0,0 +1,506 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user