叶静 3 долоо хоног өмнө
parent
commit
124996bbfd

+ 23 - 0
.promptx/memory/vue3-expert/declarative.dpml

@@ -162,4 +162,27 @@
     </content>
     <tags>#最佳实践 #工具使用</tags>
   </item>
+  <item id="mem_1757062424111_9cpquad3p" time="2025/09/05 16:53">
+    <content>
+      用户明确要求:在开发过程中,除非用户明确要求创建测试文档或测试文件,否则不要主动创建任何测试相关的文件。专注于业务功能的实现,测试文件的创建需要用户明确指示。
+    </content>
+    <tags>#其他</tags>
+  </item>
+  <item id="mem_1757062974435_gzkby2akc" time="2025/09/05 17:02">
+    <content>
+      数据报表图表提示框功能优化完成:
+      1. 将chartTipsBox组件集成到reportChart组件内部,实现了统一管理
+      2. 为chartTipsBox组件添加了完整的拖拽功能,支持自由移动
+      3. 优化了组件复用性,现在不同页面只需引入reportChart即可获得完整功能
+      4. 保持了原有的图表点击事件向后兼容性
+      5. 拖拽功能包括:鼠标抓取、边界限制、位置重置等完整体验
+    
+      技术实现要点:
+      - 使用Vue3的ref、watch、onUnmounted等API
+      - 实现了完整的鼠标事件处理(mousedown、mousemove、mouseup)
+      - 添加了边界检测防止拖拽超出视窗
+      - 使用CSS transform和position实现平滑拖拽效果
+    </content>
+    <tags>#其他</tags>
+  </item>
 </memory>

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 6 - 6
.promptx/pouch.json


+ 11 - 11
.promptx/resource/project.registry.json

@@ -4,8 +4,8 @@
   "metadata": {
     "version": "2.0.0",
     "description": "project 级资源注册表",
-    "createdAt": "2025-08-29T02:29:36.742Z",
-    "updatedAt": "2025-08-29T02:29:36.751Z",
+    "createdAt": "2025-09-05T08:52:47.129Z",
+    "updatedAt": "2025-09-05T08:52:47.166Z",
     "resourceCount": 3
   },
   "resources": [
@@ -17,9 +17,9 @@
       "description": "执行模式,定义具体的行为模式",
       "reference": "@project://.promptx/resource/role/vue3-expert/execution/vue3-development.execution.md",
       "metadata": {
-        "createdAt": "2025-08-29T02:29:36.746Z",
-        "updatedAt": "2025-08-29T02:29:36.746Z",
-        "scannedAt": "2025-08-29T02:29:36.746Z",
+        "createdAt": "2025-09-05T08:52:47.142Z",
+        "updatedAt": "2025-09-05T08:52:47.142Z",
+        "scannedAt": "2025-09-05T08:52:47.142Z",
         "path": "role/vue3-expert/execution/vue3-development.execution.md"
       }
     },
@@ -31,9 +31,9 @@
       "description": "思维模式,指导AI的思考方式",
       "reference": "@project://.promptx/resource/role/vue3-expert/thought/vue3-thinking.thought.md",
       "metadata": {
-        "createdAt": "2025-08-29T02:29:36.748Z",
-        "updatedAt": "2025-08-29T02:29:36.748Z",
-        "scannedAt": "2025-08-29T02:29:36.748Z",
+        "createdAt": "2025-09-05T08:52:47.153Z",
+        "updatedAt": "2025-09-05T08:52:47.153Z",
+        "scannedAt": "2025-09-05T08:52:47.153Z",
         "path": "role/vue3-expert/thought/vue3-thinking.thought.md"
       }
     },
