新增写入功能,追踪弹窗
This commit is contained in:
237
src/components/FloatingPanel.vue
Normal file
237
src/components/FloatingPanel.vue
Normal file
@@ -0,0 +1,237 @@
|
||||
<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: 9999;
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user