feat: 添加逻辑分析仪
This commit is contained in:
		@@ -1,6 +1,12 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="w-full h-100">
 | 
			
		||||
    <v-chart v-if="true" class="w-full h-full" :option="option" autoresize />
 | 
			
		||||
  <div class="w-full h-150">
 | 
			
		||||
    <v-chart 
 | 
			
		||||
      v-if="data" 
 | 
			
		||||
      class="w-full h-full" 
 | 
			
		||||
      :option="option" 
 | 
			
		||||
      autoresize 
 | 
			
		||||
      :update-options="updateOptions"
 | 
			
		||||
    />
 | 
			
		||||
    <div
 | 
			
		||||
      v-else
 | 
			
		||||
      class="w-full h-full flex items-center justify-center text-gray-500"
 | 
			
		||||
@@ -11,144 +17,194 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed } from "vue";
 | 
			
		||||
import { computed, shallowRef } from "vue";
 | 
			
		||||
import VChart from "vue-echarts";
 | 
			
		||||
import type { LogicDataType } from "./index";
 | 
			
		||||
 | 
			
		||||
// Echarts
 | 
			
		||||
import { use } from "echarts/core";
 | 
			
		||||
import { LineChart } from "echarts/charts";
 | 
			
		||||
import {
 | 
			
		||||
  TitleComponent,
 | 
			
		||||
  TooltipComponent,
 | 
			
		||||
  LegendComponent,
 | 
			
		||||
  ToolboxComponent,
 | 
			
		||||
  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 {
 | 
			
		||||
  TitleComponentOption,
 | 
			
		||||
  AxisPointerComponentOption,
 | 
			
		||||
  TooltipComponentOption,
 | 
			
		||||
  LegendComponentOption,
 | 
			
		||||
  ToolboxComponentOption,
 | 
			
		||||
  GridComponentOption,
 | 
			
		||||
  DataZoomComponentOption,
 | 
			
		||||
} from "echarts/components";
 | 
			
		||||
import type {
 | 
			
		||||
  ToolboxComponentOption,
 | 
			
		||||
  XAXisOption,
 | 
			
		||||
  YAXisOption,
 | 
			
		||||
} from "echarts/types/dist/shared";
 | 
			
		||||
 | 
			
		||||
use([
 | 
			
		||||
  TitleComponent,
 | 
			
		||||
  TooltipComponent,
 | 
			
		||||
  LegendComponent,
 | 
			
		||||
  ToolboxComponent,
 | 
			
		||||
  GridComponent,
 | 
			
		||||
  AxisPointerComponent,
 | 
			
		||||
  DataZoomComponent,
 | 
			
		||||
  LineChart,
 | 
			
		||||
  CanvasRenderer,
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
type EChartsOption = ComposeOption<
 | 
			
		||||
  | TitleComponentOption
 | 
			
		||||
  | AxisPointerComponentOption
 | 
			
		||||
  | TooltipComponentOption
 | 
			
		||||
  | LegendComponentOption
 | 
			
		||||
  | ToolboxComponentOption
 | 
			
		||||
  | GridComponentOption
 | 
			
		||||
  | DataZoomComponentOption
 | 
			
		||||
  | LineSeriesOption
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
// Define props
 | 
			
		||||
interface Props {
 | 
			
		||||
  data?: LogicDataType;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = defineProps<Props>();
 | 
			
		||||
 | 
			
		||||
// 添加更新选项来减少重绘
 | 
			
		||||
const updateOptions = shallowRef({
 | 
			
		||||
  notMerge: false,
 | 
			
		||||
  lazyUpdate: true,
 | 
			
		||||
  silent: false
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const option = computed((): EChartsOption => {
 | 
			
		||||
  return {
 | 
			
		||||
    title: {
 | 
			
		||||
      text: "Stacked Area Chart",
 | 
			
		||||
  if (!props.data) return {};
 | 
			
		||||
 | 
			
		||||
  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: {
 | 
			
		||||
      trigger: "axis",
 | 
			
		||||
      axisPointer: {
 | 
			
		||||
        type: "cross",
 | 
			
		||||
        type: "line",
 | 
			
		||||
        label: {
 | 
			
		||||
          backgroundColor: "#6a7985",
 | 
			
		||||
        },
 | 
			
		||||
        // 减少axisPointer的动画
 | 
			
		||||
        animation: false,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    legend: {
 | 
			
		||||
      data: ["Email", "Union Ads", "Video Ads", "Direct", "Search Engine"],
 | 
			
		||||
      formatter: (params: any) => {
 | 
			
		||||
        if (Array.isArray(params) && params.length > 0) {
 | 
			
		||||
          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: {
 | 
			
		||||
      feature: {
 | 
			
		||||
        saveAsImage: {},
 | 
			
		||||
        restore: {},
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    grid: {
 | 
			
		||||
      left: "3%",
 | 
			
		||||
      right: "4%",
 | 
			
		||||
      bottom: "3%",
 | 
			
		||||
      containLabel: true,
 | 
			
		||||
    },
 | 
			
		||||
    xAxis: [
 | 
			
		||||
    grid: grids,
 | 
			
		||||
    xAxis: xAxis,
 | 
			
		||||
    yAxis: yAxis,
 | 
			
		||||
    dataZoom: [
 | 
			
		||||
      {
 | 
			
		||||
        type: "category",
 | 
			
		||||
        boundaryGap: false,
 | 
			
		||||
        data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
    yAxis: [
 | 
			
		||||
      {
 | 
			
		||||
        type: "value",
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
    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],
 | 
			
		||||
        show: true,
 | 
			
		||||
        realtime: true,
 | 
			
		||||
        start: 0,
 | 
			
		||||
        end: 100,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        type: "inside",
 | 
			
		||||
        realtime: true,
 | 
			
		||||
        start: 0,
 | 
			
		||||
        end: 100,
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
    series: series,
 | 
			
		||||
  };
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										76
									
								
								src/components/LogicAnalyzer/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/components/LogicAnalyzer/index.ts
									
									
									
									
									
										Normal 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 };
 | 
			
		||||
@@ -31,7 +31,6 @@ import { CanvasRenderer } from "echarts/renderers";
 | 
			
		||||
import type { ComposeOption } from "echarts/core";
 | 
			
		||||
import type { LineSeriesOption } from "echarts/charts";
 | 
			
		||||
import type {
 | 
			
		||||
  TitleComponentOption,
 | 
			
		||||
  TooltipComponentOption,
 | 
			
		||||
  LegendComponentOption,
 | 
			
		||||
  ToolboxComponentOption,
 | 
			
		||||
@@ -40,7 +39,6 @@ import type {
 | 
			
		||||
} from "echarts/components";
 | 
			
		||||
 | 
			
		||||
use([
 | 
			
		||||
  TitleComponent,
 | 
			
		||||
  TooltipComponent,
 | 
			
		||||
  LegendComponent,
 | 
			
		||||
  ToolboxComponent,
 | 
			
		||||
@@ -51,7 +49,6 @@ use([
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
type EChartsOption = ComposeOption<
 | 
			
		||||
  | TitleComponentOption
 | 
			
		||||
  | TooltipComponentOption
 | 
			
		||||
  | LegendComponentOption
 | 
			
		||||
  | ToolboxComponentOption
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@
 | 
			
		||||
          type="radio"
 | 
			
		||||
          name="function-bar"
 | 
			
		||||
          id="1"
 | 
			
		||||
          checked
 | 
			
		||||
          :checked="checkID === 1"
 | 
			
		||||
          @change="handleTabChange"
 | 
			
		||||
        />
 | 
			
		||||
        <TerminalIcon class="icon" />
 | 
			
		||||
@@ -17,6 +17,7 @@
 | 
			
		||||
          type="radio"
 | 
			
		||||
          name="function-bar"
 | 
			
		||||
          id="2"
 | 
			
		||||
          :checked="checkID === 2"
 | 
			
		||||
          @change="handleTabChange"
 | 
			
		||||
        />
 | 
			
		||||
        <VideoIcon class="icon" />
 | 
			
		||||
@@ -27,6 +28,7 @@
 | 
			
		||||
          type="radio"
 | 
			
		||||
          name="function-bar"
 | 
			
		||||
          id="3"
 | 
			
		||||
          :checked="checkID === 3"
 | 
			
		||||
          @change="handleTabChange"
 | 
			
		||||
        />
 | 
			
		||||
        <SquareActivityIcon class="icon" />
 | 
			
		||||
@@ -37,6 +39,7 @@
 | 
			
		||||
          type="radio"
 | 
			
		||||
          name="function-bar"
 | 
			
		||||
          id="4"
 | 
			
		||||
          :checked="checkID === 4"
 | 
			
		||||
          @change="handleTabChange"
 | 
			
		||||
        />
 | 
			
		||||
        <Zap class="icon" />
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
          <Zap class="w-5 h-5" />
 | 
			
		||||
          逻辑信号分析
 | 
			
		||||
        </h2>
 | 
			
		||||
        <LogicalWaveFormDisplay />
 | 
			
		||||
        <LogicalWaveFormDisplay :data="generateTestLogicData()" />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@@ -38,7 +38,7 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { Zap, Settings, Layers } from "lucide-vue-next";
 | 
			
		||||
import { useEquipments } from "@/stores/equipments";
 | 
			
		||||
import LogicalWaveFormDisplay from "@/components/LogicAnalyzer/LogicalWaveFormDisplay.vue";
 | 
			
		||||
import { LogicalWaveFormDisplay, generateTestLogicData } from "@/components/LogicAnalyzer";
 | 
			
		||||
 | 
			
		||||
// 使用全局设备配置
 | 
			
		||||
const equipments = useEquipments();
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user