Prechádzať zdrojové kódy

feat:完成RuntimeChart

Jerry Liang 1 rok pred
rodič
commit
e9c1d97435

+ 1 - 0
package.json

@@ -41,6 +41,7 @@
     "@vueuse/components": "^13.5.0",
     "@vueuse/core": "^13.5.0",
     "chroma-js": "^3.1.2",
+    "dayjs": "^1.11.18",
     "es-toolkit": "^1.39.8",
     "naive-ui": "^2.42.0",
     "pinia": "^3.0.3",

+ 40 - 32
pnpm-lock.yaml

@@ -68,6 +68,9 @@ importers:
       chroma-js:
         specifier: ^3.1.2
         version: 3.1.2
+      dayjs:
+        specifier: ^1.11.18
+        version: 1.11.18
       es-toolkit:
         specifier: ^1.39.8
         version: 1.39.8
@@ -1507,43 +1510,43 @@ packages:
     resolution: {integrity: sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@typescript/native-preview-darwin-arm64@7.0.0-dev.20250914.1':
-    resolution: {integrity: sha512-0Rvj+08RqtQKFBDJ2vu1ejIZccdvZYFwAg5j3b8Yffcyp8nIKKSd4iEr8Bf0sHw4n+76bJRf7MbgEcayZGEFvg==}
+  '@typescript/native-preview-darwin-arm64@7.0.0-dev.20251012.1':
+    resolution: {integrity: sha512-x6CnbPP7QnEEK14mstam7yUf6g8ZFq2XyCuh7OURrbTMfScaosQ0iRrwztyFvAgoOm0Vhrc/+pnKOlXE7h5o2A==}
     cpu: [arm64]
     os: [darwin]
 
-  '@typescript/native-preview-darwin-x64@7.0.0-dev.20250914.1':
-    resolution: {integrity: sha512-jZFqxney4PfLvODL6ywTrlbUggNoC/rN/mdSC1Tevlc3W3itvqlyMKEbUZ7LLzonc8phDWkL4qnwnsQcfu7mMg==}
+  '@typescript/native-preview-darwin-x64@7.0.0-dev.20251012.1':
+    resolution: {integrity: sha512-RX/DWsdnrKxpVzI4D0O848GYPdUiCLDaj1YDx0oV1NBA2fcPJnVo7LQS3A2mAWnKIcWcmy8y++L5XD+qSDBN7Q==}
     cpu: [x64]
     os: [darwin]
 
-  '@typescript/native-preview-linux-arm64@7.0.0-dev.20250914.1':
-    resolution: {integrity: sha512-MjicZpmJetOwaf1Hld/GMCtjFNk9GNxxyB4XcVXedq+q5E67ipCAJcu8Hz/p7MdhUiOzqM31mpMNxP0Kl1qtEA==}
+  '@typescript/native-preview-linux-arm64@7.0.0-dev.20251012.1':
+    resolution: {integrity: sha512-XxBHBZI2e8jGcDSSTWqcnATtP8/072ci9Yxfl0AHwEgGHele2N5PHE6sOFsat7cqWxsLPolTXwD3CiWrXpUakA==}
     cpu: [arm64]
     os: [linux]
 
-  '@typescript/native-preview-linux-arm@7.0.0-dev.20250914.1':
-    resolution: {integrity: sha512-Lk0UrqLDzfbzUa5qiKeNTnOpuy08pZvnR2fqy+akuMCpsbI9YOUJxeG5e0NZ95Ca1lkCwJG5HZPyr7Ddgubz5w==}
+  '@typescript/native-preview-linux-arm@7.0.0-dev.20251012.1':
+    resolution: {integrity: sha512-zRo2kf6ES8G7r0Fsvj7AOzjckRUdbidyUIoxU4L6Vs4+FN8Om+QlN3EI6/bHljgNpeYjXGbpbplvyYivoTW/sg==}
     cpu: [arm]
     os: [linux]
 
-  '@typescript/native-preview-linux-x64@7.0.0-dev.20250914.1':
-    resolution: {integrity: sha512-R0mXfWBZpU+xs/X3Sqcpsb8Y3TcqlYOzmwczFz+K0yBIMXkEGklsYgnFho3ZmAXW7274sNq4xGA1D4+39oiryg==}
+  '@typescript/native-preview-linux-x64@7.0.0-dev.20251012.1':
+    resolution: {integrity: sha512-xbqC3Hv8q/tpMrk5++9ydbsldxCrfIfw2CuyD68e1T4cZZrDpMM+qr1TVbH6Yqr2l6zPS5kRnP/FXiPXpcddiA==}
     cpu: [x64]
     os: [linux]
 
-  '@typescript/native-preview-win32-arm64@7.0.0-dev.20250914.1':
-    resolution: {integrity: sha512-4c2c2jV5PNbjAmZSbrMMRqcInGJUYSIVOJRVmHJJNZdjYR414whyiAcb8khZc/TbqhgznXYZHz9D8OvRBddpLA==}
+  '@typescript/native-preview-win32-arm64@7.0.0-dev.20251012.1':
+    resolution: {integrity: sha512-LOA+nhk5dMpweJksDrjMPMNCYDlRwOyS2U5gktBFy8+3rsc6iABO5f/mcc0yy8cRxZ8H+0xf6x4oOYM944gZtg==}
     cpu: [arm64]
     os: [win32]
 
-  '@typescript/native-preview-win32-x64@7.0.0-dev.20250914.1':
-    resolution: {integrity: sha512-9+wfG73UAMtXkw6RxI5FZDD1PocTsnudmK2DMbYufE64sWDmIvIULL+NyttESK4/Kf8kVPebQJOd7Ue7ilFXJg==}
+  '@typescript/native-preview-win32-x64@7.0.0-dev.20251012.1':
+    resolution: {integrity: sha512-xOqeyd9+/kV3ppiXGrKDlY+2L1uhV/Sa9SAdq2ijF87kd72rXizL606lBJsStmy3erxTP7/exXS9h/Avb2t+Zw==}
     cpu: [x64]
     os: [win32]
 
-  '@typescript/native-preview@7.0.0-dev.20250914.1':
-    resolution: {integrity: sha512-dcgUc4J7hGeXxKTMrDjxnBNShOKRKYmc+Lw6dSCkOML9LjeeTo+JnqQwcXhft60oC7YRyRyA/mnArYX+UrS/jA==}
+  '@typescript/native-preview@7.0.0-dev.20251012.1':
+    resolution: {integrity: sha512-6yB55JTN3PrOxrxJPSsLDcMwWVFVZNbOX14/d9wC1nTGbx6cJPREB8ep+rrp+rfPd5kpXZ9fME+Xic674sANAQ==}
     hasBin: true
 
   '@umami/node@0.4.0':
@@ -2395,6 +2398,9 @@ packages:
   date-fns@3.6.0:
     resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==}
 
