284 lines
7.9 KiB
Vue
284 lines
7.9 KiB
Vue
<template>
|
||
<div
|
||
class="w-full"
|
||
:class="{
|
||
'h-48': !analyzer.logicData.value,
|
||
'h-150': analyzer.logicData.value,
|
||
}"
|
||
>
|
||
<v-chart
|
||
v-if="analyzer.logicData.value"
|
||
class="w-full h-full"
|
||
:option="option"
|
||
autoresize
|
||
:update-options="updateOptions"
|
||
/>
|
||
<div
|
||
v-else
|
||
class="w-full h-full flex flex-col gap-6 items-center justify-center"
|
||
>
|
||
<div class="text-center">
|
||
<h3 class="text-xl font-semibold text-slate-600 mb-2">
|
||
暂无逻辑分析数据
|
||
</h3>
|
||
<p class="text-sm text-slate-500">点击下方按钮生成测试数据用于观察</p>
|
||
</div>
|
||
|
||
<!-- <button
|
||
class="group relative px-8 py-3 bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white font-medium rounded-lg shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200 ease-in-out focus:outline-none focus:ring-4 focus:ring-blue-300 active:scale-95"
|
||
@click="analyzer.generateTestData"
|
||
>
|
||
<span class="flex items-center gap-2">
|
||
<RefreshCcw
|
||
class="w-5 h-5 group-hover:rotate-180 transition-transform duration-300"
|
||
/>
|
||
生成测试数据
|
||
</span>
|
||
<div
|
||
class="absolute inset-0 bg-white opacity-0 group-hover:opacity-20 rounded-lg transition-opacity duration-200"
|
||
></div>
|
||
</button> -->
|
||
<button
|
||
class="group relative px-8 py-3 bg-gradient-to-r text-white font-medium rounded-lg shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200 ease-in-out focus:outline-none focus:ring-4 active:scale-95"
|
||
:class="{
|
||
'from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 focus:ring-blue-300':
|
||
!analyzer.isCapturing.value,
|
||
'from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 focus:ring-red-300':
|
||
analyzer.isCapturing.value,
|
||
}"
|
||
:disabled="
|
||
analyzer.isOperationInProgress.value && !analyzer.isCapturing.value
|
||
"
|
||
@click="
|
||
analyzer.isCapturing.value
|
||
? analyzer.stopCapture()
|
||
: analyzer.startCapture()
|
||
"
|
||
>
|
||
<span class="flex items-center gap-2">
|
||
<template v-if="analyzer.isCapturing.value">
|
||
<Square class="w-5 h-5" />
|
||
停止捕获
|
||
</template>
|
||
<template v-else>
|
||
<Play class="w-5 h-5" />
|
||
开始捕获
|
||
</template>
|
||
</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, shallowRef } from "vue";
|
||
import VChart from "vue-echarts";
|
||
import { RefreshCcw, Play, Square } from "lucide-vue-next";
|
||
|
||
// Echarts
|
||
import { use } from "echarts/core";
|
||
import { LineChart } from "echarts/charts";
|
||
import {
|
||
TooltipComponent,
|
||
GridComponent,
|
||
DataZoomComponent,
|
||
AxisPointerComponent,
|
||
ToolboxComponent,
|
||
} from "echarts/components";
|
||
import { CanvasRenderer } from "echarts/renderers";
|
||
import type { ComposeOption } from "echarts/core";
|
||
import type { LineSeriesOption } from "echarts/charts";
|
||
import type {
|
||
AxisPointerComponentOption,
|
||
TooltipComponentOption,
|
||
GridComponentOption,
|
||
DataZoomComponentOption,
|
||
} from "echarts/components";
|
||
import type {
|
||
ToolboxComponentOption,
|
||
XAXisOption,
|
||
YAXisOption,
|
||
} from "echarts/types/dist/shared";
|
||
import { useLogicAnalyzerState } from "./LogicAnalyzerManager";
|
||
import { useRequiredInjection } from "@/utils/Common";
|
||
import { isUndefined } from "lodash";
|
||
|
||
use([
|
||
TooltipComponent,
|
||
ToolboxComponent,
|
||
GridComponent,
|
||
AxisPointerComponent,
|
||
DataZoomComponent,
|
||
LineChart,
|
||
CanvasRenderer,
|
||
]);
|
||
|
||
type EChartsOption = ComposeOption<
|
||
| AxisPointerComponentOption
|
||
| TooltipComponentOption
|
||
| ToolboxComponentOption
|
||
| GridComponentOption
|
||
| DataZoomComponentOption
|
||
| LineSeriesOption
|
||
>;
|
||
|
||
const analyzer = useRequiredInjection(useLogicAnalyzerState);
|
||
|
||
// 添加更新选项来减少重绘
|
||
const updateOptions = shallowRef({
|
||
notMerge: false,
|
||
lazyUpdate: true,
|
||
silent: false,
|
||
});
|
||
|
||
const option = computed((): EChartsOption => {
|
||
if (isUndefined(analyzer.logicData.value)) return {};
|
||
|
||
// 只获取启用的通道
|
||
const enabledChannels = analyzer.enabledChannels.value;
|
||
const enabledChannelIndices = analyzer.channels
|
||
.map((channel, index) => (channel.enabled ? index : -1))
|
||
.filter((index) => index !== -1);
|
||
|
||
const channelCount = enabledChannels.length;
|
||
const channelSpacing = 2; // 每个通道之间的间距
|
||
|
||
// 如果没有启用的通道,返回空配置
|
||
if (channelCount === 0) {
|
||
return {};
|
||
}
|
||
|
||
// 使用单个网格
|
||
const grids: GridComponentOption[] = [
|
||
{
|
||
left: "5%",
|
||
right: "5%",
|
||
top: "5%",
|
||
bottom: "15%",
|
||
},
|
||
];
|
||
|
||
// 单个X轴
|
||
const xAxis: XAXisOption[] = [
|
||
{
|
||
type: "category",
|
||
boundaryGap: false,
|
||
data: analyzer.logicData.value.x.map((x) => x.toFixed(3)),
|
||
axisLabel: {
|
||
formatter: (value: string) =>
|
||
analyzer.logicData.value
|
||
? `${value}${analyzer.logicData.value.xUnit}`
|
||
: `${value}`,
|
||
},
|
||
},
|
||
];
|
||
|
||
// 单个Y轴,范围根据启用通道数量调整
|
||
const yAxis: YAXisOption[] = [
|
||
{
|
||
type: "value",
|
||
min: -0.5,
|
||
max: channelCount * channelSpacing - 0.5,
|
||
interval: channelSpacing,
|
||
axisLabel: {
|
||
formatter: (value: number) => {
|
||
const channelIndex = Math.round(value / channelSpacing);
|
||
return channelIndex < channelCount
|
||
? enabledChannels[channelIndex].label
|
||
: "";
|
||
},
|
||
},
|
||
splitLine: { show: false },
|
||
},
|
||
];
|
||
|
||
// 创建系列数据,只包含启用的通道
|
||
const series: LineSeriesOption[] = enabledChannelIndices.map(
|
||
(originalIndex: number, displayIndex: number) => ({
|
||
name: enabledChannels[displayIndex].label,
|
||
type: "line",
|
||
data: analyzer.logicData.value!.y[originalIndex].map(
|
||
(value: number) => value + displayIndex * channelSpacing + 0.2,
|
||
),
|
||
step: "end",
|
||
lineStyle: {
|
||
width: 2,
|
||
color: enabledChannels[displayIndex].color,
|
||
},
|
||
areaStyle: {
|
||
opacity: 0.3,
|
||
origin: displayIndex * channelSpacing,
|
||
color: enabledChannels[displayIndex].color,
|
||
},
|
||
symbol: "none",
|
||
// 优化性能配置
|
||
sampling: "lttb",
|
||
// 减少动画以避免闪烁
|
||
animation: false,
|
||
}),
|
||
);
|
||
|
||
return {
|
||
// 全局动画配置
|
||
animation: false,
|
||
tooltip: {
|
||
trigger: "axis",
|
||
axisPointer: {
|
||
type: "line",
|
||
label: {
|
||
backgroundColor: "#6a7985",
|
||
},
|
||
// 减少axisPointer的动画
|
||
animation: false,
|
||
},
|
||
formatter: (params: any) => {
|
||
if (Array.isArray(params) && params.length > 0) {
|
||
const timeValue = analyzer.logicData.value!.x[params[0].dataIndex];
|
||
const dataIndex = params[0].dataIndex;
|
||
|
||
let tooltip = `Time: ${timeValue.toFixed(3)}${analyzer.logicData.value!.xUnit}<br/>`;
|
||
|
||
// 只显示启用通道在当前时间点的原始数值(0或1)
|
||
enabledChannelIndices.forEach(
|
||
(originalIndex: number, displayIndex: number) => {
|
||
const channelName = enabledChannels[displayIndex].label;
|
||
const originalValue =
|
||
analyzer.logicData.value!.y[originalIndex][dataIndex];
|
||
tooltip += `${channelName}: ${originalValue}<br/>`;
|
||
},
|
||
);
|
||
|
||
return tooltip;
|
||
}
|
||
return "";
|
||
},
|
||
// 优化tooltip性能
|
||
hideDelay: 100,
|
||
},
|
||
toolbox: {
|
||
feature: {
|
||
restore: {},
|
||
},
|
||
},
|
||
grid: grids,
|
||
xAxis: xAxis,
|
||
yAxis: yAxis,
|
||
dataZoom: [
|
||
{
|
||
show: true,
|
||
realtime: true,
|
||
start: 0,
|
||
end: 100,
|
||
},
|
||
{
|
||
type: "inside",
|
||
realtime: true,
|
||
start: 0,
|
||
end: 100,
|
||
},
|
||
],
|
||
series: series,
|
||
};
|
||
});
|
||
</script>
|