feat: 添加逻辑分析仪

This commit is contained in:
SikongJueluo 2025-07-14 16:07:37 +08:00
parent e8a16fd446
commit 4d6c06a0e0
No known key found for this signature in database
5 changed files with 230 additions and 98 deletions

View File

@ -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/>`;
// 01
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>

View File

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

View File

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

View File

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

View File

@ -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();