feat: 添加逻辑分析仪
This commit is contained in:
parent
e8a16fd446
commit
4d6c06a0e0
|
@ -1,6 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full h-100">
|
<div class="w-full h-150">
|
||||||
<v-chart v-if="true" class="w-full h-full" :option="option" autoresize />
|
<v-chart
|
||||||
|
v-if="data"
|
||||||
|
class="w-full h-full"
|
||||||
|
:option="option"
|
||||||
|
autoresize
|
||||||
|
:update-options="updateOptions"
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="w-full h-full flex items-center justify-center text-gray-500"
|
class="w-full h-full flex items-center justify-center text-gray-500"
|
||||||
|
@ -11,144 +17,194 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from "vue";
|
import { computed, shallowRef } from "vue";
|
||||||
|
import VChart from "vue-echarts";
|
||||||
|
import type { LogicDataType } from "./index";
|
||||||
|
|
||||||
// Echarts
|
// Echarts
|
||||||
import { use } from "echarts/core";
|
import { use } from "echarts/core";
|
||||||
import { LineChart } from "echarts/charts";
|
import { LineChart } from "echarts/charts";
|
||||||
import {
|
import {
|
||||||
TitleComponent,
|
|
||||||
TooltipComponent,
|
TooltipComponent,
|
||||||
LegendComponent,
|
|
||||||
ToolboxComponent,
|
|
||||||
GridComponent,
|
GridComponent,
|
||||||
|
DataZoomComponent,
|
||||||
|
AxisPointerComponent,
|
||||||
|
ToolboxComponent,
|
||||||
} from "echarts/components";
|
} from "echarts/components";
|
||||||
import { CanvasRenderer } from "echarts/renderers";
|
import { CanvasRenderer } from "echarts/renderers";
|
||||||
import type { ComposeOption } from "echarts/core";
|
import type { ComposeOption } from "echarts/core";
|
||||||
import type { LineSeriesOption } from "echarts/charts";
|
import type { LineSeriesOption } from "echarts/charts";
|
||||||
import type {
|
import type {
|
||||||
TitleComponentOption,
|
AxisPointerComponentOption,
|
||||||
TooltipComponentOption,
|
TooltipComponentOption,
|
||||||
LegendComponentOption,
|
|
||||||
ToolboxComponentOption,
|
|
||||||
GridComponentOption,
|
GridComponentOption,
|
||||||
|
DataZoomComponentOption,
|
||||||
} from "echarts/components";
|
} from "echarts/components";
|
||||||
|
import type {
|
||||||
|
ToolboxComponentOption,
|
||||||
|
XAXisOption,
|
||||||
|
YAXisOption,
|
||||||
|
} from "echarts/types/dist/shared";
|
||||||
|
|
||||||
use([
|
use([
|
||||||
TitleComponent,
|
|
||||||
TooltipComponent,
|
TooltipComponent,
|
||||||
LegendComponent,
|
|
||||||
ToolboxComponent,
|
ToolboxComponent,
|
||||||
GridComponent,
|
GridComponent,
|
||||||
|
AxisPointerComponent,
|
||||||
|
DataZoomComponent,
|
||||||
LineChart,
|
LineChart,
|
||||||
CanvasRenderer,
|
CanvasRenderer,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
type EChartsOption = ComposeOption<
|
type EChartsOption = ComposeOption<
|
||||||
| TitleComponentOption
|
| AxisPointerComponentOption
|
||||||
| TooltipComponentOption
|
| TooltipComponentOption
|
||||||
| LegendComponentOption
|
|
||||||
| ToolboxComponentOption
|
| ToolboxComponentOption
|
||||||
| GridComponentOption
|
| GridComponentOption
|
||||||
|
| DataZoomComponentOption
|
||||||
| LineSeriesOption
|
| LineSeriesOption
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
// Define props
|
||||||
|
interface Props {
|
||||||
|
data?: LogicDataType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
// 添加更新选项来减少重绘
|
||||||
|
const updateOptions = shallowRef({
|
||||||
|
notMerge: false,
|
||||||
|
lazyUpdate: true,
|
||||||
|
silent: false
|
||||||
|
});
|
||||||
|
|
||||||
const option = computed((): EChartsOption => {
|
const option = computed((): EChartsOption => {
|
||||||
return {
|
if (!props.data) return {};
|
||||||
title: {
|
|
||||||
text: "Stacked Area Chart",
|
const channelCount = props.data.y.length;
|
||||||
|
const channelSpacing = 2; // 每个通道之间的间距
|
||||||
|
|
||||||
|
// 使用单个网格
|
||||||
|
const grids: GridComponentOption[] = [
|
||||||
|
{
|
||||||
|
left: "5%",
|
||||||
|
right: "5%",
|
||||||
|
top: "5%",
|
||||||
|
bottom: "15%",
|
||||||
},
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 单个X轴
|
||||||
|
const xAxis: XAXisOption[] = [
|
||||||
|
{
|
||||||
|
type: "category",
|
||||||
|
boundaryGap: false,
|
||||||
|
data: props.data!.x.map((x) => x.toFixed(3)),
|
||||||
|
axisLabel: {
|
||||||
|
formatter: (value: string) => `${value}${props.data!.xUnit}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 单个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
|
||||||
|
? props.data!.channelNames[channelIndex]
|
||||||
|
: "";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
splitLine: { show: false },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 创建系列数据,每个通道有不同的Y偏移
|
||||||
|
const series: LineSeriesOption[] = props.data.y.map((channelData, index) => ({
|
||||||
|
name: props.data!.channelNames[index],
|
||||||
|
type: "line",
|
||||||
|
data: channelData.map((value) => value + index * channelSpacing + 0.2),
|
||||||
|
step: "end",
|
||||||
|
lineStyle: {
|
||||||
|
width: 2,
|
||||||
|
},
|
||||||
|
areaStyle: {
|
||||||
|
opacity: 0.3,
|
||||||
|
origin: index * channelSpacing,
|
||||||
|
},
|
||||||
|
symbol: "none",
|
||||||
|
// 优化性能配置
|
||||||
|
sampling: "lttb",
|
||||||
|
// large: true,
|
||||||
|
// largeThreshold: 2000,
|
||||||
|
// progressive: 2000,
|
||||||
|
// 减少动画以避免闪烁
|
||||||
|
animation: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 全局动画配置
|
||||||
|
animation: false,
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: "axis",
|
trigger: "axis",
|
||||||
axisPointer: {
|
axisPointer: {
|
||||||
type: "cross",
|
type: "line",
|
||||||
label: {
|
label: {
|
||||||
backgroundColor: "#6a7985",
|
backgroundColor: "#6a7985",
|
||||||
},
|
},
|
||||||
|
// 减少axisPointer的动画
|
||||||
|
animation: false,
|
||||||
},
|
},
|
||||||
},
|
formatter: (params: any) => {
|
||||||
legend: {
|
if (Array.isArray(params) && params.length > 0) {
|
||||||
data: ["Email", "Union Ads", "Video Ads", "Direct", "Search Engine"],
|
const timeValue = props.data!.x[params[0].dataIndex];
|
||||||
|
const dataIndex = params[0].dataIndex;
|
||||||
|
|
||||||
|
let tooltip = `Time: ${timeValue.toFixed(3)}${props.data!.xUnit}<br/>`;
|
||||||
|
|
||||||
|
// 显示所有通道在当前时间点的原始数值(0或1)
|
||||||
|
props.data!.channelNames.forEach((channelName, index) => {
|
||||||
|
const originalValue = props.data!.y[index][dataIndex];
|
||||||
|
tooltip += `${channelName}: ${originalValue}<br/>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return tooltip;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
// 优化tooltip性能
|
||||||
|
hideDelay: 100,
|
||||||
},
|
},
|
||||||
toolbox: {
|
toolbox: {
|
||||||
feature: {
|
feature: {
|
||||||
saveAsImage: {},
|
restore: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
grid: {
|
grid: grids,
|
||||||
left: "3%",
|
xAxis: xAxis,
|
||||||
right: "4%",
|
yAxis: yAxis,
|
||||||
bottom: "3%",
|
dataZoom: [
|
||||||
containLabel: true,
|
|
||||||
},
|
|
||||||
xAxis: [
|
|
||||||
{
|
{
|
||||||
type: "category",
|
show: true,
|
||||||
boundaryGap: false,
|
realtime: true,
|
||||||
data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
|
start: 0,
|
||||||
},
|
end: 100,
|
||||||
],
|
},
|
||||||
yAxis: [
|
{
|
||||||
{
|
type: "inside",
|
||||||
type: "value",
|
realtime: true,
|
||||||
},
|
start: 0,
|
||||||
],
|
end: 100,
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: "Email",
|
|
||||||
type: "line",
|
|
||||||
stack: "Total",
|
|
||||||
areaStyle: {},
|
|
||||||
emphasis: {
|
|
||||||
focus: "series",
|
|
||||||
},
|
|
||||||
data: [120, 132, 101, 134, 90, 230, 210],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Union Ads",
|
|
||||||
type: "line",
|
|
||||||
stack: "Total",
|
|
||||||
areaStyle: {},
|
|
||||||
emphasis: {
|
|
||||||
focus: "series",
|
|
||||||
},
|
|
||||||
data: [220, 182, 191, 234, 290, 330, 310],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Video Ads",
|
|
||||||
type: "line",
|
|
||||||
stack: "Total",
|
|
||||||
areaStyle: {},
|
|
||||||
emphasis: {
|
|
||||||
focus: "series",
|
|
||||||
},
|
|
||||||
data: [150, 232, 201, 154, 190, 330, 410],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Direct",
|
|
||||||
type: "line",
|
|
||||||
stack: "Total",
|
|
||||||
areaStyle: {},
|
|
||||||
emphasis: {
|
|
||||||
focus: "series",
|
|
||||||
},
|
|
||||||
data: [320, 332, 301, 334, 390, 330, 320],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Search Engine",
|
|
||||||
type: "line",
|
|
||||||
stack: "Total",
|
|
||||||
label: {
|
|
||||||
show: true,
|
|
||||||
position: "top",
|
|
||||||
},
|
|
||||||
areaStyle: {},
|
|
||||||
emphasis: {
|
|
||||||
focus: "series",
|
|
||||||
},
|
|
||||||
data: [820, 932, 901, 934, 1290, 1330, 1320],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
series: series,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
import LogicalWaveFormDisplay from "./LogicalWaveFormDisplay.vue";
|
||||||
|
|
||||||
|
type LogicDataType = {
|
||||||
|
x: number[];
|
||||||
|
y: number[][]; // 8 channels of digital data (0 or 1)
|
||||||
|
xUnit: "s" | "ms" | "us" | "ns";
|
||||||
|
channelNames: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test data generator for 8-channel digital signals
|
||||||
|
function generateTestLogicData(): LogicDataType {
|
||||||
|
const sampleRate = 10000; // 10kHz sampling
|
||||||
|
const duration = 1;
|
||||||
|
const points = Math.floor(sampleRate * duration);
|
||||||
|
|
||||||
|
const x = Array.from({ length: points }, (_, i) => (i / sampleRate) * 1000); // time in ms
|
||||||
|
|
||||||
|
// Generate 8 channels with different digital patterns
|
||||||
|
const y = [
|
||||||
|
// Channel 0: Clock signal 100Hz
|
||||||
|
Array.from(
|
||||||
|
{ length: points },
|
||||||
|
(_, i) => Math.floor((100 * i) / sampleRate) % 2,
|
||||||
|
),
|
||||||
|
// Channel 1: Clock/2 signal 50Hz
|
||||||
|
Array.from(
|
||||||
|
{ length: points },
|
||||||
|
(_, i) => Math.floor((50 * i) / sampleRate) % 2,
|
||||||
|
),
|
||||||
|
// Channel 2: Clock/4 signal 25Hz
|
||||||
|
Array.from(
|
||||||
|
{ length: points },
|
||||||
|
(_, i) => Math.floor((25 * i) / sampleRate) % 2,
|
||||||
|
),
|
||||||
|
// Channel 3: Clock/8 signal 12.5Hz
|
||||||
|
Array.from(
|
||||||
|
{ length: points },
|
||||||
|
(_, i) => Math.floor((12.5 * i) / sampleRate) % 2,
|
||||||
|
),
|
||||||
|
// Channel 4: Data signal (pseudo-random pattern)
|
||||||
|
Array.from(
|
||||||
|
{ length: points },
|
||||||
|
(_, i) => Math.abs( Math.floor(Math.sin(i * 0.01) * 10) % 2 ),
|
||||||
|
),
|
||||||
|
// Channel 5: Enable signal (periodic pulse)
|
||||||
|
Array.from(
|
||||||
|
{ length: points },
|
||||||
|
(_, i) => (Math.floor(i / 50) % 10) < 3 ? 1 : 0,
|
||||||
|
),
|
||||||
|
// Channel 6: Reset signal (occasional pulse)
|
||||||
|
Array.from(
|
||||||
|
{ length: points },
|
||||||
|
(_, i) => (Math.floor(i / 200) % 20) === 0 ? 1 : 0,
|
||||||
|
),
|
||||||
|
// Channel 7: Status signal (slow changing)
|
||||||
|
Array.from(
|
||||||
|
{ length: points },
|
||||||
|
(_, i) => Math.floor(i / 1000) % 2,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
const channelNames = [
|
||||||
|
"CLK",
|
||||||
|
"CLK/2",
|
||||||
|
"CLK/4",
|
||||||
|
"CLK/8",
|
||||||
|
"PWM",
|
||||||
|
"ENABLE",
|
||||||
|
"RESET",
|
||||||
|
"STATUS"
|
||||||
|
];
|
||||||
|
|
||||||
|
return { x, y, xUnit: "ms", channelNames };
|
||||||
|
}
|
||||||
|
|
||||||
|
export { LogicalWaveFormDisplay, generateTestLogicData, type LogicDataType };
|
|
@ -31,7 +31,6 @@ import { CanvasRenderer } from "echarts/renderers";
|
||||||
import type { ComposeOption } from "echarts/core";
|
import type { ComposeOption } from "echarts/core";
|
||||||
import type { LineSeriesOption } from "echarts/charts";
|
import type { LineSeriesOption } from "echarts/charts";
|
||||||
import type {
|
import type {
|
||||||
TitleComponentOption,
|
|
||||||
TooltipComponentOption,
|
TooltipComponentOption,
|
||||||
LegendComponentOption,
|
LegendComponentOption,
|
||||||
ToolboxComponentOption,
|
ToolboxComponentOption,
|
||||||
|
@ -40,7 +39,6 @@ import type {
|
||||||
} from "echarts/components";
|
} from "echarts/components";
|
||||||
|
|
||||||
use([
|
use([
|
||||||
TitleComponent,
|
|
||||||
TooltipComponent,
|
TooltipComponent,
|
||||||
LegendComponent,
|
LegendComponent,
|
||||||
ToolboxComponent,
|
ToolboxComponent,
|
||||||
|
@ -51,7 +49,6 @@ use([
|
||||||
]);
|
]);
|
||||||
|
|
||||||
type EChartsOption = ComposeOption<
|
type EChartsOption = ComposeOption<
|
||||||
| TitleComponentOption
|
|
||||||
| TooltipComponentOption
|
| TooltipComponentOption
|
||||||
| LegendComponentOption
|
| LegendComponentOption
|
||||||
| ToolboxComponentOption
|
| ToolboxComponentOption
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
type="radio"
|
type="radio"
|
||||||
name="function-bar"
|
name="function-bar"
|
||||||
id="1"
|
id="1"
|
||||||
checked
|
:checked="checkID === 1"
|
||||||
@change="handleTabChange"
|
@change="handleTabChange"
|
||||||
/>
|
/>
|
||||||
<TerminalIcon class="icon" />
|
<TerminalIcon class="icon" />
|
||||||
|
@ -17,6 +17,7 @@
|
||||||
type="radio"
|
type="radio"
|
||||||
name="function-bar"
|
name="function-bar"
|
||||||
id="2"
|
id="2"
|
||||||
|
:checked="checkID === 2"
|
||||||
@change="handleTabChange"
|
@change="handleTabChange"
|
||||||
/>
|
/>
|
||||||
<VideoIcon class="icon" />
|
<VideoIcon class="icon" />
|
||||||
|
@ -27,6 +28,7 @@
|
||||||
type="radio"
|
type="radio"
|
||||||
name="function-bar"
|
name="function-bar"
|
||||||
id="3"
|
id="3"
|
||||||
|
:checked="checkID === 3"
|
||||||
@change="handleTabChange"
|
@change="handleTabChange"
|
||||||
/>
|
/>
|
||||||
<SquareActivityIcon class="icon" />
|
<SquareActivityIcon class="icon" />
|
||||||
|
@ -37,6 +39,7 @@
|
||||||
type="radio"
|
type="radio"
|
||||||
name="function-bar"
|
name="function-bar"
|
||||||
id="4"
|
id="4"
|
||||||
|
:checked="checkID === 4"
|
||||||
@change="handleTabChange"
|
@change="handleTabChange"
|
||||||
/>
|
/>
|
||||||
<Zap class="icon" />
|
<Zap class="icon" />
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<Zap class="w-5 h-5" />
|
<Zap class="w-5 h-5" />
|
||||||
逻辑信号分析
|
逻辑信号分析
|
||||||
</h2>
|
</h2>
|
||||||
<LogicalWaveFormDisplay />
|
<LogicalWaveFormDisplay :data="generateTestLogicData()" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Zap, Settings, Layers } from "lucide-vue-next";
|
import { Zap, Settings, Layers } from "lucide-vue-next";
|
||||||
import { useEquipments } from "@/stores/equipments";
|
import { useEquipments } from "@/stores/equipments";
|
||||||
import LogicalWaveFormDisplay from "@/components/LogicAnalyzer/LogicalWaveFormDisplay.vue";
|
import { LogicalWaveFormDisplay, generateTestLogicData } from "@/components/LogicAnalyzer";
|
||||||
|
|
||||||
// 使用全局设备配置
|
// 使用全局设备配置
|
||||||
const equipments = useEquipments();
|
const equipments = useEquipments();
|
||||||
|
|
Loading…
Reference in New Issue