<template>
<transition name="slide-floating">
<div v-if="visible"
class="floating-panel"
:style="{
left: pos.x + 'px',
top: pos.y + 'px',
width: size.width + 'px',
height: size.height + 'px',
opacity: opacity
}">
<div class="floating-header" @mousedown="startDrag">
<span class="floating-title">{{ title }}</span>
<div class="floating-header-actions">
<input type="range" min="0.3" max="1" step="0.01"
v-model.number="opacity"
class="opacity-slider"
@mousedown.stop />
<button class="close-btn" @click="$emit('close')">×</button>
</div>
</div>
<div class="floating-content">
<div v-if="summary" class="summary-card">
<div class="summary-value">{{ summary.value }}</div>
<div class="summary-label">{{ summary.label }}</div>
</div>
<div class="data-list" :style="{ gridTemplateColumns: gridColumns }">
<div v-for="(item, idx) in items" :key="idx" class="data-item">
<div class="data-label">{{ item.label }}</div>
<div class="data-value">
{{ item.value }}
<span v-if="item.unit" class="unit">{{ item.unit }}</span>
</div>
</div>
</div>
</div>
<div class="resize-handle" @mousedown="startResize"></div>
</div>
</transition>
</template>
<script>
export default {
name: "FloatingPanel",
props: {
visible: { type: Boolean, default: false },
title: { type: String, default: "상세 정보" },
summary: { type: Object, default: null },
items: { type: Array, default: () => [] },
columns: { type: Number, default: 2 }
},
computed: {
gridColumns() {
return `repeat(${this.columns}, 1fr)`;
}
},
data() {
return {
pos: { x: window.innerWidth / 2 - 200, y: window.innerHeight / 2 - 250 },
size: { width: 420, height: 520 },
opacity: 1,
dragging: false,
dragOffset: { x: 0, y: 0 },
resizing: false,
resizeStart: { x: 0, y: 0 },
resizeSize: { width: 420, height: 520 }
};
},
methods: {
startDrag(e) {
this.dragging = true;
this.dragOffset = { x: e.clientX - this.pos.x, y: e.clientY - this.pos.y };
window.addEventListener("mousemove", this.onDrag);
window.addEventListener("mouseup", this.stopDrag);
},
onDrag(e) {
if (!this.dragging) return;
this.pos = { x: e.clientX - this.dragOffset.x, y: e.clientY - this.dragOffset.y };
},
stopDrag() {
this.dragging = false;
window.removeEventListener("mousemove", this.onDrag);
window.removeEventListener("mouseup", this.stopDrag);
},
startResize(e) {
this.resizing = true;
this.resizeStart = { x: e.clientX, y: e.clientY };
this.resizeSize = { ...this.size };
window.addEventListener("mousemove", this.onResize);
window.addEventListener("mouseup", this.stopResize);
e.stopPropagation();
},
onResize(e) {
if (!this.resizing) return;
const dx = e.clientX - this.resizeStart.x;
const dy = e.clientY - this.resizeStart.y;
let w = this.resizeSize.width + dx;
let h = this.resizeSize.height + dy;
w = Math.max(320, Math.min(w, 980));
h = Math.max(300, Math.min(h, 900));
this.size = { width: w, height: h };
},
stopResize() {
this.resizing = false;
window.removeEventListener("mousemove", this.onResize);
window.removeEventListener("mouseup", this.stopResize);
}
}
};
</script>
<style scoped>
.floating-panel {
position: fixed;
background: linear-gradient(135deg, #f0f4ff, #fef6f9);
border-radius: 14px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
z-index: 10000;
display: flex;
flex-direction: column;
overflow: hidden;
border: 1px solid rgba(255,255,255,0.4);
}
.floating-header {
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(255,255,255,0.6);
backdrop-filter: blur(8px);
padding: 12px 16px;
cursor: move;
font-weight: bold;
font-size: 16px;
color: #23406d;
}
.floating-header-actions {
display: flex;
align-items: center;
gap: 8px;
}
.close-btn {
background: none;
border: none;
font-size: 20px;
color: #666;
cursor: pointer;
}
.close-btn:hover {
color: #e74c3c;
}
.floating-content {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.summary-card {
background: white;
border-radius: 12px;
padding: 16px;
text-align: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
margin-bottom: 20px;
}
.summary-value {
font-size: 28px;
font-weight: bold;
color: #2c3e50;
}
.summary-label {
font-size: 14px;
color: #888;
}
.data-list {
display: grid;
gap: 12px;
}
.data-item {
background: white;
border-radius: 10px;
padding: 12px;
box-shadow: 0 1px 6px rgba(0,0,0,0.05);
}
.data-label {
font-size: 14px;
color: #555;
}
.data-value {
font-size: 18px;
font-weight: bold;
color: #23406d;
}
.unit {
font-size: 12px;
color: #888;
}
.resize-handle {
position: absolute;
right: 0;
bottom: 0;
width: 20px;
height: 20px;
cursor: se-resize;
background: linear-gradient(135deg, transparent 50%, #aab5d2 51%);
}
.opacity-slider {
width: 50px;
}
.slide-floating-enter-active, .slide-floating-leave-active {
transition: transform .3s, opacity .2s;
}
.slide-floating-enter-from, .slide-floating-leave-to {
transform: translateY(20px);
opacity: 0;
}
</style>
<FloatingPanel
:visible="showPanel"
title="학생 성적 요약"
:summary="{ value: '87.5', label: '평균 점수' }"
:items="[
{ label: '국어', value: 92, unit: '점' },
{ label: '영어', value: 85, unit: '점' },
{ label: '수학', value: 90, unit: '점' },
{ label: '과학', value: 83, unit: '점' }
]"
:columns="2"
@close="showPanel = false"
/>