建立面板和区域组件

This commit is contained in:
JoyD
2025-10-31 21:58:33 +08:00
parent b36db9cd49
commit 9167a1f578
3 changed files with 285 additions and 263 deletions

View File

@@ -0,0 +1,145 @@
<template>
<div class="vs-area select-none">
<!-- 顶部标题栏 -->
<div class="vs-title-bar">
<div class="vs-title-left">
<span class="vs-app-icon"></span>
<span class="vs-title-text">{{ title || 'GlobalHook_Test' }}</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="最大化">
<svg 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>
<rect x="3" y="3" width="5" height="1" fill="#b8c6ff"></rect>
<rect x="1" y="3" width="8.5" height="6.5" fill="#435d9c"></rect>
</svg>
</button>
<button class="button-icon p-[2px] rounded hover:opacity-100 opacity-80" aria-label="关闭">
<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">
<div class="vs-right"></div>
</div>
</div>
</template>
<script setup>
import { defineProps } from 'vue'
const props = defineProps({
id: { type: String, required: true },
title: { type: String, default: 'GlobalHook_Test' },
resizable: { type: Boolean, default: true }
})
</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 {
display: flex;
flex-direction: column;
background: var(--vs-bg);
border: 1px solid var(--vs-border);
min-width: 400px;
min-height: 200px;
}
/* 标题栏 */
.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);
}
.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;
}
.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: hidden; }
/* 左侧输出 */
.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; }
</style>

View File