@@ -45,9 +45,9 @@
       "description": "专业角色,提供特定领域的专业能力",
       "reference": "@project://.promptx/resource/role/vue3-expert/vue3-expert.role.md",
       "metadata": {
-        "createdAt": "2025-08-29T02:29:36.750Z",
-        "updatedAt": "2025-08-29T02:29:36.750Z",
-        "scannedAt": "2025-08-29T02:29:36.750Z",
+        "createdAt": "2025-09-05T08:52:47.165Z",
+        "updatedAt": "2025-09-05T08:52:47.165Z",
+        "scannedAt": "2025-09-05T08:52:47.165Z",
         "path": "role/vue3-expert/vue3-expert.role.md"
       }
     }

+ 142 - 27
src/app/shop/admin/data/report/components/chart-tips-box.vue

@@ -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 {

+ 71 - 1
src/app/shop/admin/data/report/components/report-chart.vue

@@ -92,6 +92,11 @@
         <el-text type="info" size="small">{{ description }}</el-text>
       </div>
     </el-card>
+
+    <!-- 图表提示框组件 - 使用 Teleport 传送到 body -->
+    <Teleport to="body">
+      <ChartTipsBox :visible="tipsBoxVisible" :data="tipsBoxData" @close="closeTipsBox" />
+    </Teleport>
   </div>
 </template>
 
@@ -101,6 +106,7 @@ import { ElMessage } from 'element-plus'
 import { Refresh, FullScreen } from '@element-plus/icons-vue'
 import VChart from 'vue-echarts'
 import * as echarts from 'echarts'
+import ChartTipsBox from './chart-tips-box.vue'
 
 // Props
 const props = defineProps({
@@ -168,6 +174,13 @@ const currentType = ref(props.type)
 const dateRange = ref([])
 const isFullscreen = ref(false)
 
+// 图表提示框相关数据
+const tipsBoxVisible = ref(false)
+const tipsBoxData = ref({
+  title: '',
+  items: []
+})
+
 // 可用的图表类型
 const availableTypes = computed(() => [
   { label: '折线图', value: 'line', enabled: true },
@@ -484,7 +497,59 @@ const refreshChart = () => {
 }
 
 const handleChartClick = (params) => {
-  // 发送点击事件,包含图表数据和点击位置信息
+  // 处理图表点击事件 - 显示提示框
+  console.log('图表点击事件:', params)
+
+  // 构建提示框数据
+  const clickData = params.data || params
+  const chartData = props.data || []
+  const chartType = currentType.value
+  const chartTitle = props.title || '图表'
+
+  // 根据图表类型和点击数据构建提示框内容
+  let title = ''
+  let items = []
+
+  if (chartType === 'pie') {
+    // 饼图:显示点击的扇形数据
+    title = `${chartTitle}: ${clickData.name || ''}`
+    items = [{
+      name: clickData.name || '数据项',
+      value: clickData.value || 0,
+      percent: clickData.percent ? `${clickData.percent}%` : '0%',
+      color: params.color || props.colors[0]
+    }]
+  } else {
+    // 折线图/柱状图:显示该时间点的所有系列数据
+    const xValue = clickData.name || params.name || ''
+    title = `${chartTitle}: ${xValue}`
+
+    // 获取该时间点的所有系列数据
+    const seriesData = chartData.filter(item => item[props.xAxisKey] === xValue)
+    const totalValue = seriesData.reduce((sum, item) => sum + (item[props.yAxisKey] || 0), 0)
+
+    items = seriesData.map((item, index) => ({
+      name: item[props.seriesKey] || '系列',
+      value: item[props.yAxisKey] || 0,
+      percent: totalValue > 0 ? `${((item[props.yAxisKey] || 0) / totalValue * 100).toFixed(2)}%` : '0%',
+      color: props.colors[index % props.colors.length]
+    }))
+
+    // 添加汇总行
+    if (items.length > 1) {
+      items.push({
+        name: `${chartTitle}汇总`,
+        value: totalValue,
+        percent: '100%',
+        color: '#999'
+      })
+    }
+  }
+
+  tipsBoxData.value = { title, items }
+  tipsBoxVisible.value = true
+
+  // 同时发送原有的点击事件,保持向后兼容
   emit('chartClick', {
     ...params,
     chartData: props.data,
@@ -493,6 +558,11 @@ const handleChartClick = (params) => {
   })
 }
 
+// 关闭提示框
+const closeTipsBox = () => {
+  tipsBoxVisible.value = false
+}
+
 const toggleFullscreen = () => {
   isFullscreen.value = !isFullscreen.value
   nextTick(() => {

+ 3 - 68
src/app/shop/admin/data/report/index.vue

@@ -130,9 +130,6 @@
 
 
 
-  <!-- 图表提示框组件 -->
-  <chartTipsBox :visible="tipsBoxVisible" :data="tipsBoxData" @close="closeTipsBox" />
-
 </template>
 <script setup>
 import { onMounted, reactive, ref, computed } from 'vue';
@@ -145,7 +142,6 @@ import dataDisplayEdit from './components/dataDisplayEdit.vue';
 import dashboardEdit from './components/dashboardEdit.vue';
 import chartEdit from './components/chartEdit.vue';
 import reportChart from './components/report-chart.vue';
-import chartTipsBox from './components/chart-tips-box.vue';
 import detailData from './detail.json';
 
 // 响应式数据
@@ -156,18 +152,10 @@ const dashboardList = ref([]);
 const chartData = ref([]);
 // 导出loading状态
 const exportLoading = ref(false);
-// 悬浮弹窗相关代码已移除
 
-// 图例相关数据已移除
+// 图例颜色配置
 const legendColors = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc'];
 
-// 提示框相关数据
-const tipsBoxVisible = ref(false);
-const tipsBoxData = ref({
-  title: '',
-  items: []
-});
-
 // 计算当前选中看板的图表数据
 const currentCharts = computed(() => {
   if (!selectedDashboard.value) return [];
@@ -316,63 +304,10 @@ function transformChartData(chart) {
 
 
 
-// 处理图表点击事件 - 显示固定提示框
+// 处理图表点击事件 - 现在由 reportChart 组件内部处理
 function handleChartClick(params) {
   console.log('图表点击事件:', params);
-
-  // 构建提示框数据
-  const clickData = params.data || params;
-  const chartData = params.chartData || [];
-  const chartType = params.chartType || 'line';
-  const chartTitle = params.chartTitle || '图表';
-
-  // 根据图表类型和点击数据构建提示框内容
-  let title = '';
-  let items = [];
-
-  if (chartType === 'pie') {
-    // 饼图:显示点击的扇形数据
-    title = `${chartTitle}:${clickData.name || ''}`;
-    items = [{
-      name: clickData.name || '数据项',
-      value: clickData.value || 0,
-      percent: clickData.percent ? `${clickData.percent}%` : '0%',
-      color: params.color || legendColors[0]
-    }];
-  } else {
-    // 折线图/柱状图:显示该时间点的所有系列数据
-    const xValue = clickData.name || params.name || '';
-    title = `${chartTitle}:${xValue}`;
-
-    // 获取该时间点的所有系列数据
-    const seriesData = chartData.filter(item => item.x === xValue);
-    const totalValue = seriesData.reduce((sum, item) => sum + (item.y || 0), 0);
-
-    items = seriesData.map((item, index) => ({
-      name: item.series || '系列',
-      value: item.y || 0,
-      percent: totalValue > 0 ? `${((item.y || 0) / totalValue * 100).toFixed(2)}%` : '0%',
-      color: legendColors[index % legendColors.length]
-    }));
-
-    // 添加汇总行
-    if (items.length > 1) {
-      items.push({
-        name: `${chartTitle}汇总`,
-        value: totalValue,
-        percent: '100%',
-        color: '#999'
-      });
-    }
-  }
-
-  tipsBoxData.value = { title, items };
-  tipsBoxVisible.value = true;
-}
-
-// 关闭提示框
-function closeTipsBox() {
-  tipsBoxVisible.value = false;
+  // 这里可以添加额外的处理逻辑,如果需要的话
 }
 
 // 删除看板

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно