Files
JoyD/AutoRobot/Windows/Robot/Web/src/DockLayout/Area.vue
2025-11-18 13:48:13 +08:00

985 lines
29 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="vs-area select-none"
:class="{ 'is-maximized': isMaximized, 'is-normal': !isMaximized }"
:style="areaStyle"
:data-area-id="id"
@dragover="handleDragOver"
@dragleave="handleDragLeave"
>
<!-- 调整大小的边框 -->
<div
v-if="resizable && !isMaximized"
class="resize-handle resize-handle-nw"
@mousedown="onResizeStart('nw', $event)"
></div>
<div
v-if="resizable && !isMaximized"
class="resize-handle resize-handle-ne"
@mousedown="onResizeStart('ne', $event)"
></div>
<div
v-if="resizable && !isMaximized"
class="resize-handle resize-handle-sw"
@mousedown="onResizeStart('sw', $event)"
></div>
<div
v-if="resizable && !isMaximized"
class="resize-handle resize-handle-se"
@mousedown="onResizeStart('se', $event)"
></div>
<div
v-if="resizable && !isMaximized"
class="resize-handle resize-handle-n"
@mousedown="onResizeStart('n', $event)"
></div>
<div
v-if="resizable && !isMaximized"
class="resize-handle resize-handle-e"
@mousedown="onResizeStart('e', $event)"
></div>
<div
v-if="resizable && !isMaximized"
class="resize-handle resize-handle-s"
@mousedown="onResizeStart('s', $event)"
></div>
<div
v-if="resizable && !isMaximized"
class="resize-handle resize-handle-w"
@mousedown="onResizeStart('w', $event)"
></div>
<!-- 顶部标题栏 -->
<div v-if="showTitleBar" class="vs-title-bar" :class="{ 'cursor-move': !isMaximized }" @mousedown="onDragStart">
<div class="vs-title-left">
<div class="vs-app-icon" aria-label="AppIcon">
<svg class="vs-icon" viewBox="0 0 22.4 22.4" aria-hidden="true">
<path
fill="#68217A"
fill-rule="evenodd"
clip-rule="evenodd"
style="shape-rendering: crispEdges;"
d="M0 4.2 L1.8 3.4 L5.8 6.6 L12.6 0 L16.6 1.8 L16.6 15 L12.4 16.6 L6 10.2 L1.8 13.4 L0 12.6 Z
M1.8 5.8 L4.2 8.4 L1.8 10.8 Z
M8.2 8.4 L12.6 5 L12.4 11.6 Z" />
</svg>
</div>
<span class="vs-title-text">{{ title || '面板区' }}</span>
</div>
<div class="vs-title-right title-bar-buttons flex items-center gap-0.5">
<button class="button-icon p-[2px] rounded hover:opacity-100 opacity-80"
:aria-label="isMaximized ? '还原' : '最大化'"
@click.stop="onToggleMaximize"
@mousedown.stop>
<svg v-if="!isMaximized" class="icon-square-svg" width="11" height="11" viewBox="0 0 11 11" aria-hidden="true">
<rect x="0.5" y="0.5" width="10" height="10" fill="#cbd6ff" stroke="#8ea3d8" stroke-width="1" />
<rect x="3" y="3" width="5" height="1" fill="#b8c6ff" />
<rect x="1" y="3" width="8.5" height="6.5" fill="#435d9c" />
</svg>
<svg v-else class="icon-square-svg" width="11" height="11" viewBox="0 0 11 11" aria-hidden="true">
<path
fill="#CED4DD"
fill-rule="evenodd"
clip-rule="evenodd"
d="M1 4 L4 4 L4 1 L11 1 L11 8 L8 8 L8 11 L1 11 Z
M5 4 L5 3 L10 3 L10 7 L8 7 L8 4 Z
M2 6 L12.6 5 L7 6 L7 10 L2 10 Z" />
</svg>
</button>
<button class="button-icon p-[2px] rounded hover:opacity-100 opacity-80"
aria-label="关闭"
@click.stop="onClose"
@mousedown.stop>
<svg width="11" height="11" viewBox="0 0 11 11" aria-hidden="true">
<line x1="2" y1="2" x2="9" y2="9" stroke="#e6efff" stroke-width="1"></line>
<line x1="2" y1="9" x2="9" y2="2" stroke="#e6efff" stroke-width="1"></line>
</svg>
</button>
</div>
</div>
<!-- 内容区域 -->
<div class="vs-content">
<!-- 这里是内容区域优先显示slot内容如果没有slot内容则显示接收到的外部内容 -->
<template v-if="$slots.default">
<slot></slot>
</template>
<!-- 直接显示接收到的外部TabPage内容不需要额外包装 -->
<template v-else-if="receivedContent.length > 0">
<TabPage
v-for="(item, index) in receivedContent"
:key="`received-tab-${index}`"
:id="item.tabPage.id"
:title="item.tabPage.title"
:panels="item.tabPage.panels"
:tabPosition="'bottom'"
@tabDragStart="() => {}"
@tabDragMove="() => {}"
@tabDragEnd="() => {}"
@maximize="onPanelMaximize"
/>
</template>
</div>
</div>
</template>
<script setup>
import { defineProps, computed, defineEmits, ref, onMounted, watch, defineExpose } from 'vue'
import TabPage from './TabPage.vue'
import Panel from './Panel.vue'
const props = defineProps({
id: { type: String, required: true },
title: { type: String, default: '面板区' },
resizable: { type: Boolean, default: true },
// 初始状态(支持中文值)
WindowState: { type: String, default: '正常' },
// 默认尺寸
width: { type: Number, default: 300 },
height: { type: Number, default: 250 },
// 控制标题栏显示
showTitleBar: { type: Boolean, default: true },
// 位置属性,可选
left: { type: Number, default: undefined },
top: { type: Number, default: undefined },
draggable: { type: Boolean, default: true }
})
// 本地状态
const localState = ref(props.WindowState)
// 保存原始位置和大小信息
const originalPosition = ref({
width: props.width,
height: props.height,
left: props.left,
top: props.top
})
// 保存最大化前的位置和大小,用于还原
const maximizedFromPosition = ref(null)
// 存储接收到的外部Area内容
const receivedContent = ref([])
// 拖拽相关状态
const isDragging = ref(false)
const dragStartPos = ref({ x: 0, y: 0 })
const areaStartPos = ref({ x: 0, y: 0 })
// 调整大小相关状态
const isResizing = ref(false)
const resizeStartPos = ref({ x: 0, y: 0 })
const resizeDirection = ref(null)
const resizeStartSize = ref({ width: 0, height: 0 })
const resizeStartAreaPos = ref({ left: 0, top: 0 })
// 父容器引用
const parentContainer = ref(null)
// 根据本地状态计算是否最大化
const isMaximized = computed(() => localState.value === '最大化' || localState.value === 'maximized')
// 监听props位置变化更新原始位置
watch(() => props.left, (newLeft) => {
if (newLeft !== undefined && newLeft !== originalPosition.value.left) {
originalPosition.value.left = newLeft
}
}, { immediate: true })
watch(() => props.top, (newTop) => {
if (newTop !== undefined && newTop !== originalPosition.value.top) {
originalPosition.value.top = newTop
}
}, { immediate: true })
// 监听WindowState变化同步更新localState
watch(() => props.WindowState, (newState) => {
if (newState !== localState.value) {
localState.value = newState
// 如果是从外部设置为最大化,保存当前位置以便还原
if (newState === '最大化' || newState === 'maximized') {
maximizedFromPosition.value = {
width: originalPosition.value.width,
height: originalPosition.value.height,
left: originalPosition.value.left,
top: originalPosition.value.top
}
} else if (maximizedFromPosition.value) {
// 如果是从外部设置为正常状态,恢复保存的位置
originalPosition.value = { ...maximizedFromPosition.value }
}
}
}, { immediate: true })
// 根据状态计算尺寸和位置样式
const areaStyle = computed(() => {
if (isMaximized.value) {
// 最大化时填充满父容器
return {
width: '100%',
height: '100%',
position: 'absolute',
top: 0,
left: 0,
zIndex: 100,
margin: 0,
padding: 0
}
}
// 非最大化状态:使用原始位置或默认居中
const style = {
width: `${originalPosition.value.width}px`,
height: `${originalPosition.value.height}px`
}
// 如果有明确的位置,则使用指定位置
if (originalPosition.value.left !== undefined) {
style.left = `${originalPosition.value.left}px`
}
if (originalPosition.value.top !== undefined) {
style.top = `${originalPosition.value.top}px`
}
return style
})
const emit = defineEmits(['close', 'update:windowState', 'update:position', 'dragover', 'dragleave', 'areaDragStart', 'areaDragMove', 'areaDragEnd', 'area-merged', 'toggleCollapse', 'maximize', 'close', 'toggleToolbar', 'dragStart', 'dragMove', 'dragEnd'])
// 处理Panel的最大化事件
const onPanelMaximize = (panelId) => {
console.log('🔸 Area接收最大化事件 - Panel ID:', panelId)
// 检查内容区是否只有一个Panel
const panelChildren = $slots.default ? $slots.default() : []
const isSinglePanel = panelChildren.length === 1
console.log('🔸 检查是否单Panel模式:', { panelChildren: panelChildren.length, isSinglePanel })
if (isSinglePanel) {
console.log('🔸 单Panel模式切换Area最大化状态')
onToggleMaximize()
} else {
console.log('🔸 非单Panel模式转发到父组件')
// 如果不是单Panel转发给父组件处理
emit('maximize', panelId)
}
}
// 处理拖拽悬停事件
const handleDragOver = (event) => {
emit('dragover', event, props.id)
}
// 处理拖拽离开事件
const handleDragLeave = (event) => {
emit('dragleave', event, props.id)
}
// 拖拽开始
const onDragStart = (e) => {
// 最大化状态下不允许拖拽
if (isMaximized.value) return
isDragging.value = true
dragStartPos.value = {
x: e.clientX,
y: e.clientY
}
areaStartPos.value = {
x: originalPosition.value.left || 0,
y: originalPosition.value.top || 0
}
// 通知父组件拖拽开始
emit('areaDragStart', {
areaId: props.id,
clientX: e.clientX,
clientY: e.clientY,
startLeft: originalPosition.value.left || 0,
startTop: originalPosition.value.top || 0
})
// 添加全局事件监听
document.addEventListener('mousemove', onDragMove)
document.addEventListener('mouseup', onDragEnd)
// 防止文本选择
e.preventDefault()
}
// 拖拽移动
const onDragMove = (e) => {
if (!isDragging.value) return
// 计算移动距离
const deltaX = e.clientX - dragStartPos.value.x
const deltaY = e.clientY - dragStartPos.value.y
// 计算新位置
let newLeft = areaStartPos.value.x + deltaX
let newTop = areaStartPos.value.y + deltaY
// 确保不超出父容器边界
if (parentContainer.value) {
const parentRect = parentContainer.value.getBoundingClientRect()
const areaWidth = originalPosition.value.width
const areaHeight = originalPosition.value.height
// 严格边界检查,确保元素完全在父容器内
newLeft = Math.max(0, Math.min(newLeft, parentRect.width - areaWidth))
newTop = Math.max(0, Math.min(newTop, parentRect.height - areaHeight))
}
// 更新位置
originalPosition.value.left = newLeft
originalPosition.value.top = newTop
// 通知父组件拖拽移动
emit('areaDragMove', {
areaId: props.id,
clientX: e.clientX,
clientY: e.clientY,
left: newLeft,
top: newTop
})
// 通知父组件位置变化
emit('update:position', { left: newLeft, top: newTop })
}
// 拖拽结束
const onDragEnd = () => {
// 通知父组件拖拽结束
emit('areaDragEnd', {
areaId: props.id,
left: originalPosition.value.left,
top: originalPosition.value.top
})
isDragging.value = false
// 移除全局事件监听
document.removeEventListener('mousemove', onDragMove)
document.removeEventListener('mouseup', onDragEnd)
}
// 调整大小开始
const onResizeStart = (direction, e) => {
if (isMaximized.value) return
isResizing.value = true
resizeDirection.value = direction
resizeStartPos.value = {
x: e.clientX,
y: e.clientY
}
resizeStartSize.value = {
width: originalPosition.value.width,
height: originalPosition.value.height
}
resizeStartAreaPos.value = {
left: originalPosition.value.left,
top: originalPosition.value.top
}
// 添加全局事件监听
document.addEventListener('mousemove', onResizeMove)
document.addEventListener('mouseup', onResizeEnd)
document.addEventListener('mouseleave', onResizeEnd)
// 防止文本选择
e.preventDefault()
e.stopPropagation()
}
// 调整大小移动
const onResizeMove = (e) => {
if (!isResizing.value) return
const deltaX = e.clientX - resizeStartPos.value.x
const deltaY = e.clientY - resizeStartPos.value.y
let newWidth = resizeStartSize.value.width
let newHeight = resizeStartSize.value.height
let newLeft = resizeStartAreaPos.value.left
let newTop = resizeStartAreaPos.value.top
// 根据方向调整大小
switch (resizeDirection.value) {
case 'nw':
newWidth = Math.max(200, resizeStartSize.value.width - deltaX)
newHeight = Math.max(150, resizeStartSize.value.height - deltaY)
newLeft = resizeStartAreaPos.value.left + deltaX
newTop = resizeStartAreaPos.value.top + deltaY
break
case 'ne':
newWidth = Math.max(200, resizeStartSize.value.width + deltaX)
newHeight = Math.max(150, resizeStartSize.value.height - deltaY)
newTop = resizeStartAreaPos.value.top + deltaY
break
case 'sw':
newWidth = Math.max(200, resizeStartSize.value.width - deltaX)
newHeight = Math.max(150, resizeStartSize.value.height + deltaY)
newLeft = resizeStartAreaPos.value.left + deltaX
break
case 'se':
newWidth = Math.max(200, resizeStartSize.value.width + deltaX)
newHeight = Math.max(150, resizeStartSize.value.height + deltaY)
break
case 'n':
// 拖动上边框时Area向上边扩展
newHeight = Math.max(150, resizeStartSize.value.height - deltaY)
// 当deltaY为负时鼠标向上移动增加高度并向上移动位置
newTop = resizeStartAreaPos.value.top + deltaY
break
case 'e':
newWidth = Math.max(200, resizeStartSize.value.width + deltaX)
break
case 's':
newHeight = Math.max(150, resizeStartSize.value.height + deltaY)
break
case 'w':
// 拖动左边框时Area向左边扩展
newWidth = Math.max(200, resizeStartSize.value.width - deltaX)
// 当deltaX为负时鼠标向左移动增加宽度并向左移动位置
newLeft = resizeStartAreaPos.value.left + deltaX
break
}
// 确保不超出父容器边界
if (parentContainer.value) {
const parentRect = parentContainer.value.getBoundingClientRect()
// 右边界检查
if (newLeft + newWidth > parentRect.width) {
newWidth = parentRect.width - newLeft
}
// 下边界检查
if (newTop + newHeight > parentRect.height) {
newHeight = parentRect.height - newTop
}
// 左边界检查
if (newLeft < 0) {
newWidth += newLeft
newLeft = 0
}
// 上边界检查
if (newTop < 0) {
newHeight += newTop
newTop = 0
}
}
// 更新位置和大小
originalPosition.value.width = newWidth
originalPosition.value.height = newHeight
originalPosition.value.left = newLeft
originalPosition.value.top = newTop
// 通知父组件位置变化
emit('update:position', {
left: newLeft,
top: newTop
})
// 防止文本选择
e.preventDefault()
}
// 调整大小结束
const onResizeEnd = () => {
isResizing.value = false
resizeDirection.value = null
// 移除全局事件监听
document.removeEventListener('mousemove', onResizeMove)
document.removeEventListener('mouseup', onResizeEnd)
document.removeEventListener('mouseleave', onResizeEnd)
}
const onToggleMaximize = () => {
const next = isMaximized.value ? '正常' : '最大化'
if (!isMaximized.value) {
// 切换到最大化状态前,保存当前位置和大小
maximizedFromPosition.value = {
width: originalPosition.value.width,
height: originalPosition.value.height,
left: originalPosition.value.left,
top: originalPosition.value.top
}
} else if (maximizedFromPosition.value) {
// 从最大化状态还原时,恢复到保存的位置和大小
originalPosition.value = { ...maximizedFromPosition.value }
// 通知父组件位置变化
emit('update:position', {
left: originalPosition.value.left,
top: originalPosition.value.top
})
}
localState.value = next
emit('update:WindowState', next)
}
const onClose = () => emit('close')
// 组件挂载后获取父容器引用并初始化位置
onMounted(() => {
parentContainer.value = document.querySelector('.dock-layout') || window
// 如果没有指定left或top自动居中定位
if (originalPosition.value.left === undefined || originalPosition.value.top === undefined) {
let parentWidth, parentHeight
if (parentContainer.value === window) {
parentWidth = window.innerWidth
parentHeight = window.innerHeight
} else if (parentContainer.value) {
const parentRect = parentContainer.value.getBoundingClientRect()
parentWidth = parentRect.width
parentHeight = parentRect.height
} else {
// 默认值,防止出错
parentWidth = 800
parentHeight = 600
}
const areaWidth = originalPosition.value.width || 300
const areaHeight = originalPosition.value.height || 250
// 计算居中位置
originalPosition.value.left = Math.floor((parentWidth - areaWidth) / 2)
originalPosition.value.top = Math.floor((parentHeight - areaHeight) / 2)
// 通知父组件位置变化
emit('update:position', {
left: originalPosition.value.left,
top: originalPosition.value.top
})
}
})
// 合并Area内容的方法只保留合并逻辑
const mergeAreaContent = (sourceArea) => {
console.log(`[Area] ${props.id} 接收到Area合并请求:`, sourceArea)
if (!sourceArea) {
console.warn('[Area] 源Area为空无法合并内容')
return false
}
try {
const isEmpty = receivedContent.value.length === 0
if (isEmpty) {
// 4.2.1 如果目标Area内容区为空将源Area内容区的子组件添加到目标Area内容区
console.log('[Area] 目标Area为空添加源Area的子组件')
// 处理源Area的所有tabPages
if (sourceArea.tabPages && sourceArea.tabPages.length > 0) {
sourceArea.tabPages.forEach((tabPage, tabIndex) => {
const newTabPageId = `merged-tabpage-${Date.now()}-${tabIndex}`
const newPanels = (tabPage.panels || []).map((panel, panelIndex) => {
// 保持原有Panel ID不变确保Vue响应式和状态稳定性
console.log(`[Area] 添加Panel: ${panel.id}`)
return {
...panel,
maximized: true
}
})
receivedContent.value.push({
id: `received-${newTabPageId}`,
title: tabPage.title || `标签页${tabIndex + 1}`,
tabPage: {
...tabPage,
id: newTabPageId,
panels: newPanels
},
panels: newPanels
})
console.log(`[Area] 成功添加TabPage: ${tabPage.title} (${newPanels.length} 个Panel)`)
})
}
// 触发事件通知父组件将源Area保存到隐藏列表
emit('area-merged', {
sourceArea: sourceArea,
targetAreaId: props.id,
targetAreaHasContent: false, // 目标Area为空
operation: 'add-children',
addedTabPages: receivedContent.value
})
return true
} else {
// 4.2.2 如果目标Area内容区已包含TabPage将源Area的每个TabPage添加到目标Area的TabPage中
console.log('[Area] 目标Area已有TabPage合并TabPage标签页')
// 获取第一个现有的TabPage作为合并目标
const existingTabPage = receivedContent.value[0]
if (!existingTabPage) {
console.error('[Area] 现有TabPage不存在')
return false
}
// 处理源Area的所有tabPages
if (sourceArea.tabPages && sourceArea.tabPages.length > 0) {
sourceArea.tabPages.forEach((sourceTabPage, tabIndex) => {
if (sourceTabPage && sourceTabPage.panels) {
// 保持原有Panel ID不变避免Vue组件重新创建和状态丢失
const newPanels = sourceTabPage.panels.map((panel, panelIndex) => {
console.log(`[Area] 合并Panel到现有TabPage: ${panel.id}`)
return {
...panel,
maximized: true
}
})
// 将新的Panel添加到现有TabPage
existingTabPage.tabPage.panels.push(...newPanels)
// existingTabPage.panels 是旧引用,保持结构一致性但避免重复添加
console.log(`[Area] 成功合并 ${newPanels.length} 个Panel到现有TabPage`)
}
})
}
// 触发事件通知父组件将源Area及其TabPage组件保存到隐藏列表
emit('area-merged', {
sourceArea: sourceArea,
targetAreaId: props.id,
targetAreaHasContent: true, // 目标Area已有内容
operation: 'merge-tabpages',
sourceTabPages: sourceArea.tabPages || []
})
console.log(`[Area] 合并完成现有TabPage共有 ${existingTabPage.tabPage.panels.length} 个Panel`)
return true
}
} catch (error) {
console.error('[Area] 合并Area内容时出错:', error)
return false
}
}
// 暴露方法给父组件调用
defineExpose({
mergeAreaContent, // 合并Area内容的方法
id: props.id,
title: props.title,
isMaximized: isMaximized.value
})
</script>
<style scoped>
:root { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
/* 颜色(贴近 VS 蓝色主题) */
.vs-area {
--vs-blue-top: #4f72b3;
--vs-blue-bottom: #3c5a99;
--vs-blue-deep: #2c3e7a;
--vs-tab-blue: #4869a8;
--vs-border: #c7d2ea;
--vs-bg: #f5f7fb;
--vs-panel: #ffffff;
--vs-muted: #6b7aa9;
--vs-accent: #f0a000;
}
/* 容器 */
.vs-area-wrapper {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
position: relative;
}
.vs-area {
display: flex;
flex-direction: column;
background: var(--vs-bg);
border: 1px solid var(--vs-border);
min-width: 300px;
min-height: 250px;
}
/* 正常状态样式 */
.vs-area.is-normal {
position: absolute;
z-index: 10;
}
/* 最大化状态样式 */
.vs-area.is-maximized {
width: 100% !important;
height: 100% !important;
position: absolute !important;
top: 0 !important;
left: 0 !important;
z-index: 100 !important;
margin: 0;
padding: 0;
}
/* 标题栏 */
.vs-title-bar {
height: 28px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10px;
color: #ffffff;
background: linear-gradient(to bottom, var(--vs-blue-top), var(--vs-blue-bottom));
border-bottom: 1px solid var(--vs-blue-deep);
z-index: 15; /* 确保标题栏在调整手柄之上 */
position: relative; /* 为z-index生效 */
}
.vs-title-left { display: flex; align-items: center; gap: 6px; }
.vs-app-icon { font-size: 12px; opacity: 0.9; }
.vs-title-text { font-size: 13px; }
.vs-title-right { display: flex; align-items: center; gap: 6px; }
.vs-btn {
width: 22px; height: 18px; line-height: 18px;
color: #ffffff; background: transparent; border: none; padding: 0; cursor: default;
position: relative; /* 确保按钮层级生效 */
z-index: 16; /* 确保按钮在最上层 */
}
.vs-btn:hover { background: rgba(255,255,255,0.12); }
.vs-close:hover { background: #e81123; }
/* 面板标题行(左右) */
.vs-pane-headers {
display: flex; align-items: center;
height: 26px; background: var(--vs-tab-blue);
border-bottom: 1px solid var(--vs-blue-deep);
color: #eaf1ff;
padding: 0 6px;
}
.vs-pane-header {
display: flex; align-items: center; gap: 8px;
height: 100%; padding: 0 10px;
}
.vs-pane-sep {
width: 1px; height: 18px; background: rgba(255,255,255,0.3);
margin: 0 8px;
}
.hdr-text { font-size: 12px; }
.hdr-icon { font-size: 10px; opacity: 0.9; }
.hdr-close { font-size: 12px; opacity: 0.9; }
.hdr-close:hover { opacity: 1; }
/* 内容区域 */
.vs-content {
display: flex;
flex: 1;
overflow: visible;
background-color: #C7D3FF;
position: relative;
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
}
/* 调整大小的手柄样式 */
.resize-handle {
position: absolute;
z-index: 14; /* 调整手柄应该在标题栏之下,但在正常区域内 */
background: transparent;
pointer-events: auto;
}
/* 四个角 */
.resize-handle-nw {
width: 12px;
height: 12px;
top: -6px;
left: -6px;
cursor: nwse-resize;
}
.resize-handle-ne {
width: 12px;
height: 12px;
top: -6px;
right: -6px;
cursor: nesw-resize;
}
.resize-handle-sw {
width: 12px;
height: 12px;
bottom: -6px;
left: -6px;
cursor: nesw-resize;
}
.resize-handle-se {
width: 12px;
height: 12px;
bottom: -6px;
right: -6px;
cursor: nwse-resize;
}
/* 四条边 */
.resize-handle-n {
height: 12px;
top: -6px;
left: 12px;
right: 12px;
cursor: ns-resize;
}
.resize-handle-e {
width: 12px;
right: -6px;
top: 12px;
bottom: 12px;
cursor: ew-resize;
}
.resize-handle-s {
height: 12px;
bottom: -6px;
left: 12px;
right: 12px;
cursor: ns-resize;
}
.resize-handle-w {
width: 12px;
left: -6px;
top: 12px;
bottom: 12px;
cursor: ew-resize;
}
/* 鼠标悬停在边框上时的样式提示 */
.vs-area.is-normal:not(:hover) .resize-handle {
opacity: 0;
}
.vs-area.is-normal:hover .resize-handle {
opacity: 0.5;
}
/* 左侧输出 */
.vs-left { flex: 1; background: var(--vs-panel); display: flex; }
.left-blank { flex: 1; background: #eef1f9; border-right: 1px solid var(--vs-border); }
/* 中间分割线 */
.vs-divider { width: 1px; background: var(--vs-border); }
/* 右侧 Git 更改 */
.vs-right { flex: 1; background: #f5f7fb; padding: 0; }
.sec-text { margin-bottom: 8px; }
.vs-card {
display: inline-flex; align-items: center; gap: 8px;
background: #fff; border: 1px solid var(--vs-border);
padding: 6px 8px; border-radius: 2px; margin-bottom: 10px;
box-shadow: 0 1px 0 rgba(0,0,0,0.04);
}
.card-icon { color: var(--vs-accent); }
.card-text { color: #000; }
.hint-text { color: #666; }
/* 滚动条(接近 VS */
:deep(::-webkit-scrollbar) { width: 12px; height: 12px; }
:deep(::-webkit-scrollbar-track) { background: var(--vs-bg); border-left: 1px solid var(--vs-border); }
:deep(::-webkit-scrollbar-thumb) {
background: linear-gradient(to bottom, #d0d6ea, #c0c7e0);
border: 1px solid #b0b6d6; border-radius: 6px;
}
:deep(::-webkit-scrollbar-thumb:hover) { background: linear-gradient(to bottom, #c1c7e2, #b2b8d9); }
:deep(*) { box-sizing: border-box; }
.vs-area.is-maximized {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 100;
}
.vs-icon-stage { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: transparent; overflow: auto; }
.vs-app-icon--x200 { width: 2800px; height: 2800px; }
.vs-app-icon { width: 14px; height: 14px; display: inline-block; background: transparent; opacity: 0.95; }
.vs-icon { width: 100%; height: 100%; shape-rendering: crispEdges; }
.vs-app-icon svg { display: block; }
/* 外层包裹,确保最大化时填充父容器,非最大化时居中 */
.vs-area-wrapper {
width: 100%;
height: 100%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
/* 最大化状态时wrapper不居中 */
.vs-area-wrapper.is-maximized {
align-items: stretch;
justify-content: stretch;
}
/* 接收到的外部内容样式 */
.received-content {
width: 100%;
height: 100%;
overflow: auto;
background: #f8f9ff;
border: 1px solid #e0e6f0;
border-radius: 4px;
}
.received-item {
background: white;
border: 1px solid #d0d7e2;
border-radius: 6px;
margin: 8px;
padding: 12px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: box-shadow 0.2s ease;
}
.received-item:hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.received-title {
font-size: 13px;
font-weight: 600;
color: #2c3e7a;
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px solid #e8edf7;
}
.received-body {
min-height: 60px;
}
/* TabPage和Panel容器样式 */
.tab-page-container,
.area-container {
width: 100%;
height: 100%;
border: 1px dashed #c7d2ea;
border-radius: 4px;
padding: 12px;
background: #fafbff;
color: #6b7aa9;
text-align: center;
font-style: italic;
}
</style>