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