Merge branch 'main' of http://47.109.191.115:3000/ZengQingming/JoyD
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
7. AutoRobot\Windows\Robot\Web:这是AutoRobot项目的客户端的子项目目录,编译:npm run build
|
||||
8. 在执行git或svn操作时,需要切换代码页到 chcp 936, 执行其它命令时,请先切换回默认的代码页 chcp 65001
|
||||
9. Maven 仓库地址是 http://geek.cdjkt.com:30003/repository/maven-public,isAllowInsecureProtocol = true
|
||||
10. go 仓库的地址是 http://geek.cdjkt.com:30003/repository/go-public
|
||||
10. go 仓库的地址是 http://47.111.181.23:8081/repository/go-public/
|
||||
11. Nexus 仓库地址是:http://geek.cdjkt.com:30003,用户名:admin,密码:Access2cRte
|
||||
12. 当前终端是powershell,请注意命令的格式
|
||||
13. 当文件被修改时,需要暂停执行,待我接受或者拒绝修改后,再继续执行
|
||||
|
||||
213
AutoRobot/Windows/Robot/Web/src/DockLayout/Area.vue
Normal file
213
AutoRobot/Windows/Robot/Web/src/DockLayout/Area.vue
Normal file
@@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<div class="vs-area-wrapper">
|
||||
<div class="vs-area select-none" :class="{ 'is-maximized': isMaximized }" :style="areaStyle">
|
||||
<!-- 顶部标题栏 -->
|
||||
<div v-if="showTitleBar" class="vs-title-bar">
|
||||
<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="onToggleMaximize">
|
||||
<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="onClose">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, computed, defineEmits, ref } from '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 }
|
||||
})
|
||||
|
||||
// 本地状态:不再向父组件发事件,内部维护最大化/还原
|
||||
const localState = ref(props.WindowState)
|
||||
|
||||
// 根据本地状态计算是否最大化
|
||||
const isMaximized = computed(() => localState.value === '最大化' || localState.value === 'maximized')
|
||||
|
||||
// 新增:根据状态计算尺寸样式
|
||||
const areaStyle = computed(() => {
|
||||
if (isMaximized.value) {
|
||||
return { width: '100%', height: '100%' }
|
||||
}
|
||||
return { width: `${props.width}px`, height: `${props.height}px` }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const onToggleMaximize = () => {
|
||||
const next = isMaximized.value ? '正常' : '最大化'
|
||||
// 直接更新本地状态,不再 emit 到父组件
|
||||
localState.value = next
|
||||
}
|
||||
|
||||
const onClose = () => emit('close')
|
||||
</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: 300px;
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
/* 标题栏 */
|
||||
.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; }
|
||||
.vs-area.is-maximized { width: 100%; height: 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%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
129
AutoRobot/Windows/Robot/Web/src/DockLayout/DockLayout.vue
Normal file
129
AutoRobot/Windows/Robot/Web/src/DockLayout/DockLayout.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<div class="dock-layout">
|
||||
<!-- 主区域 -->
|
||||
<Area :WindowState="windowState" :showTitleBar="false" title="主区域" />
|
||||
|
||||
<!-- 浮动区域列表 -->
|
||||
<div
|
||||
v-for="area in floatingAreas"
|
||||
:key="area.id"
|
||||
class="floating-area"
|
||||
:style="{
|
||||
left: area.x + 'px',
|
||||
top: area.y + 'px',
|
||||
width: area.width + 'px',
|
||||
height: area.height + 'px'
|
||||
}"
|
||||
>
|
||||
<Area
|
||||
:id="area.id"
|
||||
:title="area.title"
|
||||
:WindowState="'正常'"
|
||||
:showTitleBar="true"
|
||||
:width="area.width"
|
||||
:height="area.height"
|
||||
@close="onCloseFloatingArea(area.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineExpose } from 'vue'
|
||||
import Area from './Area.vue';
|
||||
import Panel from './Panel.vue';
|
||||
|
||||
// 主区域状态
|
||||
const windowState = ref('最大化')
|
||||
|
||||
// 浮动区域列表
|
||||
const floatingAreas = ref([])
|
||||
|
||||
// 区域ID计数器
|
||||
let areaIdCounter = 1
|
||||
|
||||
// 添加新的浮动区域
|
||||
const addFloatingArea = () => {
|
||||
const newArea = {
|
||||
id: `floating-area-${areaIdCounter++}`,
|
||||
title: `浮动区域 ${areaIdCounter - 1}`,
|
||||
x: 50 + (areaIdCounter - 2) * 20, // 错开位置
|
||||
y: 50 + (areaIdCounter - 2) * 20,
|
||||
width: 300,
|
||||
height: 200,
|
||||
collapsed: false,
|
||||
toolbarExpanded: false
|
||||
}
|
||||
floatingAreas.value.push(newArea)
|
||||
}
|
||||
|
||||
// 切换折叠状态
|
||||
const onToggleCollapse = (id) => {
|
||||
const area = floatingAreas.value.find(a => a.id === id)
|
||||
if (area) {
|
||||
area.collapsed = !area.collapsed
|
||||
}
|
||||
}
|
||||
|
||||
// 最大化/还原
|
||||
const onMaximize = (id) => {
|
||||
const area = floatingAreas.value.find(a => a.id === id)
|
||||
if (area) {
|
||||
// 简单实现:交换宽高
|
||||
const temp = area.width
|
||||
area.width = area.height
|
||||
area.height = temp
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭浮动区域
|
||||
const onCloseFloatingArea = (id) => {
|
||||
const index = floatingAreas.value.findIndex(a => a.id === id)
|
||||
if (index !== -1) {
|
||||
floatingAreas.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 切换工具栏
|
||||
const onToggleToolbar = (id) => {
|
||||
const area = floatingAreas.value.find(a => a.id === id)
|
||||
if (area) {
|
||||
area.toolbarExpanded = !area.toolbarExpanded
|
||||
}
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
addFloatingArea
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dock-layout {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 浮动区域样式 */
|
||||
.floating-area {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
background-color: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 添加浮动区域按钮样式 */
|
||||
.add-floating-btn {
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.add-floating-btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
</style>
|
||||
@@ -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); }
|
||||
`
|
||||
});
|
||||
140
AutoRobot/Windows/Robot/Web/src/DockLayout/Panel.vue
Normal file
140
AutoRobot/Windows/Robot/Web/src/DockLayout/Panel.vue
Normal 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>
|
||||
@@ -44,35 +44,19 @@
|
||||
|
||||
<!-- 预留主区域(暂不放面板) -->
|
||||
<div ref="panelHost" class="flex-1 w-full h-[calc(100%-4rem)] relative bg-gray-100">
|
||||
<!-- 浮动面板渲染区 - 使用Panel组件 -->
|
||||
<Panel
|
||||
v-for="panel in floatingPanels"
|
||||
:key="panel.id"
|
||||
:panel="panel"
|
||||
:hostRef="panelHost"
|
||||
@close="closePanel"
|
||||
@toggleCollapse="toggleCollapse"
|
||||
@toggleToolbar="toggleToolbarExpand"
|
||||
@maximize="maximizePanel"
|
||||
>
|
||||
<template #content>
|
||||
<!-- 面板内容区域 -->
|
||||
<div class="p-4">
|
||||
<div class="text-center text-gray-500 text-sm">面板内容区域 - {{ panel.id }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</Panel>
|
||||
<DockLayout ref="dockLayoutRef" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import Panel from '../DockLayout/Panel.js';
|
||||
import DockLayout from '../DockLayout/DockLayout.vue';
|
||||
|
||||
// 顶部控制栏按钮的引用
|
||||
const layoutFileInput = ref(null);
|
||||
const panelHost = ref(null);
|
||||
const dockLayoutRef = ref(null);
|
||||
|
||||
// 浮动面板列表
|
||||
const floatingPanels = ref([]);
|
||||
@@ -85,24 +69,13 @@ function addPanel(position) {
|
||||
|
||||
function addFloatingPanel() {
|
||||
console.log('[DockLayoutTest] addFloatingPanel called');
|
||||
// 复原截图样式的默认面板参数
|
||||
const defaultSize = { width: 300, height: 180 };
|
||||
const offset = 16;
|
||||
const idx = nextPanelIdx.value++;
|
||||
const panel = {
|
||||
id: 'float-' + idx,
|
||||
title: '输出',
|
||||
x: offset * idx,
|
||||
y: offset * idx,
|
||||
width: defaultSize.width,
|
||||
height: defaultSize.height,
|
||||
collapsed: false,
|
||||
toolbarExpanded: false,
|
||||
maximized: false,
|
||||
};
|
||||
console.log('[DockLayoutTest] Adding panel:', panel);
|
||||
floatingPanels.value.push(panel);
|
||||
console.log('[DockLayoutTest] Panels after add:', floatingPanels.value.length);
|
||||
// 向DockLayout组件发送添加浮动区命令
|
||||
if (dockLayoutRef.value && dockLayoutRef.value.addFloatingArea) {
|
||||
console.log('[DockLayoutTest] 调用DockLayout的addFloatingArea方法');
|
||||
dockLayoutRef.value.addFloatingArea();
|
||||
} else {
|
||||
console.warn('[DockLayoutTest] DockLayout引用未初始化或addFloatingArea方法不可用');
|
||||
}
|
||||
}
|
||||
|
||||
function closePanel(id) {
|
||||
|
||||
Reference in New Issue
Block a user