+  dayjs@1.11.18:
+    resolution: {integrity: sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==}
+
   de-indent@1.0.2:
     resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
 
@@ -6196,36 +6202,36 @@ snapshots:
       '@typescript-eslint/types': 8.39.0
       eslint-visitor-keys: 4.2.1
 
-  '@typescript/native-preview-darwin-arm64@7.0.0-dev.20250914.1':
+  '@typescript/native-preview-darwin-arm64@7.0.0-dev.20251012.1':
     optional: true
 
-  '@typescript/native-preview-darwin-x64@7.0.0-dev.20250914.1':
+  '@typescript/native-preview-darwin-x64@7.0.0-dev.20251012.1':
     optional: true
 
-  '@typescript/native-preview-linux-arm64@7.0.0-dev.20250914.1':
+  '@typescript/native-preview-linux-arm64@7.0.0-dev.20251012.1':
     optional: true
 
-  '@typescript/native-preview-linux-arm@7.0.0-dev.20250914.1':
+  '@typescript/native-preview-linux-arm@7.0.0-dev.20251012.1':
     optional: true
 
-  '@typescript/native-preview-linux-x64@7.0.0-dev.20250914.1':
+  '@typescript/native-preview-linux-x64@7.0.0-dev.20251012.1':
     optional: true
 
-  '@typescript/native-preview-win32-arm64@7.0.0-dev.20250914.1':
+  '@typescript/native-preview-win32-arm64@7.0.0-dev.20251012.1':
     optional: true
 
-  '@typescript/native-preview-win32-x64@7.0.0-dev.20250914.1':
+  '@typescript/native-preview-win32-x64@7.0.0-dev.20251012.1':
     optional: true
 
-  '@typescript/native-preview@7.0.0-dev.20250914.1':
+  '@typescript/native-preview@7.0.0-dev.20251012.1':
     optionalDependencies:
-      '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20250914.1
-      '@typescript/native-preview-darwin-x64': 7.0.0-dev.20250914.1
-      '@typescript/native-preview-linux-arm': 7.0.0-dev.20250914.1
-      '@typescript/native-preview-linux-arm64': 7.0.0-dev.20250914.1
-      '@typescript/native-preview-linux-x64': 7.0.0-dev.20250914.1
-      '@typescript/native-preview-win32-arm64': 7.0.0-dev.20250914.1
-      '@typescript/native-preview-win32-x64': 7.0.0-dev.20250914.1
+      '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20251012.1
+      '@typescript/native-preview-darwin-x64': 7.0.0-dev.20251012.1
+      '@typescript/native-preview-linux-arm': 7.0.0-dev.20251012.1
+      '@typescript/native-preview-linux-arm64': 7.0.0-dev.20251012.1
+      '@typescript/native-preview-linux-x64': 7.0.0-dev.20251012.1
+      '@typescript/native-preview-win32-arm64': 7.0.0-dev.20251012.1
+      '@typescript/native-preview-win32-x64': 7.0.0-dev.20251012.1
 
   '@umami/node@0.4.0': {}
 
