Files
l2-g30/src/components/FloatingPanel.vue
砂糖 48001db94a style(components): 调整浮动面板的z-index并移除body.vue中多余的元素
移除body.vue中未使用的概览栏元素,同时调整FloatingPanel的z-index值以避免可能的层叠冲突
2026-01-03 09:46:47 +08:00

238 lines
5.5 KiB
Vue

<template>
<div v-show="visible" class="fp-root" :style="rootStyle" @mousedown.stop>
<div class="fp-header" @mousedown.prevent.stop="onDragStart">
<div class="fp-title">{{ title }}</div>
<div class="fp-actions">
<el-button type="text" class="fp-btn" @click.stop="toggleMinimize">
<i :class="minimized ? 'el-icon-caret-bottom' : 'el-icon-caret-top'"></i>
</el-button>
<el-button type="text" class="fp-btn" @click.stop="close">
<i class="el-icon-close"></i>
</el-button>
</div>
</div>
<div v-show="!minimized" class="fp-body">
<slot />
</div>
<!-- Resize handle / 缩放手柄 -->
<div v-show="!minimized" class="fp-resize" @mousedown.prevent.stop="onResizeStart"></div>
</div>
</template>
<script>
// English UI + Chinese comments
export default {
name: 'FloatingPanel',
props: {
title: { type: String, default: 'Floating Panel' },
storageKey: { type: String, required: true },
defaultX: { type: Number, default: 20 },
defaultY: { type: Number, default: 20 },
defaultW: { type: Number, default: 420 },
defaultH: { type: Number, default: 520 }
},
data() {
return {
visible: true,
minimized: false,
x: this.defaultX,
y: this.defaultY,
w: this.defaultW,
h: this.defaultH,
dragging: false,
resizing: false,
startMouseX: 0,
startMouseY: 0,
startX: 0,
startY: 0,
startW: 0,
startH: 0
}
},
computed: {
rootStyle() {
return {
left: this.x + 'px',
top: this.y + 'px',
width: this.w + 'px',
height: this.minimized ? 'auto' : this.h + 'px'
}
}
},
created() {
this.restore()
},
beforeDestroy() {
this.detachEvents()
},
methods: {
// 外部可调用:重新打开
open() {
this.visible = true
this.persist()
},
close() {
this.visible = false
this.persist()
this.$emit('close')
},
toggleMinimize() {
this.minimized = !this.minimized
this.persist()
},
restore() {
try {
const raw = localStorage.getItem(this.storageKey)
if (!raw) return
const s = JSON.parse(raw)
if (typeof s.visible === 'boolean') this.visible = s.visible
if (typeof s.minimized === 'boolean') this.minimized = s.minimized
if (typeof s.x === 'number') this.x = s.x
if (typeof s.y === 'number') this.y = s.y
if (typeof s.w === 'number') this.w = s.w
if (typeof s.h === 'number') this.h = s.h
} catch (e) {
// ignore
}
},
persist() {
try {
localStorage.setItem(
this.storageKey,
JSON.stringify({
visible: this.visible,
minimized: this.minimized,
x: this.x,
y: this.y,
w: this.w,
h: this.h
})
)
} catch (e) {
// ignore
}
},
// Drag
onDragStart(e) {
this.dragging = true
this.startMouseX = e.clientX
this.startMouseY = e.clientY
this.startX = this.x
this.startY = this.y
this.attachEvents()
},
onDragMove(e) {
if (!this.dragging) return
const dx = e.clientX - this.startMouseX
const dy = e.clientY - this.startMouseY
this.x = Math.max(0, this.startX + dx)
this.y = Math.max(0, this.startY + dy)
},
onDragEnd() {
if (!this.dragging) return
this.dragging = false
this.persist()
this.detachEvents()
},
// Resize
onResizeStart(e) {
this.resizing = true
this.startMouseX = e.clientX
this.startMouseY = e.clientY
this.startW = this.w
this.startH = this.h
this.attachEvents()
},
onResizeMove(e) {
if (!this.resizing) return
const dx = e.clientX - this.startMouseX
const dy = e.clientY - this.startMouseY
const minW = 320
const minH = 220
this.w = Math.max(minW, this.startW + dx)
this.h = Math.max(minH, this.startH + dy)
},
onResizeEnd() {
if (!this.resizing) return
this.resizing = false
this.persist()
this.detachEvents()
},
attachEvents() {
window.addEventListener('mousemove', this.onGlobalMove)
window.addEventListener('mouseup', this.onGlobalUp)
},
detachEvents() {
window.removeEventListener('mousemove', this.onGlobalMove)
window.removeEventListener('mouseup', this.onGlobalUp)
},
onGlobalMove(e) {
// 复用一套全局事件
this.onDragMove(e)
this.onResizeMove(e)
},
onGlobalUp() {
this.onDragEnd()
this.onResizeEnd()
}
}
}
</script>
<style scoped>
.fp-root {
position: fixed;
z-index: 2000;
background: #ffffff;
border: 1px solid #dcdfe6;
border-radius: 6px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
/* overflow: hidden; */
}
.fp-header {
height: 36px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10px;
background: linear-gradient(135deg, #f5f7fa, #eef2f7);
border-bottom: 1px solid #ebeef5;
cursor: move;
user-select: none;
}
.fp-title {
font-size: 13px;
font-weight: 600;
color: #303133;
}
.fp-actions {
display: flex;
gap: 4px;
}
.fp-btn {
padding: 0 4px;
}
.fp-body {
height: calc(100% - 36px);
padding: 10px;
overflow: auto;
}
.fp-resize {
position: absolute;
width: 14px;
height: 14px;
right: 0;
bottom: 0;
cursor: se-resize;
background: linear-gradient(135deg, transparent 50%, rgba(64, 158, 255, 0.35) 50%);
}
</style>