|
@@ -1,8 +1,9 @@
|
|
|
<template>
|
|
|
- <div v-if="visible" class="chart-tips-overlay" @click="handleOverlayClick">
|
|
|
- <div class="chart-tips-box" @click.stop>
|
|
|
- <!-- 标题栏 -->
|
|
|
- <div class="tips-header">
|
|
|
+ <div v-if="visible">
|
|
|
+ <!-- 提示框本体 -->
|
|
|
+ <div ref="tipsBoxRef" class="chart-tips-box" @click.stop @mousedown="handleMouseDown">
|
|
|
+ <!-- 标题栏 - 可拖拽区域 -->
|
|
|
+ <div class="tips-header drag-handle" @mousedown="handleDragStart">
|
|
|
<span class="tips-title">{{ data.title }}</span>
|
|
|
<button class="close-btn" @click="$emit('close')">
|
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
@@ -30,7 +31,7 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { computed } from 'vue'
|
|
|
+import { ref, watch, nextTick, onUnmounted } from 'vue'
|
|
|
|
|
|
// Props
|
|
|
const props = defineProps({
|
|
@@ -50,32 +51,130 @@ const props = defineProps({
|
|
|
// Emits
|
|
|
const emit = defineEmits(['close'])
|
|
|
|
|
|
-// 处理遮罩层点击
|
|
|
-function handleOverlayClick() {
|
|
|
- emit('close')
|
|
|
-}
|
|
|
+// 拖拽相关
|
|
|
+const tipsBoxRef = ref(null)
|
|
|
+const isDragging = ref(false)
|
|
|
+const dragOffset = ref({ x: 0, y: 0 })
|
|
|
+const position = ref({ x: 0, y: 0 })
|
|
|
+
|
|
|
+// 移除遮罩层点击处理,不再需要
|
|
|
|
|
|
// 格式化数字
|
|
|
function formatNumber(value) {
|
|
|
if (typeof value !== 'number') return value
|
|
|
return value.toLocaleString()
|
|
|
}
|
|
|
+
|
|
|
+// 拖拽开始
|
|
|
+function handleDragStart(event) {
|
|
|
+ if (!tipsBoxRef.value) return
|
|
|
+
|
|
|
+ isDragging.value = true
|
|
|
+ const rect = tipsBoxRef.value.getBoundingClientRect()
|
|
|
+ dragOffset.value = {
|
|
|
+ x: event.clientX - rect.left,
|
|
|
+ y: event.clientY - rect.top
|
|
|
+ }
|
|
|
+
|
|
|
+ // 阻止默认的文本选择行为
|
|
|
+ event.preventDefault()
|
|
|
+
|
|
|
+ // 添加全局鼠标事件监听
|
|
|
+ document.addEventListener('mousemove', handleDragMove)
|
|
|
+ document.addEventListener('mouseup', handleDragEnd)
|
|
|
+}
|
|
|
+
|
|
|
+// 拖拽移动
|
|
|
+function handleDragMove(event) {
|
|
|
+ if (!isDragging.value || !tipsBoxRef.value) return
|
|
|
+
|
|
|
+ // 计算新位置
|
|
|
+ let newX = event.clientX - dragOffset.value.x
|
|
|
+ let newY = event.clientY - dragOffset.value.y
|
|
|
+
|
|
|
+ // 获取窗口尺寸和弹窗尺寸
|
|
|
+ const windowWidth = window.innerWidth
|
|
|
+ const windowHeight = window.innerHeight
|
|
|
+ const boxRect = tipsBoxRef.value.getBoundingClientRect()
|
|
|
+
|
|
|
+ // 限制拖拽范围,确保弹窗不会超出视窗
|
|
|
+ newX = Math.max(0, Math.min(newX, windowWidth - boxRect.width))
|
|
|
+ newY = Math.max(0, Math.min(newY, windowHeight - boxRect.height))
|
|
|
+
|
|
|
+ position.value = { x: newX, y: newY }
|
|
|
+
|
|
|
+ // 应用位置
|
|
|
+ tipsBoxRef.value.style.left = `${newX}px`
|
|
|
+ tipsBoxRef.value.style.top = `${newY}px`
|
|
|
+ tipsBoxRef.value.style.transform = 'none'
|
|
|
+}
|
|
|
+
|
|
|
+// 拖拽结束
|
|
|
+function handleDragEnd() {
|
|
|
+ isDragging.value = false
|
|
|
+
|
|
|
+ // 移除全局事件监听
|
|
|
+ document.removeEventListener('mousemove', handleDragMove)
|
|
|
+ document.removeEventListener('mouseup', handleDragEnd)
|
|
|
+}
|
|
|
+
|
|
|
+// 重置位置
|
|
|
+function resetPosition() {
|
|
|
+ if (tipsBoxRef.value) {
|
|
|
+ position.value = { x: 0, y: 0 }
|
|
|
+ tipsBoxRef.value.style.left = '50%'
|
|
|
+ tipsBoxRef.value.style.top = '15vh'
|
|
|
+ tipsBoxRef.value.style.transform = 'translateX(-50%)'
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 监听visible变化,重置位置
|
|
|
+watch(() => props.visible, (newVal) => {
|
|
|
+ if (newVal) {
|
|
|
+ // 延迟重置位置,确保DOM已渲染
|
|
|
+ nextTick(() => {
|
|
|
+ resetPosition()
|
|
|
+ })
|
|
|
+ // 添加键盘事件监听
|
|
|
+ document.addEventListener('keydown', handleKeyDown)
|
|
|
+ } else {
|
|
|
+ // 移除键盘事件监听
|
|
|
+ document.removeEventListener('keydown', handleKeyDown)
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+// 处理键盘事件
|
|
|
+function handleKeyDown(event) {
|
|
|
+ // ESC 键关闭提示框
|
|
|
+ if (event.key === 'Escape') {
|
|
|
+ emit('close')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 处理鼠标按下事件(阻止冒泡)
|
|
|
+function handleMouseDown(event) {
|
|
|
+ // 只有在标题栏区域才允许拖拽
|
|
|
+ if (event.target.closest('.drag-handle')) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // 阻止事件冒泡,但不阻止页面其他区域的点击
|
|
|
+ event.stopPropagation()
|
|
|
+}
|
|
|
+
|
|
|
+// 组件卸载时清理事件监听
|
|
|
+const cleanup = () => {
|
|
|
+ document.removeEventListener('mousemove', handleDragMove)
|
|
|
+ document.removeEventListener('mouseup', handleDragEnd)
|
|
|
+ document.removeEventListener('keydown', handleKeyDown)
|
|
|
+}
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ cleanup()
|
|
|
+})
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
-.chart-tips-overlay {
|
|
|
- position: fixed;
|
|
|
- top: 0;
|
|
|
- left: 0;
|
|
|
- right: 0;
|
|
|
- bottom: 0;
|
|
|
- background: rgba(0, 0, 0, 0.3);
|
|
|
- z-index: 9999;
|
|
|
- display: flex;
|
|
|
- align-items: flex-start;
|
|
|
- justify-content: center;
|
|
|
- padding-top: 10vh;
|
|
|
-}
|
|
|
+/* 移除遮罩层样式,不再需要 */
|
|
|
|
|
|
.chart-tips-box {
|
|
|
background: #1a1a1a;
|
|
@@ -85,6 +184,19 @@ function formatNumber(value) {
|
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
|
|
overflow: hidden;
|
|
|
animation: slideDown 0.3s ease-out;
|
|
|
+ position: fixed;
|
|
|
+ z-index: 10001;
|
|
|
+
|
|
|
+ /* 默认居中位置 */
|
|
|
+ left: 50%;
|
|
|
+ top: 15vh;
|
|
|
+ transform: translateX(-50%);
|
|
|
+}
|
|
|
+
|
|
|
+/* 拖拽时的样式 */
|
|
|
+.chart-tips-box.dragging {
|
|
|
+ user-select: none;
|
|
|
+ cursor: grabbing;
|
|
|
}
|
|
|
|
|
|
@keyframes slideDown {
|
|
@@ -106,6 +218,12 @@ function formatNumber(value) {
|
|
|
padding: 16px 20px;
|
|
|
background: #1a1a1a;
|
|
|
border-bottom: 1px solid #333;
|
|
|
+ cursor: grab;
|
|
|
+ user-select: none;
|
|
|
+}
|
|
|
+
|
|
|
+.tips-header:active {
|
|
|
+ cursor: grabbing;
|
|
|
}
|
|
|
|
|
|
.tips-title {
|
|
@@ -204,14 +322,11 @@ function formatNumber(value) {
|
|
|
|
|
|
/* 响应式设计 */
|
|
|
@media (max-width: 768px) {
|
|
|
- .chart-tips-overlay {
|
|
|
- padding-top: 5vh;
|
|
|
- }
|
|
|
-
|
|
|
.chart-tips-box {
|
|
|
min-width: 90vw;
|
|
|
max-width: 90vw;
|
|
|
- margin: 0 5vw;
|
|
|
+ left: 50%;
|
|
|
+ transform: translateX(-50%);
|
|
|
}
|
|
|
|
|
|
.tips-item {
|