Files
JoyD/AutoRobot/Windows/Robot/Web/src/DockLayout/Area.vue
2025-12-29 16:52:28 +08:00

1014 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
ref="areaRef"
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="shouldShowTitleBar" 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">
<!-- 使用Render组件渲染children配置 -->
<div v-for="child in Array.isArray(children) ? children : [children]" :key="child.id" style="width: 100%; height: 100%;">
<Render
:type="child.type"
:config="child"
/>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps, computed, ref, onMounted, onUnmounted, watch, defineExpose } from 'vue'
import { emitEvent, EVENT_TYPES } from './eventBus'
import TabPage from './TabPage.vue'
import Panel from './Panel.vue'
import Render from './Render.vue'
import { zIndexManager, Z_INDEX_LAYERS } from './dockLayers'
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 },
// 子组件配置
children: {
type: [Array, Object],
default: () => []
}
})
// 使用全局事件总线和拖拽管理器
// 本地状态
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内容改为通过children配置管理
// 组件引用
const areaRef = ref(null)
// 不再需要获取插槽改为使用props.children
// 计算属性:是否显示标题栏
const shouldShowTitleBar = computed(() => {
// 基础条件props.showTitleBar为true
if (!props.showTitleBar) return false;
// 检查children配置
if (props.children) {
const childrenArray = Array.isArray(props.children) ? props.children : [props.children];
// 如果没有children显示标题栏
if (childrenArray.length === 0) return true;
// 如果children不是TabPage显示标题栏
const firstChild = childrenArray[0];
if (firstChild.type !== 'TabPage') return true;
// 检查TabPage的children
const tabPageChildren = firstChild.children;
if (!tabPageChildren) return true;
const tabPageChildrenArray = Array.isArray(tabPageChildren) ? tabPageChildren : [tabPageChildren];
// 如果TabPage包含多个Panel显示标题栏
if (tabPageChildrenArray.length !== 1) return true;
// 如果TabPage只包含一个Panel不显示标题栏
return false;
}
// 默认显示标题栏
return true;
});
// 拖拽相关状态
const isDragging = ref(false)
const dragStartPos = ref({ x: 0, y: 0 })
const areaStartPos = ref({ x: 0, y: 0 })
const currentDragId = ref(null)
// 调整大小相关状态
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) {
// 最大化时填充满父容器使用更高的z-index确保在最顶层
return {
width: '100%',
height: '100%',
position: 'absolute',
top: 0,
left: 0,
zIndex: Z_INDEX_LAYERS.CONTENT_ACTIVE, // 使用统一的z-index层级
margin: 0,
padding: 0
}
}
// 非最大化状态:使用原始位置或默认居中
const style = {
width: `${originalPosition.value.width}px`,
height: `${originalPosition.value.height}px`,
zIndex: Z_INDEX_LAYERS.CONTENT // 使用统一的z-index层级
}
// 如果有明确的位置,则使用指定位置
if (originalPosition.value.left !== undefined) {
style.left = `${originalPosition.value.left}px`
}
if (originalPosition.value.top !== undefined) {
style.top = `${originalPosition.value.top}px`
}
return style
})
// 使用事件总线替代直接emit
// 处理Panel的最大化事件
const onPanelMaximize = (panelId) => {
// // console.log('🔸 Area接收最大化事件 - Panel ID:', panelId)
// 检查内容区是否只有一个Panel
let isSinglePanel = false
// 检查children配置
if (props.children) {
const childrenArray = Array.isArray(props.children) ? props.children : [props.children]
// 查找TabPage组件
const tabPages = childrenArray.filter(child => child.type === 'TabPage')
if (tabPages.length === 1) {
// 检查TabPage的children
const tabPageChildren = tabPages[0].children
if (tabPageChildren) {
const tabPageChildrenArray = Array.isArray(tabPageChildren) ? tabPageChildren : [tabPageChildren]
// 如果TabPage只有一个Panel认为是单Panel模式
isSinglePanel = tabPageChildrenArray.length === 1
}
}
}
// // console.log('🔸 检查是否单Panel模式:', { tabPages: tabPages.length, isSinglePanel })
if (isSinglePanel) {
// // console.log('🔸 单Panel模式切换Area最大化状态')
onToggleMaximize()
} else {
// // console.log('🔸 非单Panel模式转发到父组件')
// 如果不是单Panel转发给父组件处理
emitEvent(EVENT_TYPES.PANEL_MAXIMIZE, { panelId, areaId: props.id }, {
source: { component: 'Area', areaId: props.id }
})
}
}
// 处理拖拽悬停事件
const handleDragOver = (event) => {
emitEvent(EVENT_TYPES.AREA_DRAG_OVER, { event, areaId: props.id }, {
source: { component: 'Area', areaId: props.id }
})
}
// 处理拖拽离开事件
const handleDragLeave = (event) => {
emitEvent(EVENT_TYPES.AREA_DRAG_LEAVE, { event, areaId: props.id }, {
source: { component: 'Area', areaId: 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
}
// 生成统一的 dragId
currentDragId.value = `area_${props.id}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`
// 使用事件总线通知拖拽开始,包含 dragId 和标准化数据格式
emitEvent(EVENT_TYPES.AREA_DRAG_START, {
dragId: currentDragId.value,
areaId: props.id,
position: { x: e.clientX, y: e.clientY },
startLeft: originalPosition.value.left || 0,
startTop: originalPosition.value.top || 0,
timestamp: Date.now()
}, {
source: { component: 'Area', areaId: props.id, dragId: currentDragId.value }
})
// 防止文本选择
e.preventDefault()
}
// 拖拽移动
const onDragMove = (e) => {
if (!isDragging.value || !currentDragId.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
// 使用事件总线通知拖拽移动,包含 dragId
emitEvent(EVENT_TYPES.AREA_DRAG_MOVE, {
dragId: currentDragId.value,
areaId: props.id,
position: { x: e.clientX, y: e.clientY },
left: newLeft,
top: newTop,
timestamp: Date.now()
}, {
source: { component: 'Area', areaId: props.id, dragId: currentDragId.value }
})
// 使用事件总线通知位置变化,包含 dragId
emitEvent(EVENT_TYPES.AREA_POSITION_UPDATE, {
dragId: currentDragId.value,
areaId: props.id,
left: newLeft,
top: newTop
}, {
source: { component: 'Area', areaId: props.id, dragId: currentDragId.value }
})
}
// 拖拽结束
const onDragEnd = () => {
if (!currentDragId.value) return
// 使用事件总线通知拖拽结束,包含 dragId 和标准化数据格式
emitEvent(EVENT_TYPES.AREA_DRAG_END, {
dragId: currentDragId.value,
areaId: props.id,
finalPosition: {
x: originalPosition.value.left,
y: originalPosition.value.top
},
left: originalPosition.value.left,
top: originalPosition.value.top,
timestamp: Date.now()
}, {
source: { component: 'Area', areaId: props.id, dragId: currentDragId.value }
})
isDragging.value = false
currentDragId.value = null
}
// 调整大小开始
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
}
// 防止文本选择
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
// 使用事件总线通知位置变化
emitEvent(EVENT_TYPES.AREA_POSITION_UPDATE, {
areaId: props.id,
left: newLeft,
top: newTop
}, {
source: { component: 'Area', areaId: props.id }
})
// 防止文本选择
e.preventDefault()
}
// 调整大小结束
const onResizeEnd = () => {
isResizing.value = false
resizeDirection.value = null
}
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 }
// 通知父组件位置变化
emitEvent(EVENT_TYPES.AREA_POSITION_UPDATE, {
areaId: props.id,
left: originalPosition.value.left,
top: originalPosition.value.top
}, {
source: { component: 'Area', areaId: props.id }
})
}
localState.value = next
emitEvent(EVENT_TYPES.WINDOW_STATE_CHANGE, {
areaId: props.id,
state: next
}, {
source: { component: 'Area', areaId: props.id }
})
}
const onClose = () => emitEvent(EVENT_TYPES.PANEL_CLOSE_REQUEST, {
areaId: props.id
}, {
source: { component: 'Area', areaId: props.id }
})
// 组件挂载后获取父容器引用并初始化位置
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)
// 通知父组件位置变化
emitEvent(EVENT_TYPES.AREA_POSITION_UPDATE, {
areaId: props.id,
left: originalPosition.value.left,
top: originalPosition.value.top
}, {
source: { component: 'Area', areaId: props.id }
})
}
})
// 组件卸载时清理状态
onUnmounted(() => {
// 清理拖拽和调整大小状态
isDragging.value = false
currentDragId.value = null
isResizing.value = false
resizeDirection.value = null
})
// 处理Area合并内容
const mergeAreaContent = (sourceArea) => {
// console.log(`[Area] ${props.id} 接收到Area合并请求:`, sourceArea)
if (!sourceArea) {
// console.warn('[Area] 源Area为空无法合并内容')
return false
}
try {
// 发送合并请求事件,让父组件处理配置修改
emitEvent(EVENT_TYPES.AREA_MERGE_REQUEST, {
sourceArea: sourceArea,
targetAreaId: props.id
}, {
source: { component: 'Area', areaId: props.id }
})
// 触发事件通知父组件将源Area保存到隐藏列表
emitEvent(EVENT_TYPES.AREA_MERGED, {
sourceArea: sourceArea,
targetAreaId: props.id,
targetAreaHasContent: false, // 简化处理,由父组件判断
operation: 'merge-children',
sourceTabPages: sourceArea.children ? [sourceArea.children] : []
}, {
source: { component: 'Area', areaId: props.id }
})
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>