@@ -1,263 +0,0 @@
import { ref, computed, defineComponent, onMounted, onUnmounted, watch, nextTick } from 'vue';
/**
* 浮动面板组件
* 提供可拖拽、最大化、最小化、折叠和工具栏扩展功能的窗口组件
*/
export default defineComponent({
name: 'Panel',
props: {
panel: {
type: Object,
required: true,
validator: (value) => {
return value && typeof value === 'object' && 'id' in value && 'title' in value;
}
},
hostRef: {
type: Object,
default: null
}
},
emits: ['close', 'toggleCollapse', 'toggleToolbar', 'maximize'],
setup(props, { emit }) {
// 响应式状态管理
const isDragging = ref(false);
const dragStartPos = ref({ x: 0, y: 0 });
const panelPosition = ref({
x: props.panel.x || 100,
y: props.panel.y || 100
});
const panelSize = ref({
width: props.panel.width || 300,
height: props.panel.height || 180
});
// 同步面板属性变化
const syncPanelProperties = () => {
if (props.panel.x !== undefined) panelPosition.value.x = props.panel.x;
if (props.panel.y !== undefined) panelPosition.value.y = props.panel.y;
if (props.panel.width !== undefined) panelSize.value.width = props.panel.width;
if (props.panel.height !== undefined) panelSize.value.height = props.panel.height;
};
// 计算面板样式
const panelStyle = computed(() => {
// 直接使用props.panel中的值确保与父组件状态同步
const posX = props.panel.x || 100;
const posY = props.panel.y || 100;
const width = props.panel.width || 300;
const height = props.panel.height || 180;
// 始终创建新对象避免Vue响应式系统无法检测变化
const style = {
position: 'absolute',
top: `${posY}px`,
left: `${posX}px`,
width: `${width}px`,
height: `${height}px`,
backgroundColor: 'white',
border: '1px solid #435d9c', // 更深的边框颜色以增强可见性
borderRadius: '4px',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.15)',
overflow: 'hidden',
zIndex: 10,
display: 'flex',
flexDirection: 'column'
};
// 最大化状态
if (props.panel.maximized) {
// 获取host元素 - 直接使用hostRef或者hostRef.value如果是ref
const host = props.hostRef && typeof props.hostRef.getBoundingClientRect === 'function'
? props.hostRef
: props.hostRef?.value;
const rect = host?.getBoundingClientRect();
if (rect) {
style.top = '0px';
style.left = '0px';
style.width = `${rect.width - 2}px`;
style.height = `${rect.height - 2}px`;
}
}
return style;
});
// 处理标题栏拖拽
const handleTitleBarMouseDown = (event) => {
if (props.panel.maximized) return;
isDragging.value = true;
dragStartPos.value = {
x: event.clientX - panelPosition.value.x,
y: event.clientY - panelPosition.value.y
};
event.preventDefault();
};
// 处理窗口关闭
const handleClose = () => {
emit('close', props.panel.id);
};
// 处理折叠/展开
const handleToggleCollapse = () => {
emit('toggleCollapse', props.panel.id);
};
// 处理工具栏展开/收起
const handleToggleToolbar = () => {
emit('toggleToolbar', props.panel.id);
};
// 处理最大化/还原
const handleMaximize = () => {
emit('maximize', props.panel.id);
};
// 鼠标移动事件处理
const handleMouseMove = (event) => {
if (isDragging.value && !props.panel.maximized) {
panelPosition.value = {
x: event.clientX - dragStartPos.value.x,
y: event.clientY - dragStartPos.value.y
};
}
};
// 鼠标松开事件处理
const handleMouseUp = () => {
isDragging.value = false;
};
// 监听面板属性变化
watch(() => props.panel, (newPanel) => {
if (newPanel) {
syncPanelProperties();
}
}, { deep: true, immediate: true });
// 生命周期钩子
onMounted(async () => {
// 确保DOM更新后再同步状态
await nextTick();
// 同步外部面板状态
syncPanelProperties();
// 添加全局事件监听
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
});
onUnmounted(() => {
// 清理全局事件监听
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
});
return {
isDragging,
panelStyle,
handleTitleBarMouseDown,
handleClose,
handleToggleCollapse,
handleToggleToolbar,
handleMaximize
};
},
template: `
<div class="panel-container" :style="panelStyle">
<div class="flex flex-col h-full">
<!-- 标题栏 -->
<div
class="h-6 bg-[#435d9c] text-white px-2 flex items-center justify-between select-none"
@mousedown="handleTitleBarMouseDown"
>
<div class="flex items-center">
<span class="text-xs">{{ panel.title }}</span>
</div>
<div class="flex items-center gap-0.5">
<!-- 折叠按钮 -->
<button
class="p-[2px] rounded hover:opacity-100 opacity-80 flex items-center justify-center w-5 h-5"
@click.stop="handleToggleCollapse"
aria-label="折叠/展开"
>
<div class="icon-triangle-down"></div>
</button>
<!-- 最大化按钮 -->
<button
class="p-[2px] rounded hover:opacity-100 opacity-80 flex items-center justify-center w-5 h-5"
@click.stop="handleMaximize"
aria-label="最大化"
>
<div class="icon-square"></div>
</button>
<!-- 关闭按钮 -->
<button
class="p-[2px] rounded hover:opacity-100 opacity-80 flex items-center justify-center w-5 h-5"
@click.stop="handleClose"
aria-label="关闭"
>
<div class="icon-x"></div>
</button>
</div>
</div>
<!-- 工具栏 -->
<div class="h-6 bg-[#d5e2f6] text-[#2c3e7a] px-2 flex items-center justify-between border-b border-[#c7d2ea]">
<div class="flex items-center gap-2">
<span class="text-xs">工具栏</span>
<button v-if="panel.toolbarExpanded" class="px-2 py-0.5 text-xs bg-white/60 rounded hover:bg-white">示例按钮</button>
</div>
<button
class="px-2 py-0.5 text-xs rounded hover:bg-white/40"
@click.stop="handleToggleToolbar"
aria-label="展开工具栏"
>
<span v-if="panel.toolbarExpanded" class="text-xs">-</span>
<span v-else class="text-xs">+</span>
</button>
</div>
<!-- 内容区 -->
<div class="bg-[#f5f7fb] flex-1 p-2" v-if="!panel.collapsed">
<slot name="content"></slot>
</div>
</div>
</div>
`,
// 样式应该放在模板外部作为组件的style选项
style: `
/* 向下小三角 */
.panel-container .icon-triangle-down {
width: 0; height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 6px solid #cbd6ff;
}
/* 最大化图标 */
.panel-container .icon-square {
position: relative;
width: 11px; height: 11px;
background: linear-gradient(180deg, #cbd6ff 0%, #b9c8ff 100%);
border: 1px solid #b8c6ff;
box-sizing: border-box;
}
/* X图标 */
.panel-container .icon-x {
position: relative; width: 11px; height: 11px;
}
.panel-container .icon-x::before, .panel-container .icon-x::after {
content: ''; position: absolute; left: 5px; top: 0; width: 1px; height: 11px; background: #e6efff;
}
.panel-container .icon-x::before { transform: rotate(45deg); }
.panel-container .icon-x::after { transform: rotate(-45deg); }
`
});