@@ -7288,6 +7294,8 @@ snapshots:
 
   date-fns@3.6.0: {}
 
+  dayjs@1.11.18: {}
+
   de-indent@1.0.2: {}
 
   debug@4.4.1:
@@ -9855,7 +9863,7 @@ snapshots:
 
   vue-vine-tsc@1.7.0(typescript@5.8.3):
     dependencies:
-      '@typescript/native-preview': 7.0.0-dev.20250914.1
+      '@typescript/native-preview': 7.0.0-dev.20251012.1
       '@volar/language-core': 2.4.22
       '@volar/typescript': 2.4.22
       '@vue-vine/language-service': 1.7.0(typescript@5.8.3)

+ 246 - 0
src/views/sites-analysis/component/siteRuntimeChart.vue

@@ -0,0 +1,246 @@
+<template>
+  <NSlider v-model:value="timeRange" :max="getMaxRange()" :marks="dateMarks" range :format-tooltip="sliderFormatTooltip"
+    @dragend="handleDragEnd" />
+  <div ref="chartRuntime" class="w-full"></div>
+</template>
+
+<script setup lang="ts">
+import { Chart, type G2Spec, type Mark, type Composition } from '@antv/g2'
+import { NSlider } from 'naive-ui';
+import { ref, onMounted, onBeforeUnmount } from 'vue'
+import dayjs from 'dayjs'
+
+const props = defineProps<{
+  height?: number
+  data: RuntimeDataType[] // 数据必须按正确的时间排序
+}>()
+
+const chartRuntime = ref<HTMLDivElement>()
+let runtimeChart: Chart | null = null
+
+export interface RuntimeDataType {
+  time: string // format YYYY-MM-DD HH:mm:ss
+  [key: string]: string | number // number是曲线 string是事件
+}
+
+interface ChartDataType {
+  time: number // 距离其实时间的秒数
+  [key: string]: string | number // number是曲线 string是事件
+}
+
+// 模拟数据:10 分钟,每秒一个点
+// const data: RuntimeDataType[] = [];
+// const start = dayjs('2025-10-09 12:00:00');
+// for (let i = 0; i < 600; i++) {
+//   const time = start.add(i, 'second').format('YYYY-MM-DD HH:mm:ss');
+//   data.push({
+//     time,
+//     value: Math.sin(i / 10) * 10 + 20,
+//   })
+// }
+
+// 计算时间相关信息
+const startTime = props.data[0].time
+const startTimeDayjs = dayjs(props.data[0].time)
+const endTime = props.data[props.data.length - 1].time
+const endTimeDayjs = dayjs(props.data[props.data.length - 1].time)
+const diffSeconds = endTimeDayjs.diff(startTimeDayjs, 'second');
+
+// 计算某个点的时间 YYYY-MM-DD HH:mm:ss
+const getPointTime = (value: number): string => {
+  console.log({ startTime, endTime })
+  if (value >= diffSeconds) return endTime
+  return startTimeDayjs.add(value, 'second').format('YYYY-MM-DD HH:mm:ss')
+}
+
+// 计算最大选择范围
+const getMaxRange = () => {
+  if (diffSeconds > 0) return diffSeconds
+  return 0
+}
+
+// 计算选择数据范围
+const timeRange = ref<[number, number]>([0, (diffSeconds < 24 * 60 * 60) ? diffSeconds : 24 * 60 * 60])
+
+// 计算日期标注
+const dateMarks = computed<{ [markValue: number]: string }>(() => {
+  const start = dayjs(startTime);
+  const end = dayjs(endTime);
+  const marks: Record<number, string> = {};
+
+  if (!start.isValid() || !end.isValid() || !start.isBefore(end)) {
+    return marks; // 无效输入
+  }
+
+  // 找出所有0点整
+  let current = start.startOf('day'); // 当天0点
+  if (current.isBefore(start)) {
+    current = current.add(1, 'day'); // 如果start已经过了当天0点,取下一天
+  }
+
+  const midnights: dayjs.Dayjs[] = [];
+  while (current.isBefore(end) || current.isSame(end)) {
+    midnights.push(current);
+    current = current.add(1, 'day');
+  }
+
+  // 若超过10个,均匀取样
+  let selected = midnights;
+  const MAX_COUNT = 10;
+  if (midnights.length > MAX_COUNT) {
+    const step = (midnights.length - 1) / (MAX_COUNT - 1);
+    selected = Array.from({ length: MAX_COUNT }, (_, i) =>
+      midnights[Math.round(i * step)]
+    );
+  }
+
+  // 计算每个点相对起始时间的秒数差
+  selected.forEach((d) => {
+    const diffSec = d.diff(start, 'second'); // 秒差
+    const dateStr = d.format('YYYY-MM-DD'); // 格式化成 "YYYY-MM-DD"
+    marks[diffSec] = dateStr;
+  });
+
+  return marks;
+});
+
+const sliderFormatTooltip = (value: number) => {
+  console.log({ value, time: getPointTime(value) })
+  return getPointTime(value)
+}
+
+interface RuntimeChartOpt {
+  data: ChartDataType[]
+  childrenName: Record<string, string>
+}
+
+// 生成Chart数据和子字段SET表
+const handleChartData = (objList: RuntimeDataType[]): RuntimeChartOpt => {
+  const chartData: ChartDataType[] = []
+  const childrenSet: Record<string, string> = {};
+
+  objList.map(obj => {
+    chartData.push({
+      ...obj,
+      time: dayjs(obj.time).diff(startTimeDayjs, 'second'),
+    })
+    Object.entries(obj).forEach(([key, value]) => {
+      if (key !== 'time') {
+        const keyType = typeof value
+        childrenSet[key] = keyType
+      }
+    });
+  })
+
+  return {
+    data: chartData,
+    childrenName: childrenSet
+  }
+}
+const runtimeChartOpt = handleChartData(props.data)
+
+// 生成Chart子配置
+const getChildrenChartOpt = (xRange: [number, number]): (Mark | Composition)[] => {
+  const childrenChart: (Mark | Composition)[] = []
+
+  Object.entries(runtimeChartOpt.childrenName).forEach(([key, value]) => {
+    if (value === 'number') {
+      // 子Chart为曲线
+      childrenChart.push(
+        {
+          type: 'line',
+          height: 100,
+          encode: {
+            x: 'time',
+            y: key
+          },
+          axis: {
+            x: false,
+            y: {
+              tickCount: 0,
+              position: 'bottom'
+            }
+          },
+          scale: {
+            x: {
+              domain: xRange,
+            }
+          },
+          tooltip: {
+            title: (d: ChartDataType) => {
+              return getPointTime(d.time)
+            },
+          }
+        }
+      )
+    } else if (value === 'string') {
+      // 子Chart是事件点
+      childrenChart.push(
+        {
+          type: 'point',
+          height: 100,
+          encode: {
+            x: 'time',
+            y: key
+          },
+          axis: {
+            x: false,
+            y: {
+              tickCount: 0,
+              position: 'bottom'
+            }
+          },
+          scale: {
+            x: {
+              domain: xRange,
+            }
+          },
+          tooltip: {
+            title: (d: ChartDataType) => {
+              return getPointTime(d.time)
+            },
+          }
+        }
+      )
+    }
+  })
+
+  return childrenChart
+}
+
+// 生成最新Chart配置
+const getChartOpt = (xRange: [number, number]): G2Spec => {
+  return {
+    type: 'spaceFlex',
+    direction: 'col',
+    data: runtimeChartOpt.data,
+    children: getChildrenChartOpt(xRange)
+  }
+}
+
+const handleDragEnd = () => {
+  if (runtimeChart === null) return
+  const selectRang = timeRange.value
+  if (selectRang[0] > selectRang[1]) {
+    runtimeChart.options(getChartOpt([selectRang[1], selectRang[0]]))
+  } else {
+    runtimeChart.options(getChartOpt(timeRange.value))
+  }
+  runtimeChart.render()
+}
+
+onMounted(() => {
+  runtimeChart = new Chart({
+    container: chartRuntime.value!,
+    autoFit: true
+  });
+
+  runtimeChart.options(getChartOpt(timeRange.value))
+  runtimeChart.render()
+})
+
+onBeforeUnmount(() => {
+  runtimeChart?.destroy()
+})
+
+</script>

+ 4 - 0
src/views/sites-analysis/index.vue

@@ -4,6 +4,9 @@
       <NTabPane name="timeline" tab="时间线">
         <SiteTimeline :id="analysisID" />
       </NTabPane>
+      <NTabPane name="runtime" tab="运行时">
+        <SiteRuntimeChart />
+      </NTabPane>
     </NTabs>
   </div>
 </template>
@@ -12,6 +15,7 @@
 import { useRoute } from 'vue-router'
 import { NTabs, NTabPane } from 'naive-ui'
 import SiteTimeline from './component/siteTimeline.vue'
+import SiteRuntimeChart from './component/siteRuntimeChart.vue'
 
 defineOptions({
   name: 'SiteAnalysis',