View File

@@ -0,0 +1,140 @@
<template>
<div class="panel absolute bg-white shadow rounded border overflow-hidden"
:style="{ top: y + 'px', left: x + 'px', width: width + 'px', height: height + 'px' }">
<div class="flex flex-col h-full">
<!-- 标题栏 -->
<div class="title-bar h-6 bg-[#435d9c] text-white px-2 flex items-center justify-between select-none">
<div class="flex items-center">
<span class="text-xs">{{ title }}</span>
</div>
<div class="title-bar-buttons flex items-center gap-0.5">
<button class="button-icon p-[2px] rounded hover:opacity-100 opacity-80"
@click="onToggleCollapse"
aria-label="折叠/展开">
<!-- 向下小三角使用内联SVG避免样式作用域问题 -->
<svg width="11" height="11" viewBox="0 0 11 11" aria-hidden="true">
<polygon points="5.5,8 2,3.5 9,3.5" fill="#cbd6ff" />
</svg>
</button>
<button class="button-icon p-[2px] rounded hover:opacity-100 opacity-80"
@click="onMaximize"
aria-label="最大化">
<!-- 最大化图标仅外框1px + 两行填充 -->
<svg 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>
</button>
<button class="button-icon p-[2px] rounded hover:opacity-100 opacity-80"
@click="onClose"
aria-label="关闭">
<!-- 关闭图标X内联SVG确保1px线条 -->
<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 x1="2" y1="9" x2="9" y2="2" stroke="#e6efff" stroke-width="1" />
</svg>
</button>
</div>
</div>
<!-- 工具栏位于标题栏下方右侧扩展钮 -->
<div class="toolbar h-6 bg-[#d5e2f6] text-[#2c3e7a] px-2 flex items-center justify-between border-b border-[#c7d2ea]">
<div class="toolbar-left flex items-center gap-2">
<span class="text-xs">工具栏</span>
<button v-if="toolbarExpanded" class="toolbar-button px-2 py-0.5 text-xs bg-white/60 rounded hover:bg-white">示例按钮</button>
</div>
<button class="toolbar-toggle px-2 py-0.5 text-xs rounded hover:bg-white/40"
@click="onToggleToolbar"
aria-label="展开工具栏">
<i class="fa-solid" :class="toolbarExpanded ? 'fa-angles-left' : 'fa-angles-right'"></i>
</button>
</div>
<!-- 内容区可折叠 -->
<div class="content-area bg-[#f5f7fb] flex-1" v-show="!collapsed"></div>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
// 定义组件属性
const props = defineProps({
id: {
type: String,
required: true
},
title: {
type: String,
default: ''
},
x: {
type: Number,
default: 0
},
y: {
type: Number,
default: 0
},
width: {
type: Number,
default: 300
},
height: {
type: Number,
default: 180
},
collapsed: {
type: Boolean,
default: false
},
toolbarExpanded: {
type: Boolean,
default: false
}
});
// 定义事件
const emit = defineEmits(['toggleCollapse', 'maximize', 'close', 'toggleToolbar']);
// 事件处理函数
const onToggleCollapse = () => {
emit('toggleCollapse', props.id);
};
const onMaximize = () => {
emit('maximize', props.id);
};
const onClose = () => {
emit('close', props.id);
};
const onToggleToolbar = () => {
emit('toggleToolbar', props.id);
};
</script>
<style scoped>
/* 面板基础样式 */
.panel {
border: 1px solid #c7d2ea;
}
/* 图标样式优化 */
.icon-square-svg {
/* 优化SVG渲染避免1px边框显示过粗的问题 */
shape-rendering: crispEdges;
}
/* 禁用可能存在的旧伪元素样式 */
:deep(.icon-square::before),
:deep(.icon-square::after) {
content: none !important;
display: none !important;
border: 0 !important;
}
</style>