feat: 使用SignalR实时发送示波器数据,并美化示波器界面
This commit is contained in:
		@@ -88,7 +88,8 @@ try
 | 
			
		||||
                            path.StartsWithSegments("/hubs/JtagHub") ||
 | 
			
		||||
                            path.StartsWithSegments("/hubs/ProgressHub") ||
 | 
			
		||||
                            path.StartsWithSegments("/hubs/DigitalTubesHub") ||
 | 
			
		||||
                            path.StartsWithSegments("/hubs/RotaryEncoderHub")
 | 
			
		||||
                            path.StartsWithSegments("/hubs/RotaryEncoderHub") ||
 | 
			
		||||
                            path.StartsWithSegments("/hubs/OscilloscopeHub")
 | 
			
		||||
                        ))
 | 
			
		||||
                    {
 | 
			
		||||
                        // Read the token out of the query string
 | 
			
		||||
@@ -256,6 +257,7 @@ try
 | 
			
		||||
    app.MapHub<server.Hubs.ProgressHub>("/hubs/ProgressHub");
 | 
			
		||||
    app.MapHub<server.Hubs.DigitalTubesHub>("/hubs/DigitalTubesHub");
 | 
			
		||||
    app.MapHub<server.Hubs.RotaryEncoderHub>("/hubs/RotaryEncoderHub");
 | 
			
		||||
    app.MapHub<server.Hubs.OscilloscopeHub>("/hubs/OscilloscopeHub");
 | 
			
		||||
 | 
			
		||||
    // Setup Program
 | 
			
		||||
    MsgBus.Init();
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authorization;
 | 
			
		||||
using Microsoft.AspNetCore.Cors;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
using Peripherals.OscilloscopeClient;
 | 
			
		||||
using server.Hubs;
 | 
			
		||||
 | 
			
		||||
namespace server.Controllers;
 | 
			
		||||
 | 
			
		||||
@@ -9,6 +10,7 @@ namespace server.Controllers;
 | 
			
		||||
/// 示波器API控制器 - 普通用户权限
 | 
			
		||||
/// </summary>
 | 
			
		||||
[ApiController]
 | 
			
		||||
[EnableCors("Development")]
 | 
			
		||||
[Route("api/[controller]")]
 | 
			
		||||
[Authorize]
 | 
			
		||||
public class OscilloscopeApiController : ControllerBase
 | 
			
		||||
@@ -20,7 +22,7 @@ public class OscilloscopeApiController : ControllerBase
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取示波器实例
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    private Oscilloscope? GetOscilloscope()
 | 
			
		||||
    private OscilloscopeCtrl? GetOscilloscope()
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
@@ -41,7 +43,7 @@ public class OscilloscopeApiController : ControllerBase
 | 
			
		||||
                return null;
 | 
			
		||||
 | 
			
		||||
            var board = boardRet.Value.Value;
 | 
			
		||||
            return new Oscilloscope(board.IpAddr, board.Port);
 | 
			
		||||
            return new OscilloscopeCtrl(board.IpAddr, board.Port);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
@@ -56,12 +58,11 @@ public class OscilloscopeApiController : ControllerBase
 | 
			
		||||
    /// <param name="config">示波器配置</param>
 | 
			
		||||
    /// <returns>操作结果</returns>
 | 
			
		||||
    [HttpPost("Initialize")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
 | 
			
		||||
    public async Task<IActionResult> Initialize([FromBody] OscilloscopeFullConfig config)
 | 
			
		||||
    public async Task<IActionResult> Initialize([FromBody] OscilloscopeConfig config)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
@@ -151,7 +152,6 @@ public class OscilloscopeApiController : ControllerBase
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>操作结果</returns>
 | 
			
		||||
    [HttpPost("StartCapture")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
@@ -185,7 +185,6 @@ public class OscilloscopeApiController : ControllerBase
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>操作结果</returns>
 | 
			
		||||
    [HttpPost("StopCapture")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
@@ -219,7 +218,6 @@ public class OscilloscopeApiController : ControllerBase
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>示波器数据和状态信息</returns>
 | 
			
		||||
    [HttpGet("GetData")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(OscilloscopeDataResponse), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
@@ -293,7 +291,6 @@ public class OscilloscopeApiController : ControllerBase
 | 
			
		||||
    /// <param name="risingEdge">触发边沿(true为上升沿,false为下降沿)</param>
 | 
			
		||||
    /// <returns>操作结果</returns>
 | 
			
		||||
    [HttpPost("UpdateTrigger")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
@@ -338,7 +335,6 @@ public class OscilloscopeApiController : ControllerBase
 | 
			
		||||
    /// <param name="decimationRate">抽样率(0-1023)</param>
 | 
			
		||||
    /// <returns>操作结果</returns>
 | 
			
		||||
    [HttpPost("UpdateSampling")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
@@ -387,7 +383,6 @@ public class OscilloscopeApiController : ControllerBase
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>操作结果</returns>
 | 
			
		||||
    [HttpPost("RefreshRAM")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
@@ -415,72 +410,4 @@ public class OscilloscopeApiController : ControllerBase
 | 
			
		||||
            return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 示波器完整配置
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class OscilloscopeFullConfig
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 是否启动捕获
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public bool CaptureEnabled { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 触发电平(0-255)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public byte TriggerLevel { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 触发边沿(true为上升沿,false为下降沿)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public bool TriggerRisingEdge { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 水平偏移量(0-1023)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public ushort HorizontalShift { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 抽样率(0-1023)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public ushort DecimationRate { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 是否自动刷新RAM
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public bool AutoRefreshRAM { get; set; } = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 示波器状态和数据
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class OscilloscopeDataResponse
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// AD采样频率
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public uint ADFrequency { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// AD采样幅度
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public byte ADVpp { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// AD采样最大值
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public byte ADMax { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// AD采样最小值
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public byte ADMin { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 波形数据(Base64编码)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public string WaveformData { get; set; } = string.Empty;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -32,15 +32,9 @@ public class DigitalTubeTaskStatus
 | 
			
		||||
{
 | 
			
		||||
    public int Frequency { get; set; } = 100;
 | 
			
		||||
    public bool IsRunning { get; set; } = false;
 | 
			
		||||
 | 
			
		||||
    public DigitalTubeTaskStatus(ScanTaskInfo info)
 | 
			
		||||
    {
 | 
			
		||||
        Frequency = info.Frequency;
 | 
			
		||||
        IsRunning = info.IsRunning;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class ScanTaskInfo
 | 
			
		||||
class DigitalTubesScanTaskInfo
 | 
			
		||||
{
 | 
			
		||||
    public string BoardID { get; set; }
 | 
			
		||||
    public string ClientID { get; set; }
 | 
			
		||||
@@ -50,13 +44,22 @@ public class ScanTaskInfo
 | 
			
		||||
    public int Frequency { get; set; } = 100;
 | 
			
		||||
    public bool IsRunning { get; set; } = false;
 | 
			
		||||
 | 
			
		||||
    public ScanTaskInfo(
 | 
			
		||||
    public DigitalTubesScanTaskInfo(
 | 
			
		||||
        string boardID, string clientID, SevenDigitalTubesCtrl client)
 | 
			
		||||
    {
 | 
			
		||||
        BoardID = boardID;
 | 
			
		||||
        ClientID = clientID;
 | 
			
		||||
        TubeClient = client;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public DigitalTubeTaskStatus ToDigitalTubeTaskStatus()
 | 
			
		||||
    {
 | 
			
		||||
        return new DigitalTubeTaskStatus
 | 
			
		||||
        {
 | 
			
		||||
            Frequency = Frequency,
 | 
			
		||||
            IsRunning = IsRunning
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
[Authorize]
 | 
			
		||||
@@ -67,7 +70,7 @@ public class DigitalTubesHub : Hub<IDigitalTubesReceiver>, IDigitalTubesHub
 | 
			
		||||
    private readonly IHubContext<DigitalTubesHub, IDigitalTubesReceiver> _hubContext;
 | 
			
		||||
    private readonly Database.UserManager _userManager = new();
 | 
			
		||||
 | 
			
		||||
    private ConcurrentDictionary<(string, string), ScanTaskInfo> _scanTasks = new();
 | 
			
		||||
    private ConcurrentDictionary<(string, string), DigitalTubesScanTaskInfo> _scanTasks = new();
 | 
			
		||||
 | 
			
		||||
    public DigitalTubesHub(IHubContext<DigitalTubesHub, IDigitalTubesReceiver> hubContext)
 | 
			
		||||
    {
 | 
			
		||||
@@ -100,7 +103,7 @@ public class DigitalTubesHub : Hub<IDigitalTubesReceiver>, IDigitalTubesHub
 | 
			
		||||
        return boardRet.Value.Value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task ScanAllTubes(ScanTaskInfo scanInfo)
 | 
			
		||||
    private Task ScanAllTubes(DigitalTubesScanTaskInfo scanInfo)
 | 
			
		||||
    {
 | 
			
		||||
        var token = scanInfo.CTS.Token;
 | 
			
		||||
        return Task.Run(async () =>
 | 
			
		||||
@@ -163,7 +166,7 @@ public class DigitalTubesHub : Hub<IDigitalTubesReceiver>, IDigitalTubesHub
 | 
			
		||||
                return true;
 | 
			
		||||
 | 
			
		||||
            var cts = new CancellationTokenSource();
 | 
			
		||||
            var scanTaskInfo = new ScanTaskInfo(
 | 
			
		||||
            var scanTaskInfo = new DigitalTubesScanTaskInfo(
 | 
			
		||||
                board.ID.ToString(), Context.ConnectionId,
 | 
			
		||||
                new SevenDigitalTubesCtrl(board.IpAddr, board.Port, 0)
 | 
			
		||||
            );
 | 
			
		||||
@@ -240,7 +243,7 @@ public class DigitalTubesHub : Hub<IDigitalTubesReceiver>, IDigitalTubesHub
 | 
			
		||||
 | 
			
		||||
            if (_scanTasks.TryGetValue(key, out var scanInfo))
 | 
			
		||||
            {
 | 
			
		||||
                return new DigitalTubeTaskStatus(scanInfo);
 | 
			
		||||
                return scanInfo.ToDigitalTubeTaskStatus();
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										386
									
								
								server/src/Hubs/OscilloscopeHub.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										386
									
								
								server/src/Hubs/OscilloscopeHub.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,386 @@
 | 
			
		||||
using Microsoft.AspNetCore.Authorization;
 | 
			
		||||
using System.Security.Claims;
 | 
			
		||||
using Microsoft.AspNetCore.SignalR;
 | 
			
		||||
using Microsoft.AspNetCore.Cors;
 | 
			
		||||
using TypedSignalR.Client;
 | 
			
		||||
using DotNext;
 | 
			
		||||
using Tapper;
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using Peripherals.OscilloscopeClient;
 | 
			
		||||
 | 
			
		||||
#pragma warning disable 1998
 | 
			
		||||
 | 
			
		||||
namespace server.Hubs;
 | 
			
		||||
 | 
			
		||||
[Hub]
 | 
			
		||||
public interface IOscilloscopeHub
 | 
			
		||||
{
 | 
			
		||||
    Task<bool> Initialize(OscilloscopeFullConfig config);
 | 
			
		||||
    Task<bool> StartCapture();
 | 
			
		||||
    Task<bool> StopCapture();
 | 
			
		||||
    Task<OscilloscopeDataResponse?> GetData();
 | 
			
		||||
    Task<bool> SetTrigger(byte level);
 | 
			
		||||
    Task<bool> SetRisingEdge(bool risingEdge);
 | 
			
		||||
    Task<bool> SetSampling(ushort decimationRate);
 | 
			
		||||
    Task<bool> SetFrequency(int frequency);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
[Receiver]
 | 
			
		||||
public interface IOscilloscopeReceiver
 | 
			
		||||
{
 | 
			
		||||
    Task OnDataReceived(OscilloscopeDataResponse data);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
[TranspilationSource]
 | 
			
		||||
public class OscilloscopeDataResponse
 | 
			
		||||
{
 | 
			
		||||
    public uint ADFrequency { get; set; }
 | 
			
		||||
    public byte ADVpp { get; set; }
 | 
			
		||||
    public byte ADMax { get; set; }
 | 
			
		||||
    public byte ADMin { get; set; }
 | 
			
		||||
    public string WaveformData { get; set; } = "";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
[TranspilationSource]
 | 
			
		||||
public class OscilloscopeFullConfig
 | 
			
		||||
{
 | 
			
		||||
    public bool CaptureEnabled { get; set; }
 | 
			
		||||
    public byte TriggerLevel { get; set; }
 | 
			
		||||
    public bool TriggerRisingEdge { get; set; }
 | 
			
		||||
    public ushort HorizontalShift { get; set; }
 | 
			
		||||
    public ushort DecimationRate { get; set; }
 | 
			
		||||
    public int CaptureFrequency { get; set; }
 | 
			
		||||
    // public bool AutoRefreshRAM { get; set; }
 | 
			
		||||
 | 
			
		||||
    public OscilloscopeConfig ToOscilloscopeConfig()
 | 
			
		||||
    {
 | 
			
		||||
        return new OscilloscopeConfig
 | 
			
		||||
        {
 | 
			
		||||
            CaptureEnabled = CaptureEnabled,
 | 
			
		||||
            TriggerLevel = TriggerLevel,
 | 
			
		||||
            TriggerRisingEdge = TriggerRisingEdge,
 | 
			
		||||
            HorizontalShift = HorizontalShift,
 | 
			
		||||
            DecimationRate = DecimationRate,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class OscilloscopeScanTaskInfo
 | 
			
		||||
{
 | 
			
		||||
    public Task? ScanTask { get; set; }
 | 
			
		||||
    public OscilloscopeCtrl Client { get; set; }
 | 
			
		||||
    public CancellationTokenSource CTS { get; set; } = new();
 | 
			
		||||
    public int Frequency { get; set; } = 100;
 | 
			
		||||
    public bool IsRunning { get; set; } = false;
 | 
			
		||||
 | 
			
		||||
    public OscilloscopeScanTaskInfo(OscilloscopeCtrl client)
 | 
			
		||||
    {
 | 
			
		||||
        Client = client;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
[Authorize]
 | 
			
		||||
[EnableCors("SignalR")]
 | 
			
		||||
public class OscilloscopeHub : Hub<IOscilloscopeReceiver>, IOscilloscopeHub
 | 
			
		||||
{
 | 
			
		||||
    private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
			
		||||
    private readonly IHubContext<OscilloscopeHub, IOscilloscopeReceiver> _hubContext;
 | 
			
		||||
    private readonly Database.UserManager _userManager = new();
 | 
			
		||||
 | 
			
		||||
    private ConcurrentDictionary<(string, string), OscilloscopeScanTaskInfo> _scanTasks = new();
 | 
			
		||||
 | 
			
		||||
    public OscilloscopeHub(IHubContext<OscilloscopeHub, IOscilloscopeReceiver> hubContext)
 | 
			
		||||
    {
 | 
			
		||||
        _hubContext = hubContext;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Optional<Database.Board> TryGetBoard()
 | 
			
		||||
    {
 | 
			
		||||
        var userName = Context.User?.FindFirstValue(ClaimTypes.Name);
 | 
			
		||||
        if (string.IsNullOrEmpty(userName))
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error("User name is null or empty");
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var boardRet = _userManager.GetBoardByUserName(userName);
 | 
			
		||||
        if (!boardRet.IsSuccessful || !boardRet.Value.HasValue)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"Board not found");
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
        return boardRet.Value.Value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Optional<OscilloscopeCtrl> GetOscilloscope()
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
 | 
			
		||||
            var client = new OscilloscopeCtrl(board.IpAddr, board.Port, 0);
 | 
			
		||||
            return client;
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error(ex, "Failed to get oscilloscope");
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> Initialize(OscilloscopeFullConfig config)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var client = GetOscilloscope().OrThrow(() => new Exception("Oscilloscope not found"));
 | 
			
		||||
 | 
			
		||||
            var result = await client.Init(config.ToOscilloscopeConfig());
 | 
			
		||||
            if (!result.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error(result.Error, "Initialize failed");
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
            return result.Value;
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error(ex, "Failed to initialize oscilloscope");
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> StartCapture()
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
 | 
			
		||||
            var key = (board.ID.ToString(), Context.ConnectionId);
 | 
			
		||||
            var client = GetOscilloscope().OrThrow(() => new Exception("Oscilloscope not found"));
 | 
			
		||||
 | 
			
		||||
            if (_scanTasks.TryGetValue(key, out var existing) && existing.IsRunning)
 | 
			
		||||
                return true;
 | 
			
		||||
 | 
			
		||||
            var result = await client.SetCaptureEnable(true);
 | 
			
		||||
            if (!result.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error(result.Error, "StartCapture failed");
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var scanTaskInfo = new OscilloscopeScanTaskInfo(client);
 | 
			
		||||
            var token = scanTaskInfo.CTS.Token;
 | 
			
		||||
            scanTaskInfo.ScanTask = Task.Run(async () =>
 | 
			
		||||
            {
 | 
			
		||||
                while (!token.IsCancellationRequested)
 | 
			
		||||
                {
 | 
			
		||||
                    var data = await GetData();
 | 
			
		||||
                    if (data == null)
 | 
			
		||||
                    {
 | 
			
		||||
                        logger.Error("GetData failed");
 | 
			
		||||
                        continue;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    await Clients.Client(Context.ConnectionId).OnDataReceived(data);
 | 
			
		||||
                    await Task.Delay(1000 / scanTaskInfo.Frequency, token);
 | 
			
		||||
                }
 | 
			
		||||
            }, token);
 | 
			
		||||
 | 
			
		||||
            _scanTasks[key] = scanTaskInfo;
 | 
			
		||||
 | 
			
		||||
            return result.Value;
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error(ex, "Failed to start capture");
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> StopCapture()
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
 | 
			
		||||
            var client = GetOscilloscope().OrThrow(() => new Exception("Oscilloscope not found"));
 | 
			
		||||
 | 
			
		||||
            var key = (board.ID.ToString(), Context.ConnectionId);
 | 
			
		||||
 | 
			
		||||
            if (_scanTasks.TryRemove(key, out var scanInfo))
 | 
			
		||||
            {
 | 
			
		||||
                scanInfo.IsRunning = false;
 | 
			
		||||
                scanInfo.CTS.Cancel();
 | 
			
		||||
                if (scanInfo.ScanTask != null) await scanInfo.ScanTask;
 | 
			
		||||
                scanInfo.CTS.Dispose();
 | 
			
		||||
 | 
			
		||||
                var result = await client.SetCaptureEnable(false);
 | 
			
		||||
                if (!result.IsSuccessful)
 | 
			
		||||
                {
 | 
			
		||||
                    logger.Error(result.Error, "StopCapture failed");
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
                return result.Value;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            throw new Exception("Task not found");
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error(ex, "Failed to stop capture");
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<OscilloscopeDataResponse?> GetData()
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var oscilloscope = GetOscilloscope().OrThrow(() => new Exception("用户未绑定有效的实验板"));
 | 
			
		||||
 | 
			
		||||
            var freqResult = await oscilloscope.GetADFrequency();
 | 
			
		||||
            var vppResult = await oscilloscope.GetADVpp();
 | 
			
		||||
            var maxResult = await oscilloscope.GetADMax();
 | 
			
		||||
            var minResult = await oscilloscope.GetADMin();
 | 
			
		||||
            var waveformResult = await oscilloscope.GetWaveformData();
 | 
			
		||||
 | 
			
		||||
            if (!freqResult.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"获取AD采样频率失败: {freqResult.Error}");
 | 
			
		||||
                throw new Exception($"获取AD采样频率失败: {freqResult.Error}");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!vppResult.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"获取AD采样幅度失败: {vppResult.Error}");
 | 
			
		||||
                throw new Exception($"获取AD采样幅度失败: {vppResult.Error}");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!maxResult.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"获取AD采样最大值失败: {maxResult.Error}");
 | 
			
		||||
                throw new Exception($"获取AD采样最大值失败: {maxResult.Error}");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!minResult.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"获取AD采样最小值失败: {minResult.Error}");
 | 
			
		||||
                throw new Exception($"获取AD采样最小值失败: {minResult.Error}");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!waveformResult.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"获取波形数据失败: {waveformResult.Error}");
 | 
			
		||||
                throw new Exception($"获取波形数据失败: {waveformResult.Error}");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var response = new OscilloscopeDataResponse
 | 
			
		||||
            {
 | 
			
		||||
                ADFrequency = freqResult.Value,
 | 
			
		||||
                ADVpp = vppResult.Value,
 | 
			
		||||
                ADMax = maxResult.Value,
 | 
			
		||||
                ADMin = minResult.Value,
 | 
			
		||||
                WaveformData = Convert.ToBase64String(waveformResult.Value)
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            return new OscilloscopeDataResponse
 | 
			
		||||
            {
 | 
			
		||||
                ADFrequency = freqResult.Value,
 | 
			
		||||
                ADVpp = vppResult.Value,
 | 
			
		||||
                ADMax = maxResult.Value,
 | 
			
		||||
                ADMin = minResult.Value,
 | 
			
		||||
                WaveformData = Convert.ToBase64String(waveformResult.Value)
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error(ex, "获取示波器数据时发生异常");
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> SetTrigger(byte level)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var client = GetOscilloscope().OrThrow(() => new Exception("Oscilloscope not found"));
 | 
			
		||||
            var ret = await client.SetTriggerLevel(level);
 | 
			
		||||
            if (!ret.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error(ret.Error, "UpdateTrigger failed");
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
            return ret.Value;
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error(ex, "Failed to update trigger");
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> SetRisingEdge(bool risingEdge)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var client = GetOscilloscope().OrThrow(() => new Exception("Oscilloscope not found"));
 | 
			
		||||
            var ret = await client.SetTriggerEdge(risingEdge);
 | 
			
		||||
            if (!ret.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error(ret.Error, "Update Rising Edge failed");
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
            return ret.Value;
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error(ex, "SetRisingEdge failed");
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> SetSampling(ushort decimationRate)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var client = GetOscilloscope().OrThrow(() => new Exception("Oscilloscope not found"));
 | 
			
		||||
            var result = await client.SetDecimationRate(decimationRate);
 | 
			
		||||
            if (!result.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error(result.Error, "UpdateSampling failed");
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
            return result.Value;
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error(ex, "Failed to update sampling");
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> SetFrequency(int frequency)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            if (frequency < 1 || frequency > 1000)
 | 
			
		||||
                return false;
 | 
			
		||||
 | 
			
		||||
            var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
 | 
			
		||||
            var key = (board.ID.ToString(), Context.ConnectionId);
 | 
			
		||||
 | 
			
		||||
            if (_scanTasks.TryGetValue(key, out var scanInfo) && scanInfo.IsRunning)
 | 
			
		||||
            {
 | 
			
		||||
                scanInfo.Frequency = frequency;
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                logger.Warn($"SetFrequency called but no running scan for board {board.ID} and client {Context.ConnectionId}");
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error(ex, "Failed to set frequency");
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -2,9 +2,20 @@ using System.Net;
 | 
			
		||||
using Common;
 | 
			
		||||
using DotNext;
 | 
			
		||||
using WebProtocol;
 | 
			
		||||
using Tapper;
 | 
			
		||||
 | 
			
		||||
namespace Peripherals.OscilloscopeClient;
 | 
			
		||||
 | 
			
		||||
public class OscilloscopeConfig
 | 
			
		||||
{
 | 
			
		||||
    public bool CaptureEnabled { get; set; }
 | 
			
		||||
    public byte TriggerLevel { get; set; }
 | 
			
		||||
    public bool TriggerRisingEdge { get; set; }
 | 
			
		||||
    public ushort HorizontalShift { get; set; }
 | 
			
		||||
    public ushort DecimationRate { get; set; }
 | 
			
		||||
    // public bool AutoRefreshRAM { get; set; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static class OscilloscopeAddr
 | 
			
		||||
{
 | 
			
		||||
    const UInt32 BASE = 0x8000_0000;
 | 
			
		||||
@@ -71,7 +82,7 @@ static class OscilloscopeAddr
 | 
			
		||||
    public const UInt32 RD_DATA_LENGTH = 0x0000_0400;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class Oscilloscope
 | 
			
		||||
class OscilloscopeCtrl
 | 
			
		||||
{
 | 
			
		||||
    private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
			
		||||
 | 
			
		||||
@@ -88,7 +99,7 @@ class Oscilloscope
 | 
			
		||||
    /// <param name="address">示波器设备IP地址</param>
 | 
			
		||||
    /// <param name="port">示波器设备端口</param>
 | 
			
		||||
    /// <param name="timeout">超时时间(毫秒)</param>
 | 
			
		||||
    public Oscilloscope(string address, int port, int timeout = 2000)
 | 
			
		||||
    public OscilloscopeCtrl(string address, int port, int timeout = 2000)
 | 
			
		||||
    {
 | 
			
		||||
        if (timeout < 0)
 | 
			
		||||
            throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
 | 
			
		||||
@@ -98,6 +109,49 @@ class Oscilloscope
 | 
			
		||||
        this.timeout = timeout;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 一次性初始化/配置示波器
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="config">完整配置</param>
 | 
			
		||||
    /// <returns>操作结果,全部成功返回true,否则返回异常信息</returns>
 | 
			
		||||
    public async ValueTask<Result<bool>> Init(OscilloscopeConfig config)
 | 
			
		||||
    {
 | 
			
		||||
        // 1. 捕获使能
 | 
			
		||||
        var ret = await SetCaptureEnable(config.CaptureEnabled);
 | 
			
		||||
        if (!ret.IsSuccessful || !ret.Value)
 | 
			
		||||
            return new(ret.Error ?? new Exception("Failed to set capture enable"));
 | 
			
		||||
 | 
			
		||||
        // 2. 触发电平
 | 
			
		||||
        ret = await SetTriggerLevel(config.TriggerLevel);
 | 
			
		||||
        if (!ret.IsSuccessful || !ret.Value)
 | 
			
		||||
            return new(ret.Error ?? new Exception("Failed to set trigger level"));
 | 
			
		||||
 | 
			
		||||
        // 3. 触发边沿
 | 
			
		||||
        ret = await SetTriggerEdge(config.TriggerRisingEdge);
 | 
			
		||||
        if (!ret.IsSuccessful || !ret.Value)
 | 
			
		||||
            return new(ret.Error ?? new Exception("Failed to set trigger edge"));
 | 
			
		||||
 | 
			
		||||
        // 4. 水平偏移
 | 
			
		||||
        ret = await SetHorizontalShift(config.HorizontalShift);
 | 
			
		||||
        if (!ret.IsSuccessful || !ret.Value)
 | 
			
		||||
            return new(ret.Error ?? new Exception("Failed to set horizontal shift"));
 | 
			
		||||
 | 
			
		||||
        // 5. 抽样率
 | 
			
		||||
        ret = await SetDecimationRate(config.DecimationRate);
 | 
			
		||||
        if (!ret.IsSuccessful || !ret.Value)
 | 
			
		||||
            return new(ret.Error ?? new Exception("Failed to set decimation rate"));
 | 
			
		||||
 | 
			
		||||
        // 6. RAM刷新(如果需要)
 | 
			
		||||
        // if (config.AutoRefreshRAM)
 | 
			
		||||
        // {
 | 
			
		||||
        //     ret = await RefreshRAM();
 | 
			
		||||
        //     if (!ret.IsSuccessful || !ret.Value)
 | 
			
		||||
        //         return new(ret.Error ?? new Exception("Failed to refresh RAM"));
 | 
			
		||||
        // }
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 控制示波器的捕获开关
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -309,13 +363,13 @@ class Oscilloscope
 | 
			
		||||
        // 等待WAVE_READY[0]位为1,最多等待50ms(5次x10ms间隔)
 | 
			
		||||
        var readyResult = await UDPClientPool.ReadAddrWithWait(
 | 
			
		||||
            this.ep, this.taskID, OscilloscopeAddr.WAVE_READY, 0b00, 0x01, 10, 50);
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        if (!readyResult.IsSuccessful)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"Failed to wait for wave ready: {readyResult.Error}");
 | 
			
		||||
            return new(readyResult.Error);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        // 无论准备好与否,都继续读取数据(readyResult.Value表示是否在超时前准备好)
 | 
			
		||||
        if (!readyResult.Value)
 | 
			
		||||
        {
 | 
			
		||||
@@ -365,14 +419,14 @@ class Oscilloscope
 | 
			
		||||
            logger.Error("ReadAddr returned invalid data for trigger position");
 | 
			
		||||
            return new(new Exception("Failed to read trigger position"));
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        UInt32 trigAddr = Number.BytesToUInt32(trigPosResult.Value.Options.Data).Value;
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        // 根据触发地址对数据进行偏移,使触发点位于数据中间
 | 
			
		||||
        int targetPos = sampleCount / 2; // 目标位置:数据中间
 | 
			
		||||
        int actualTrigPos = (int)(trigAddr % (UInt32)sampleCount); // 实际触发位置
 | 
			
		||||
        int shiftAmount = targetPos - actualTrigPos;
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        // 创建偏移后的数据数组
 | 
			
		||||
        byte[] offsetData = new byte[sampleCount];
 | 
			
		||||
        for (int i = 0; i < sampleCount; i++)
 | 
			
		||||
 
 | 
			
		||||
@@ -299,7 +299,7 @@ export class VideoStreamClient {
 | 
			
		||||
        return Promise.resolve<boolean>(null as any);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setVideoStreamEnable(enable: boolean | undefined, cancelToken?: CancelToken): Promise<any> {
 | 
			
		||||
    setVideoStreamEnable(enable: boolean | undefined, cancelToken?: CancelToken): Promise<string> {
 | 
			
		||||
        let url_ = this.baseUrl + "/api/VideoStream/SetVideoStreamEnable?";
 | 
			
		||||
        if (enable === null)
 | 
			
		||||
            throw new Error("The parameter 'enable' cannot be null.");
 | 
			
		||||
@@ -327,7 +327,7 @@ export class VideoStreamClient {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected processSetVideoStreamEnable(response: AxiosResponse): Promise<any> {
 | 
			
		||||
    protected processSetVideoStreamEnable(response: AxiosResponse): Promise<string> {
 | 
			
		||||
        const status = response.status;
 | 
			
		||||
        let _headers: any = {};
 | 
			
		||||
        if (response.headers && typeof response.headers === "object") {
 | 
			
		||||
@@ -343,7 +343,7 @@ export class VideoStreamClient {
 | 
			
		||||
            let resultData200  = _responseText;
 | 
			
		||||
                result200 = resultData200 !== undefined ? resultData200 : <any>null;
 | 
			
		||||
    
 | 
			
		||||
            return Promise.resolve<any>(result200);
 | 
			
		||||
            return Promise.resolve<string>(result200);
 | 
			
		||||
 | 
			
		||||
        } else if (status === 500) {
 | 
			
		||||
            const _responseText = response.data;
 | 
			
		||||
@@ -357,7 +357,7 @@ export class VideoStreamClient {
 | 
			
		||||
            const _responseText = response.data;
 | 
			
		||||
            return throwException("An unexpected server error occurred.", status, _responseText, _headers);
 | 
			
		||||
        }
 | 
			
		||||
        return Promise.resolve<any>(null as any);
 | 
			
		||||
        return Promise.resolve<string>(null as any);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -5569,7 +5569,7 @@ export class OscilloscopeApiClient {
 | 
			
		||||
     * @param config 示波器配置
 | 
			
		||||
     * @return 操作结果
 | 
			
		||||
     */
 | 
			
		||||
    initialize(config: OscilloscopeFullConfig, cancelToken?: CancelToken): Promise<boolean> {
 | 
			
		||||
    initialize(config: OscilloscopeConfig, cancelToken?: CancelToken): Promise<boolean> {
 | 
			
		||||
        let url_ = this.baseUrl + "/api/OscilloscopeApi/Initialize";
 | 
			
		||||
        url_ = url_.replace(/[?&]$/, "");
 | 
			
		||||
 | 
			
		||||
@@ -9090,22 +9090,14 @@ export interface INetworkInterfaceDto {
 | 
			
		||||
    macAddress: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 示波器完整配置 */
 | 
			
		||||
export class OscilloscopeFullConfig implements IOscilloscopeFullConfig {
 | 
			
		||||
    /** 是否启动捕获 */
 | 
			
		||||
export class OscilloscopeConfig implements IOscilloscopeConfig {
 | 
			
		||||
    captureEnabled!: boolean;
 | 
			
		||||
    /** 触发电平(0-255) */
 | 
			
		||||
    triggerLevel!: number;
 | 
			
		||||
    /** 触发边沿(true为上升沿,false为下降沿) */
 | 
			
		||||
    triggerRisingEdge!: boolean;
 | 
			
		||||
    /** 水平偏移量(0-1023) */
 | 
			
		||||
    horizontalShift!: number;
 | 
			
		||||
    /** 抽样率(0-1023) */
 | 
			
		||||
    decimationRate!: number;
 | 
			
		||||
    /** 是否自动刷新RAM */
 | 
			
		||||
    autoRefreshRAM!: boolean;
 | 
			
		||||
 | 
			
		||||
    constructor(data?: IOscilloscopeFullConfig) {
 | 
			
		||||
    constructor(data?: IOscilloscopeConfig) {
 | 
			
		||||
        if (data) {
 | 
			
		||||
            for (var property in data) {
 | 
			
		||||
                if (data.hasOwnProperty(property))
 | 
			
		||||
@@ -9121,13 +9113,12 @@ export class OscilloscopeFullConfig implements IOscilloscopeFullConfig {
 | 
			
		||||
            this.triggerRisingEdge = _data["triggerRisingEdge"];
 | 
			
		||||
            this.horizontalShift = _data["horizontalShift"];
 | 
			
		||||
            this.decimationRate = _data["decimationRate"];
 | 
			
		||||
            this.autoRefreshRAM = _data["autoRefreshRAM"];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static fromJS(data: any): OscilloscopeFullConfig {
 | 
			
		||||
    static fromJS(data: any): OscilloscopeConfig {
 | 
			
		||||
        data = typeof data === 'object' ? data : {};
 | 
			
		||||
        let result = new OscilloscopeFullConfig();
 | 
			
		||||
        let result = new OscilloscopeConfig();
 | 
			
		||||
        result.init(data);
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
@@ -9139,38 +9130,23 @@ export class OscilloscopeFullConfig implements IOscilloscopeFullConfig {
 | 
			
		||||
        data["triggerRisingEdge"] = this.triggerRisingEdge;
 | 
			
		||||
        data["horizontalShift"] = this.horizontalShift;
 | 
			
		||||
        data["decimationRate"] = this.decimationRate;
 | 
			
		||||
        data["autoRefreshRAM"] = this.autoRefreshRAM;
 | 
			
		||||
        return data;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 示波器完整配置 */
 | 
			
		||||
export interface IOscilloscopeFullConfig {
 | 
			
		||||
    /** 是否启动捕获 */
 | 
			
		||||
export interface IOscilloscopeConfig {
 | 
			
		||||
    captureEnabled: boolean;
 | 
			
		||||
    /** 触发电平(0-255) */
 | 
			
		||||
    triggerLevel: number;
 | 
			
		||||
    /** 触发边沿(true为上升沿,false为下降沿) */
 | 
			
		||||
    triggerRisingEdge: boolean;
 | 
			
		||||
    /** 水平偏移量(0-1023) */
 | 
			
		||||
    horizontalShift: number;
 | 
			
		||||
    /** 抽样率(0-1023) */
 | 
			
		||||
    decimationRate: number;
 | 
			
		||||
    /** 是否自动刷新RAM */
 | 
			
		||||
    autoRefreshRAM: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 示波器状态和数据 */
 | 
			
		||||
export class OscilloscopeDataResponse implements IOscilloscopeDataResponse {
 | 
			
		||||
    /** AD采样频率 */
 | 
			
		||||
    adFrequency!: number;
 | 
			
		||||
    /** AD采样幅度 */
 | 
			
		||||
    adVpp!: number;
 | 
			
		||||
    /** AD采样最大值 */
 | 
			
		||||
    adMax!: number;
 | 
			
		||||
    /** AD采样最小值 */
 | 
			
		||||
    adMin!: number;
 | 
			
		||||
    /** 波形数据(Base64编码) */
 | 
			
		||||
    waveformData!: string;
 | 
			
		||||
 | 
			
		||||
    constructor(data?: IOscilloscopeDataResponse) {
 | 
			
		||||
@@ -9210,17 +9186,11 @@ export class OscilloscopeDataResponse implements IOscilloscopeDataResponse {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 示波器状态和数据 */
 | 
			
		||||
export interface IOscilloscopeDataResponse {
 | 
			
		||||
    /** AD采样频率 */
 | 
			
		||||
    adFrequency: number;
 | 
			
		||||
    /** AD采样幅度 */
 | 
			
		||||
    adVpp: number;
 | 
			
		||||
    /** AD采样最大值 */
 | 
			
		||||
    adMax: number;
 | 
			
		||||
    /** AD采样最小值 */
 | 
			
		||||
    adMin: number;
 | 
			
		||||
    /** 波形数据(Base64编码) */
 | 
			
		||||
    waveformData: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,35 @@
 | 
			
		||||
import { autoResetRef, createInjectionState } from "@vueuse/core";
 | 
			
		||||
import { shallowRef, reactive, ref, computed } from "vue";
 | 
			
		||||
import { Mutex } from "async-mutex";
 | 
			
		||||
import {
 | 
			
		||||
  OscilloscopeFullConfig,
 | 
			
		||||
  OscilloscopeDataResponse,
 | 
			
		||||
  OscilloscopeApiClient,
 | 
			
		||||
} from "@/APIClient";
 | 
			
		||||
  autoResetRef,
 | 
			
		||||
  createInjectionState,
 | 
			
		||||
  watchDebounced,
 | 
			
		||||
} from "@vueuse/core";
 | 
			
		||||
import {
 | 
			
		||||
  shallowRef,
 | 
			
		||||
  reactive,
 | 
			
		||||
  ref,
 | 
			
		||||
  computed,
 | 
			
		||||
  onMounted,
 | 
			
		||||
  onUnmounted,
 | 
			
		||||
  watchEffect,
 | 
			
		||||
} from "vue";
 | 
			
		||||
import { Mutex } from "async-mutex";
 | 
			
		||||
import { OscilloscopeApiClient } from "@/APIClient";
 | 
			
		||||
import { AuthManager } from "@/utils/AuthManager";
 | 
			
		||||
import { useAlertStore } from "@/components/Alert";
 | 
			
		||||
import { useRequiredInjection } from "@/utils/Common";
 | 
			
		||||
import type { HubConnection } from "@microsoft/signalr";
 | 
			
		||||
import type {
 | 
			
		||||
  IOscilloscopeHub,
 | 
			
		||||
  IOscilloscopeReceiver,
 | 
			
		||||
} from "@/utils/signalR/TypedSignalR.Client/server.Hubs";
 | 
			
		||||
import {
 | 
			
		||||
  getHubProxyFactory,
 | 
			
		||||
  getReceiverRegister,
 | 
			
		||||
} from "@/utils/signalR/TypedSignalR.Client";
 | 
			
		||||
import type {
 | 
			
		||||
  OscilloscopeDataResponse,
 | 
			
		||||
  OscilloscopeFullConfig,
 | 
			
		||||
} from "@/utils/signalR/server.Hubs";
 | 
			
		||||
 | 
			
		||||
export type OscilloscopeDataType = {
 | 
			
		||||
  x: number[];
 | 
			
		||||
@@ -22,41 +43,103 @@ export type OscilloscopeDataType = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 默认配置
 | 
			
		||||
const DEFAULT_CONFIG: OscilloscopeFullConfig = new OscilloscopeFullConfig({
 | 
			
		||||
const DEFAULT_CONFIG: OscilloscopeFullConfig = {
 | 
			
		||||
  captureEnabled: false,
 | 
			
		||||
  triggerLevel: 128,
 | 
			
		||||
  triggerRisingEdge: true,
 | 
			
		||||
  horizontalShift: 0,
 | 
			
		||||
  decimationRate: 50,
 | 
			
		||||
  autoRefreshRAM: false,
 | 
			
		||||
});
 | 
			
		||||
  captureFrequency: 100,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 采样频率常量(后端返回)
 | 
			
		||||
const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(
 | 
			
		||||
  () => {
 | 
			
		||||
    const oscData = shallowRef<OscilloscopeDataType>();
 | 
			
		||||
    // Global Store
 | 
			
		||||
    const alert = useRequiredInjection(useAlertStore);
 | 
			
		||||
 | 
			
		||||
    // Data
 | 
			
		||||
    const oscData = shallowRef<OscilloscopeDataType>();
 | 
			
		||||
    const clearOscilloscopeData = () => {
 | 
			
		||||
      oscData.value = undefined;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // SignalR Hub
 | 
			
		||||
    const oscilloscopeHub = shallowRef<{
 | 
			
		||||
      connection: HubConnection;
 | 
			
		||||
      proxy: IOscilloscopeHub;
 | 
			
		||||
    } | null>(null);
 | 
			
		||||
 | 
			
		||||
    const oscilloscopeReceiver: IOscilloscopeReceiver = {
 | 
			
		||||
      onDataReceived: async (data) => {
 | 
			
		||||
        analyzeOscilloscopeData(data);
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    onMounted(() => {
 | 
			
		||||
      initHub();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    onUnmounted(() => {
 | 
			
		||||
      clearHub();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    function initHub() {
 | 
			
		||||
      if (oscilloscopeHub.value) return;
 | 
			
		||||
 | 
			
		||||
      const connection = AuthManager.createHubConnection("OscilloscopeHub");
 | 
			
		||||
 | 
			
		||||
      const proxy =
 | 
			
		||||
        getHubProxyFactory("IOscilloscopeHub").createHubProxy(connection);
 | 
			
		||||
 | 
			
		||||
      getReceiverRegister("IOscilloscopeReceiver").register(
 | 
			
		||||
        connection,
 | 
			
		||||
        oscilloscopeReceiver,
 | 
			
		||||
      );
 | 
			
		||||
      connection.start();
 | 
			
		||||
      oscilloscopeHub.value = { connection, proxy };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function clearHub() {
 | 
			
		||||
      if (!oscilloscopeHub.value) return;
 | 
			
		||||
      oscilloscopeHub.value.connection.stop();
 | 
			
		||||
      oscilloscopeHub.value = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function reinitializeHub() {
 | 
			
		||||
      clearHub();
 | 
			
		||||
      initHub();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getHubProxy() {
 | 
			
		||||
      if (!oscilloscopeHub.value) throw new Error("Hub not initialized");
 | 
			
		||||
      return oscilloscopeHub.value.proxy;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 互斥锁
 | 
			
		||||
    const operationMutex = new Mutex();
 | 
			
		||||
 | 
			
		||||
    // 状态
 | 
			
		||||
    const isApplying = ref(false);
 | 
			
		||||
    const isCapturing = ref(false);
 | 
			
		||||
    const isAutoApplying = ref(false);
 | 
			
		||||
 | 
			
		||||
    // 配置
 | 
			
		||||
    const config = reactive<OscilloscopeFullConfig>(
 | 
			
		||||
      new OscilloscopeFullConfig({ ...DEFAULT_CONFIG }),
 | 
			
		||||
    );
 | 
			
		||||
    const config = reactive<OscilloscopeFullConfig>({ ...DEFAULT_CONFIG });
 | 
			
		||||
    watchDebounced(
 | 
			
		||||
      config,
 | 
			
		||||
      () => {
 | 
			
		||||
        if (!isAutoApplying.value) return;
 | 
			
		||||
 | 
			
		||||
    // 采样点数(由后端数据决定)
 | 
			
		||||
    const sampleCount = ref(0);
 | 
			
		||||
 | 
			
		||||
    // 采样周期(ns),由adFrequency计算
 | 
			
		||||
    const samplePeriodNs = computed(() =>
 | 
			
		||||
      oscData.value?.adFrequency
 | 
			
		||||
        ? 1_000_000_000 / oscData.value.adFrequency
 | 
			
		||||
        : 200,
 | 
			
		||||
        if (
 | 
			
		||||
          !isApplying.value ||
 | 
			
		||||
          !isCapturing.value ||
 | 
			
		||||
          !operationMutex.isLocked()
 | 
			
		||||
        ) {
 | 
			
		||||
          applyConfiguration();
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      { debounce: 200, maxWait: 1000 },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // 应用配置
 | 
			
		||||
@@ -68,14 +151,18 @@ const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(
 | 
			
		||||
      const release = await operationMutex.acquire();
 | 
			
		||||
      isApplying.value = true;
 | 
			
		||||
      try {
 | 
			
		||||
        const client = AuthManager.createClient(OscilloscopeApiClient);
 | 
			
		||||
        const success = await client.initialize({ ...config });
 | 
			
		||||
        const proxy = getHubProxy();
 | 
			
		||||
 | 
			
		||||
        const success = await proxy.initialize(config);
 | 
			
		||||
 | 
			
		||||
        if (success) {
 | 
			
		||||
          alert.success("示波器配置已应用", 2000);
 | 
			
		||||
        } else {
 | 
			
		||||
          throw new Error("应用失败");
 | 
			
		||||
        }
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        if (error instanceof Error && error.message === "Hub not initialized")
 | 
			
		||||
          reinitializeHub();
 | 
			
		||||
        alert.error("应用配置失败", 3000);
 | 
			
		||||
      } finally {
 | 
			
		||||
        isApplying.value = false;
 | 
			
		||||
@@ -89,68 +176,55 @@ const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(
 | 
			
		||||
      alert.info("配置已重置", 2000);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const clearOscilloscopeData = () => {
 | 
			
		||||
      oscData.value = undefined;
 | 
			
		||||
    // 采样点数(由后端数据决定)
 | 
			
		||||
    const sampleCount = ref(0);
 | 
			
		||||
 | 
			
		||||
    // 采样周期(ns),由adFrequency计算
 | 
			
		||||
    const samplePeriodNs = computed(() =>
 | 
			
		||||
      oscData.value?.adFrequency
 | 
			
		||||
        ? 1_000_000_000 / oscData.value.adFrequency
 | 
			
		||||
        : 200,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const analyzeOscilloscopeData = (resp: OscilloscopeDataResponse) => {
 | 
			
		||||
      // 解析波形数据
 | 
			
		||||
      const binaryString = atob(resp.waveformData);
 | 
			
		||||
      const bytes = new Uint8Array(binaryString.length);
 | 
			
		||||
      for (let i = 0; i < binaryString.length; i++) {
 | 
			
		||||
        bytes[i] = binaryString.charCodeAt(i);
 | 
			
		||||
      }
 | 
			
		||||
      sampleCount.value = bytes.length;
 | 
			
		||||
 | 
			
		||||
      // 构建时间轴
 | 
			
		||||
      const x = Array.from(
 | 
			
		||||
        { length: bytes.length },
 | 
			
		||||
        (_, i) => (i * samplePeriodNs.value) / 1000, // us
 | 
			
		||||
      );
 | 
			
		||||
      const y = Array.from(bytes);
 | 
			
		||||
 | 
			
		||||
      oscData.value = {
 | 
			
		||||
        x,
 | 
			
		||||
        y,
 | 
			
		||||
        xUnit: "us",
 | 
			
		||||
        yUnit: "V",
 | 
			
		||||
        adFrequency: resp.aDFrequency,
 | 
			
		||||
        adVpp: resp.aDVpp,
 | 
			
		||||
        adMax: resp.aDMax,
 | 
			
		||||
        adMin: resp.aDMin,
 | 
			
		||||
      };
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // 获取数据
 | 
			
		||||
    const getOscilloscopeData = async () => {
 | 
			
		||||
      try {
 | 
			
		||||
        const client = AuthManager.createClient(OscilloscopeApiClient);
 | 
			
		||||
        const resp: OscilloscopeDataResponse = await client.getData();
 | 
			
		||||
 | 
			
		||||
        // 解析波形数据
 | 
			
		||||
        const binaryString = atob(resp.waveformData);
 | 
			
		||||
        const bytes = new Uint8Array(binaryString.length);
 | 
			
		||||
        for (let i = 0; i < binaryString.length; i++) {
 | 
			
		||||
          bytes[i] = binaryString.charCodeAt(i);
 | 
			
		||||
        }
 | 
			
		||||
        sampleCount.value = bytes.length;
 | 
			
		||||
 | 
			
		||||
        // 构建时间轴
 | 
			
		||||
        const x = Array.from(
 | 
			
		||||
          { length: bytes.length },
 | 
			
		||||
          (_, i) => (i * samplePeriodNs.value) / 1000, // us
 | 
			
		||||
        );
 | 
			
		||||
        const y = Array.from(bytes);
 | 
			
		||||
 | 
			
		||||
        oscData.value = {
 | 
			
		||||
          x,
 | 
			
		||||
          y,
 | 
			
		||||
          xUnit: "us",
 | 
			
		||||
          yUnit: "V",
 | 
			
		||||
          adFrequency: resp.adFrequency,
 | 
			
		||||
          adVpp: resp.adVpp,
 | 
			
		||||
          adMax: resp.adMax,
 | 
			
		||||
          adMin: resp.adMin,
 | 
			
		||||
        };
 | 
			
		||||
        const proxy = getHubProxy();
 | 
			
		||||
        const resp = await proxy.getData();
 | 
			
		||||
        analyzeOscilloscopeData(resp);
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        alert.error("获取示波器数据失败", 3000);
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // 定时器引用
 | 
			
		||||
    let refreshIntervalId: number | undefined;
 | 
			
		||||
    // 刷新间隔(毫秒),可根据需要调整
 | 
			
		||||
    const refreshIntervalMs = ref(1000);
 | 
			
		||||
 | 
			
		||||
    // 定时刷新函数
 | 
			
		||||
    const startAutoRefresh = () => {
 | 
			
		||||
      if (refreshIntervalId !== undefined) return;
 | 
			
		||||
      refreshIntervalId = window.setInterval(async () => {
 | 
			
		||||
        await refreshRAM();
 | 
			
		||||
        await getOscilloscopeData();
 | 
			
		||||
      }, refreshIntervalMs.value);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const stopAutoRefresh = () => {
 | 
			
		||||
      if (refreshIntervalId !== undefined) {
 | 
			
		||||
        clearInterval(refreshIntervalId);
 | 
			
		||||
        refreshIntervalId = undefined;
 | 
			
		||||
        isCapturing.value = false;
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // 启动捕获
 | 
			
		||||
    const startCapture = async () => {
 | 
			
		||||
      if (operationMutex.isLocked()) {
 | 
			
		||||
@@ -160,17 +234,13 @@ const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(
 | 
			
		||||
      isCapturing.value = true;
 | 
			
		||||
      const release = await operationMutex.acquire();
 | 
			
		||||
      try {
 | 
			
		||||
        const client = AuthManager.createClient(OscilloscopeApiClient);
 | 
			
		||||
        const started = await client.startCapture();
 | 
			
		||||
        const proxy = getHubProxy();
 | 
			
		||||
        const started = await proxy.startCapture();
 | 
			
		||||
        if (!started) throw new Error("无法启动捕获");
 | 
			
		||||
        alert.info("开始捕获...", 2000);
 | 
			
		||||
 | 
			
		||||
        // 启动定时刷新
 | 
			
		||||
        startAutoRefresh();
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        alert.error("捕获失败", 3000);
 | 
			
		||||
        isCapturing.value = false;
 | 
			
		||||
        stopAutoRefresh();
 | 
			
		||||
      } finally {
 | 
			
		||||
        release();
 | 
			
		||||
      }
 | 
			
		||||
@@ -183,11 +253,10 @@ const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      isCapturing.value = false;
 | 
			
		||||
      stopAutoRefresh();
 | 
			
		||||
      const release = await operationMutex.acquire();
 | 
			
		||||
      try {
 | 
			
		||||
        const client = AuthManager.createClient(OscilloscopeApiClient);
 | 
			
		||||
        const stopped = await client.stopCapture();
 | 
			
		||||
        const proxy = getHubProxy();
 | 
			
		||||
        const stopped = await proxy.stopCapture();
 | 
			
		||||
        if (!stopped) throw new Error("无法停止捕获");
 | 
			
		||||
        alert.info("捕获已停止", 2000);
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
@@ -197,6 +266,14 @@ const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const toggleCapture = async () => {
 | 
			
		||||
      if (isCapturing.value) {
 | 
			
		||||
        await stopCapture();
 | 
			
		||||
      } else {
 | 
			
		||||
        await startCapture();
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // 更新触发参数
 | 
			
		||||
    const updateTrigger = async (level: number, risingEdge: boolean) => {
 | 
			
		||||
      const client = AuthManager.createClient(OscilloscopeApiClient);
 | 
			
		||||
@@ -279,9 +356,9 @@ const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(
 | 
			
		||||
      config,
 | 
			
		||||
      isApplying,
 | 
			
		||||
      isCapturing,
 | 
			
		||||
      isAutoApplying,
 | 
			
		||||
      sampleCount,
 | 
			
		||||
      samplePeriodNs,
 | 
			
		||||
      refreshIntervalMs,
 | 
			
		||||
 | 
			
		||||
      applyConfiguration,
 | 
			
		||||
      resetConfiguration,
 | 
			
		||||
@@ -289,6 +366,7 @@ const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(
 | 
			
		||||
      getOscilloscopeData,
 | 
			
		||||
      startCapture,
 | 
			
		||||
      stopCapture,
 | 
			
		||||
      toggleCapture,
 | 
			
		||||
      updateTrigger,
 | 
			
		||||
      updateSampling,
 | 
			
		||||
      refreshRAM,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,36 +1,93 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="w-full h-100 flex flex-col">
 | 
			
		||||
    <!-- 原有内容 -->
 | 
			
		||||
    <v-chart v-if="hasData" class="w-full h-full" :option="option" autoresize />
 | 
			
		||||
    <div v-else class="w-full h-full flex flex-col gap-4 items-center justify-center text-gray-500">
 | 
			
		||||
      <span> 暂无数据 </span>
 | 
			
		||||
      <!-- 采集控制按钮 -->
 | 
			
		||||
      <div class="flex justify-center items-center mb-2">
 | 
			
		||||
  <div
 | 
			
		||||
    class="waveform-container w-full h-full relative overflow-hidden rounded-lg"
 | 
			
		||||
  >
 | 
			
		||||
    <!-- 波形图表 -->
 | 
			
		||||
    <v-chart
 | 
			
		||||
      v-if="hasData"
 | 
			
		||||
      class="w-full h-full transition-all duration-500 ease-in-out"
 | 
			
		||||
      :option="option"
 | 
			
		||||
      autoresize
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
    <!-- 无数据状态 -->
 | 
			
		||||
    <div
 | 
			
		||||
      v-else
 | 
			
		||||
      class="w-full h-full flex flex-col items-center justify-center bg-gradient-to-br from-slate-50 to-blue-50 dark:from-slate-900 dark:to-slate-800"
 | 
			
		||||
    >
 | 
			
		||||
      <!-- 动画图标 -->
 | 
			
		||||
      <div class="relative mb-6">
 | 
			
		||||
        <div
 | 
			
		||||
          class="w-24 h-24 rounded-full border-4 border-blue-200 dark:border-blue-800 animate-pulse"
 | 
			
		||||
        ></div>
 | 
			
		||||
        <div class="absolute inset-0 flex items-center justify-center">
 | 
			
		||||
          <Activity class="w-12 h-12 text-blue-500 animate-bounce" />
 | 
			
		||||
        </div>
 | 
			
		||||
        <!-- 扫描线效果 -->
 | 
			
		||||
        <div
 | 
			
		||||
          class="absolute inset-0 rounded-full border-2 border-transparent border-t-blue-500 animate-spin"
 | 
			
		||||
        ></div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <!-- 状态文本 -->
 | 
			
		||||
      <div class="text-center space-y-2 mb-8">
 | 
			
		||||
        <h3 class="text-xl font-semibold text-slate-700 dark:text-slate-300">
 | 
			
		||||
          等待信号输入
 | 
			
		||||
        </h3>
 | 
			
		||||
        <p class="text-slate-500 dark:text-slate-400">
 | 
			
		||||
          请启动数据采集以显示波形
 | 
			
		||||
        </p>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <!-- 快速启动按钮 -->
 | 
			
		||||
      <div class="flex justify-center items-center">
 | 
			
		||||
        <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="group relative px-8 py-4 bg-gradient-to-r text-white font-semibold rounded-xl shadow-xl hover:shadow-2xl transform hover:scale-110 transition-all duration-300 ease-out focus:outline-none focus:ring-4 active:scale-95 overflow-hidden"
 | 
			
		||||
          :class="{
 | 
			
		||||
            'from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 focus:ring-blue-300':
 | 
			
		||||
            'from-emerald-500 via-blue-500 to-purple-600 hover:from-emerald-600 hover:via-blue-600 hover:to-purple-700 focus:ring-blue-300':
 | 
			
		||||
              !oscManager.isCapturing.value,
 | 
			
		||||
            'from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 focus:ring-red-300':
 | 
			
		||||
            'from-red-500 via-pink-500 to-red-600 hover:from-red-600 hover:via-pink-600 hover:to-red-700 focus:ring-red-300':
 | 
			
		||||
              oscManager.isCapturing.value,
 | 
			
		||||
          }" @click="
 | 
			
		||||
          }"
 | 
			
		||||
          @click="
 | 
			
		||||
            oscManager.isCapturing.value
 | 
			
		||||
              ? oscManager.stopCapture()
 | 
			
		||||
              : oscManager.startCapture()
 | 
			
		||||
            ">
 | 
			
		||||
          <span class="flex items-center gap-2">
 | 
			
		||||
          "
 | 
			
		||||
        >
 | 
			
		||||
          <!-- 背景动画效果 -->
 | 
			
		||||
          <div
 | 
			
		||||
            class="absolute inset-0 bg-white opacity-0 group-hover:opacity-20 transition-opacity duration-300"
 | 
			
		||||
          ></div>
 | 
			
		||||
 | 
			
		||||
          <!-- 按钮内容 -->
 | 
			
		||||
          <span class="relative flex items-center gap-3">
 | 
			
		||||
            <template v-if="oscManager.isCapturing.value">
 | 
			
		||||
              <Square class="w-5 h-5" />
 | 
			
		||||
              <Square class="w-6 h-6 animate-pulse" />
 | 
			
		||||
              停止采集
 | 
			
		||||
            </template>
 | 
			
		||||
            <template v-else>
 | 
			
		||||
              <Play class="w-5 h-5" />
 | 
			
		||||
              <Play class="w-6 h-6 group-hover:animate-pulse" />
 | 
			
		||||
              开始采集
 | 
			
		||||
            </template>
 | 
			
		||||
          </span>
 | 
			
		||||
 | 
			
		||||
          <!-- 光晕效果 -->
 | 
			
		||||
          <div
 | 
			
		||||
            class="absolute inset-0 rounded-xl bg-gradient-to-r from-transparent via-white to-transparent opacity-0 group-hover:opacity-30 transform -skew-x-12 translate-x-full group-hover:translate-x-[-200%] transition-transform duration-700"
 | 
			
		||||
          ></div>
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- 数据采集状态指示器 -->
 | 
			
		||||
    <div
 | 
			
		||||
      v-if="hasData && oscManager.isCapturing.value"
 | 
			
		||||
      class="absolute top-4 right-4 flex items-center gap-2 bg-red-500/90 text-white px-3 py-1 rounded-full text-sm font-medium backdrop-blur-sm"
 | 
			
		||||
    >
 | 
			
		||||
      <div class="w-2 h-2 bg-white rounded-full animate-pulse"></div>
 | 
			
		||||
      采集中
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
@@ -61,7 +118,7 @@ import type {
 | 
			
		||||
  GridComponentOption,
 | 
			
		||||
} from "echarts/components";
 | 
			
		||||
import { useRequiredInjection } from "@/utils/Common";
 | 
			
		||||
import { Play, Square } from "lucide-vue-next";
 | 
			
		||||
import { Play, Square, Activity } from "lucide-vue-next";
 | 
			
		||||
 | 
			
		||||
use([
 | 
			
		||||
  TooltipComponent,
 | 
			
		||||
@@ -113,12 +170,23 @@ const option = computed((): EChartsOption => {
 | 
			
		||||
    ? (oscData.value.y as number[][])
 | 
			
		||||
    : [oscData.value.y as number[]];
 | 
			
		||||
 | 
			
		||||
  // 预定义的通道颜色
 | 
			
		||||
  const channelColors = [
 | 
			
		||||
    "#3B82F6", // blue-500
 | 
			
		||||
    "#EF4444", // red-500
 | 
			
		||||
    "#10B981", // emerald-500
 | 
			
		||||
    "#F59E0B", // amber-500
 | 
			
		||||
    "#8B5CF6", // violet-500
 | 
			
		||||
    "#06B6D4", // cyan-500
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  forEach(yChannels, (yData, index) => {
 | 
			
		||||
    if (!oscData.value || !yData) return;
 | 
			
		||||
    const seriesData = oscData.value.x.map((xValue, i) => [
 | 
			
		||||
      xValue,
 | 
			
		||||
      yData && yData[i] !== undefined ? yData[i] : 0,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    series.push({
 | 
			
		||||
      type: "line",
 | 
			
		||||
      name: `通道 ${index + 1}`,
 | 
			
		||||
@@ -126,41 +194,82 @@ const option = computed((): EChartsOption => {
 | 
			
		||||
      smooth: false,
 | 
			
		||||
      symbol: "none",
 | 
			
		||||
      lineStyle: {
 | 
			
		||||
        width: 2,
 | 
			
		||||
        width: 2.5,
 | 
			
		||||
        color: channelColors[index % channelColors.length],
 | 
			
		||||
        shadowColor: channelColors[index % channelColors.length],
 | 
			
		||||
        shadowBlur: isCapturing ? 0 : 4,
 | 
			
		||||
        shadowOffsetY: 2,
 | 
			
		||||
      },
 | 
			
		||||
      // 关闭系列动画
 | 
			
		||||
      itemStyle: {
 | 
			
		||||
        color: channelColors[index % channelColors.length],
 | 
			
		||||
      },
 | 
			
		||||
      // 动画配置
 | 
			
		||||
      animation: !isCapturing,
 | 
			
		||||
      animationDuration: isCapturing ? 0 : 1000,
 | 
			
		||||
      animationDuration: isCapturing ? 0 : 1200,
 | 
			
		||||
      animationEasing: isCapturing ? "linear" : "cubicOut",
 | 
			
		||||
      animationDelay: index * 100, // 错开动画时间
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    backgroundColor: "transparent",
 | 
			
		||||
    grid: {
 | 
			
		||||
      left: "10%",
 | 
			
		||||
      right: "10%",
 | 
			
		||||
      top: "15%",
 | 
			
		||||
      bottom: "25%",
 | 
			
		||||
      left: "8%",
 | 
			
		||||
      right: "5%",
 | 
			
		||||
      top: "12%",
 | 
			
		||||
      bottom: "20%",
 | 
			
		||||
      borderWidth: 1,
 | 
			
		||||
      borderColor: "#E2E8F0",
 | 
			
		||||
      backgroundColor: "rgba(248, 250, 252, 0.8)",
 | 
			
		||||
    },
 | 
			
		||||
    tooltip: {
 | 
			
		||||
      trigger: "axis",
 | 
			
		||||
      backgroundColor: "rgba(255, 255, 255, 0.95)",
 | 
			
		||||
      borderColor: "#E2E8F0",
 | 
			
		||||
      borderWidth: 1,
 | 
			
		||||
      textStyle: {
 | 
			
		||||
        color: "#334155",
 | 
			
		||||
        fontSize: 12,
 | 
			
		||||
      },
 | 
			
		||||
      formatter: (params: any) => {
 | 
			
		||||
        if (!oscData.value) return "";
 | 
			
		||||
        let result = `时间: ${params[0].data[0].toFixed(2)} ${oscData.value.xUnit}<br/>`;
 | 
			
		||||
        let result = `<div style="font-weight: 600; margin-bottom: 4px;">时间: ${params[0].data[0].toFixed(2)} ${oscData.value.xUnit}</div>`;
 | 
			
		||||
        params.forEach((param: any) => {
 | 
			
		||||
          result += `${param.seriesName}: ${param.data[1].toFixed(3)} ${oscData.value?.yUnit ?? ""}<br/>`;
 | 
			
		||||
          result += `<div style="color: ${param.color};">● ${param.seriesName}: ${param.data[1].toFixed(3)} ${oscData.value?.yUnit ?? ""}</div>`;
 | 
			
		||||
        });
 | 
			
		||||
        return result;
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    legend: {
 | 
			
		||||
      top: "5%",
 | 
			
		||||
      top: "2%",
 | 
			
		||||
      left: "center",
 | 
			
		||||
      textStyle: {
 | 
			
		||||
        color: "#64748B",
 | 
			
		||||
        fontSize: 12,
 | 
			
		||||
        fontWeight: 500,
 | 
			
		||||
      },
 | 
			
		||||
      itemGap: 20,
 | 
			
		||||
      data: series.map((s) => s.name) as string[],
 | 
			
		||||
    },
 | 
			
		||||
    toolbox: {
 | 
			
		||||
      right: "2%",
 | 
			
		||||
      top: "2%",
 | 
			
		||||
      feature: {
 | 
			
		||||
        restore: {},
 | 
			
		||||
        saveAsImage: {},
 | 
			
		||||
        restore: {
 | 
			
		||||
          title: "重置缩放",
 | 
			
		||||
        },
 | 
			
		||||
        saveAsImage: {
 | 
			
		||||
          title: "保存图片",
 | 
			
		||||
          name: `oscilloscope_${new Date().toISOString().slice(0, 19)}`,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      iconStyle: {
 | 
			
		||||
        borderColor: "#64748B",
 | 
			
		||||
      },
 | 
			
		||||
      emphasis: {
 | 
			
		||||
        iconStyle: {
 | 
			
		||||
          borderColor: "#3B82F6",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    dataZoom: [
 | 
			
		||||
@@ -168,47 +277,275 @@ const option = computed((): EChartsOption => {
 | 
			
		||||
        type: "inside",
 | 
			
		||||
        start: 0,
 | 
			
		||||
        end: 100,
 | 
			
		||||
        filterMode: "weakFilter",
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        start: 0,
 | 
			
		||||
        end: 100,
 | 
			
		||||
        height: 25,
 | 
			
		||||
        bottom: "8%",
 | 
			
		||||
        borderColor: "#E2E8F0",
 | 
			
		||||
        fillerColor: "rgba(59, 130, 246, 0.1)",
 | 
			
		||||
        handleStyle: {
 | 
			
		||||
          color: "#3B82F6",
 | 
			
		||||
          borderColor: "#1E40AF",
 | 
			
		||||
        },
 | 
			
		||||
        textStyle: {
 | 
			
		||||
          color: "#64748B",
 | 
			
		||||
          fontSize: 11,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
    xAxis: {
 | 
			
		||||
      type: "value",
 | 
			
		||||
      name: oscData.value ? `时间 (${oscData.value.xUnit})` : "时间",
 | 
			
		||||
      nameLocation: "middle",
 | 
			
		||||
      nameGap: 30,
 | 
			
		||||
      nameGap: 35,
 | 
			
		||||
      nameTextStyle: {
 | 
			
		||||
        color: "#64748B",
 | 
			
		||||
        fontSize: 12,
 | 
			
		||||
        fontWeight: 500,
 | 
			
		||||
      },
 | 
			
		||||
      axisLine: {
 | 
			
		||||
        show: true,
 | 
			
		||||
        lineStyle: {
 | 
			
		||||
          color: "#CBD5E1",
 | 
			
		||||
          width: 1.5,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      axisTick: {
 | 
			
		||||
        show: true,
 | 
			
		||||
        lineStyle: {
 | 
			
		||||
          color: "#E2E8F0",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      axisLabel: {
 | 
			
		||||
        color: "#64748B",
 | 
			
		||||
        fontSize: 11,
 | 
			
		||||
      },
 | 
			
		||||
      splitLine: {
 | 
			
		||||
        show: false,
 | 
			
		||||
        show: true,
 | 
			
		||||
        lineStyle: {
 | 
			
		||||
          color: "#F1F5F9",
 | 
			
		||||
          type: "dashed",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    yAxis: {
 | 
			
		||||
      type: "value",
 | 
			
		||||
      name: oscData.value ? `电压 (${oscData.value.yUnit})` : "电压",
 | 
			
		||||
      nameLocation: "middle",
 | 
			
		||||
      nameGap: 40,
 | 
			
		||||
      nameGap: 50,
 | 
			
		||||
      nameTextStyle: {
 | 
			
		||||
        color: "#64748B",
 | 
			
		||||
        fontSize: 12,
 | 
			
		||||
        fontWeight: 500,
 | 
			
		||||
      },
 | 
			
		||||
      axisLine: {
 | 
			
		||||
        show: true,
 | 
			
		||||
        lineStyle: {
 | 
			
		||||
          color: "#CBD5E1",
 | 
			
		||||
          width: 1.5,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      axisTick: {
 | 
			
		||||
        show: true,
 | 
			
		||||
        lineStyle: {
 | 
			
		||||
          color: "#E2E8F0",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      axisLabel: {
 | 
			
		||||
        color: "#64748B",
 | 
			
		||||
        fontSize: 11,
 | 
			
		||||
      },
 | 
			
		||||
      splitLine: {
 | 
			
		||||
        show: false,
 | 
			
		||||
        show: true,
 | 
			
		||||
        lineStyle: {
 | 
			
		||||
          color: "#F1F5F9",
 | 
			
		||||
          type: "dashed",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    // 全局动画开关
 | 
			
		||||
    animation: !isCapturing,
 | 
			
		||||
    animationDuration: isCapturing ? 0 : 1000,
 | 
			
		||||
    animationDuration: isCapturing ? 0 : 1200,
 | 
			
		||||
    animationEasing: isCapturing ? "linear" : "cubicOut",
 | 
			
		||||
    series: series,
 | 
			
		||||
  };
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="postcss">
 | 
			
		||||
@import "@/assets/main.css";
 | 
			
		||||
/* 波形容器样式 */
 | 
			
		||||
.waveform-container {
 | 
			
		||||
  background: linear-gradient(
 | 
			
		||||
    135deg,
 | 
			
		||||
    rgba(248, 250, 252, 0.8) 0%,
 | 
			
		||||
    rgba(241, 245, 249, 0.8) 100%
 | 
			
		||||
  );
 | 
			
		||||
  border: 1px solid rgba(226, 232, 240, 0.5);
 | 
			
		||||
  position: relative;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.waveform-container::before {
 | 
			
		||||
  content: "";
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  inset: 0;
 | 
			
		||||
  background: linear-gradient(
 | 
			
		||||
    45deg,
 | 
			
		||||
    transparent 48%,
 | 
			
		||||
    rgba(59, 130, 246, 0.05) 50%,
 | 
			
		||||
    transparent 52%
 | 
			
		||||
  );
 | 
			
		||||
  pointer-events: none;
 | 
			
		||||
  z-index: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 无数据状态的背景动画 */
 | 
			
		||||
.waveform-container:not(:has(canvas)) {
 | 
			
		||||
  background: linear-gradient(
 | 
			
		||||
    135deg,
 | 
			
		||||
    rgba(248, 250, 252, 1) 0%,
 | 
			
		||||
    rgba(239, 246, 255, 1) 25%,
 | 
			
		||||
    rgba(219, 234, 254, 1) 50%,
 | 
			
		||||
    rgba(239, 246, 255, 1) 75%,
 | 
			
		||||
    rgba(248, 250, 252, 1) 100%
 | 
			
		||||
  );
 | 
			
		||||
  background-size: 200% 200%;
 | 
			
		||||
  animation: gradient-shift 8s ease-in-out infinite;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes gradient-shift {
 | 
			
		||||
  0%,
 | 
			
		||||
  100% {
 | 
			
		||||
    background-position: 0% 50%;
 | 
			
		||||
  }
 | 
			
		||||
  50% {
 | 
			
		||||
    background-position: 100% 50%;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 深色模式支持 */
 | 
			
		||||
@media (prefers-color-scheme: dark) {
 | 
			
		||||
  .waveform-container {
 | 
			
		||||
    background: linear-gradient(
 | 
			
		||||
      135deg,
 | 
			
		||||
      rgba(15, 23, 42, 0.8) 0%,
 | 
			
		||||
      rgba(30, 41, 59, 0.8) 100%
 | 
			
		||||
    );
 | 
			
		||||
    border-color: rgba(71, 85, 105, 0.5);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .waveform-container:not(:has(canvas)) {
 | 
			
		||||
    background: linear-gradient(
 | 
			
		||||
      135deg,
 | 
			
		||||
      rgba(15, 23, 42, 1) 0%,
 | 
			
		||||
      rgba(30, 41, 59, 1) 25%,
 | 
			
		||||
      rgba(51, 65, 85, 1) 50%,
 | 
			
		||||
      rgba(30, 41, 59, 1) 75%,
 | 
			
		||||
      rgba(15, 23, 42, 1) 100%
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 按钮光晕效果增强 */
 | 
			
		||||
button {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
button::after {
 | 
			
		||||
  content: "";
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: 50%;
 | 
			
		||||
  left: 50%;
 | 
			
		||||
  width: 0;
 | 
			
		||||
  height: 0;
 | 
			
		||||
  border-radius: 50%;
 | 
			
		||||
  background: rgba(255, 255, 255, 0.3);
 | 
			
		||||
  transform: translate(-50%, -50%);
 | 
			
		||||
  transition:
 | 
			
		||||
    width 0.6s,
 | 
			
		||||
    height 0.6s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
button:active::after {
 | 
			
		||||
  width: 300px;
 | 
			
		||||
  height: 300px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 扫描线动画优化 */
 | 
			
		||||
@keyframes scan-line {
 | 
			
		||||
  0% {
 | 
			
		||||
    transform: rotate(0deg) scale(1);
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
  }
 | 
			
		||||
  50% {
 | 
			
		||||
    transform: rotate(180deg) scale(1.1);
 | 
			
		||||
    opacity: 0.7;
 | 
			
		||||
  }
 | 
			
		||||
  100% {
 | 
			
		||||
    transform: rotate(360deg) scale(1);
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.animate-spin {
 | 
			
		||||
  animation: scan-line 3s linear infinite;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 状态指示器增强 */
 | 
			
		||||
.absolute.top-4.right-4 {
 | 
			
		||||
  backdrop-filter: blur(8px);
 | 
			
		||||
  box-shadow: 0 4px 12px rgba(239, 68, 68, 0.25);
 | 
			
		||||
  animation: float 2s ease-in-out infinite;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes float {
 | 
			
		||||
  0%,
 | 
			
		||||
  100% {
 | 
			
		||||
    transform: translateY(0px);
 | 
			
		||||
  }
 | 
			
		||||
  50% {
 | 
			
		||||
    transform: translateY(-2px);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 图表容器增强 */
 | 
			
		||||
.w-full.h-full.transition-all {
 | 
			
		||||
  border-radius: 8px;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 响应式调整 */
 | 
			
		||||
@media (max-width: 768px) {
 | 
			
		||||
  .waveform-container {
 | 
			
		||||
    min-height: 300px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  button {
 | 
			
		||||
    padding: 12px 20px;
 | 
			
		||||
    font-size: 14px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .absolute.top-4.right-4 {
 | 
			
		||||
    top: 8px;
 | 
			
		||||
    right: 8px;
 | 
			
		||||
    font-size: 12px;
 | 
			
		||||
    padding: 4px 8px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 平滑过渡效果 */
 | 
			
		||||
* {
 | 
			
		||||
  transition: all 0.2s ease-in-out;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 焦点样式 */
 | 
			
		||||
button:focus-visible {
 | 
			
		||||
  outline: 2px solid rgba(59, 130, 246, 0.5);
 | 
			
		||||
  outline-offset: 2px;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -45,7 +45,12 @@ export class AuthManager {
 | 
			
		||||
 | 
			
		||||
  // SignalR连接 - 简单明了
 | 
			
		||||
  static createHubConnection(
 | 
			
		||||
    hubPath: "ProgressHub" | "JtagHub" | "DigitalTubesHub" | "RotaryEncoderHub",
 | 
			
		||||
    hubPath:
 | 
			
		||||
      | "ProgressHub"
 | 
			
		||||
      | "JtagHub"
 | 
			
		||||
      | "DigitalTubesHub"
 | 
			
		||||
      | "RotaryEncoderHub"
 | 
			
		||||
      | "OscilloscopeHub",
 | 
			
		||||
  ) {
 | 
			
		||||
    return new HubConnectionBuilder()
 | 
			
		||||
      .withUrl(`http://127.0.0.1:5000/hubs/${hubPath}`, {
 | 
			
		||||
 
 | 
			
		||||
@@ -3,8 +3,8 @@
 | 
			
		||||
/* tslint:disable */
 | 
			
		||||
// @ts-nocheck
 | 
			
		||||
import type { HubConnection, IStreamResult, Subject } from '@microsoft/signalr';
 | 
			
		||||
import type { IDigitalTubesHub, IJtagHub, IProgressHub, IRotaryEncoderHub, IDigitalTubesReceiver, IJtagReceiver, IProgressReceiver, IRotaryEncoderReceiver } from './server.Hubs';
 | 
			
		||||
import type { DigitalTubeTaskStatus, ProgressInfo } from '../server.Hubs';
 | 
			
		||||
import type { IDigitalTubesHub, IJtagHub, IOscilloscopeHub, IProgressHub, IRotaryEncoderHub, IDigitalTubesReceiver, IJtagReceiver, IOscilloscopeReceiver, IProgressReceiver, IRotaryEncoderReceiver } from './server.Hubs';
 | 
			
		||||
import type { DigitalTubeTaskStatus, OscilloscopeFullConfig, OscilloscopeDataResponse, ProgressInfo } from '../server.Hubs';
 | 
			
		||||
import type { RotaryEncoderDirection } from '../Peripherals.RotaryEncoderClient';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -46,6 +46,7 @@ class ReceiverMethodSubscription implements Disposable {
 | 
			
		||||
export type HubProxyFactoryProvider = {
 | 
			
		||||
    (hubType: "IDigitalTubesHub"): HubProxyFactory<IDigitalTubesHub>;
 | 
			
		||||
    (hubType: "IJtagHub"): HubProxyFactory<IJtagHub>;
 | 
			
		||||
    (hubType: "IOscilloscopeHub"): HubProxyFactory<IOscilloscopeHub>;
 | 
			
		||||
    (hubType: "IProgressHub"): HubProxyFactory<IProgressHub>;
 | 
			
		||||
    (hubType: "IRotaryEncoderHub"): HubProxyFactory<IRotaryEncoderHub>;
 | 
			
		||||
}
 | 
			
		||||
@@ -57,6 +58,9 @@ export const getHubProxyFactory = ((hubType: string) => {
 | 
			
		||||
    if(hubType === "IJtagHub") {
 | 
			
		||||
        return IJtagHub_HubProxyFactory.Instance;
 | 
			
		||||
    }
 | 
			
		||||
    if(hubType === "IOscilloscopeHub") {
 | 
			
		||||
        return IOscilloscopeHub_HubProxyFactory.Instance;
 | 
			
		||||
    }
 | 
			
		||||
    if(hubType === "IProgressHub") {
 | 
			
		||||
        return IProgressHub_HubProxyFactory.Instance;
 | 
			
		||||
    }
 | 
			
		||||
@@ -68,6 +72,7 @@ export const getHubProxyFactory = ((hubType: string) => {
 | 
			
		||||
export type ReceiverRegisterProvider = {
 | 
			
		||||
    (receiverType: "IDigitalTubesReceiver"): ReceiverRegister<IDigitalTubesReceiver>;
 | 
			
		||||
    (receiverType: "IJtagReceiver"): ReceiverRegister<IJtagReceiver>;
 | 
			
		||||
    (receiverType: "IOscilloscopeReceiver"): ReceiverRegister<IOscilloscopeReceiver>;
 | 
			
		||||
    (receiverType: "IProgressReceiver"): ReceiverRegister<IProgressReceiver>;
 | 
			
		||||
    (receiverType: "IRotaryEncoderReceiver"): ReceiverRegister<IRotaryEncoderReceiver>;
 | 
			
		||||
}
 | 
			
		||||
@@ -79,6 +84,9 @@ export const getReceiverRegister = ((receiverType: string) => {
 | 
			
		||||
    if(receiverType === "IJtagReceiver") {
 | 
			
		||||
        return IJtagReceiver_Binder.Instance;
 | 
			
		||||
    }
 | 
			
		||||
    if(receiverType === "IOscilloscopeReceiver") {
 | 
			
		||||
        return IOscilloscopeReceiver_Binder.Instance;
 | 
			
		||||
    }
 | 
			
		||||
    if(receiverType === "IProgressReceiver") {
 | 
			
		||||
        return IProgressReceiver_Binder.Instance;
 | 
			
		||||
    }
 | 
			
		||||
@@ -151,6 +159,55 @@ class IJtagHub_HubProxy implements IJtagHub {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class IOscilloscopeHub_HubProxyFactory implements HubProxyFactory<IOscilloscopeHub> {
 | 
			
		||||
    public static Instance = new IOscilloscopeHub_HubProxyFactory();
 | 
			
		||||
 | 
			
		||||
    private constructor() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public readonly createHubProxy = (connection: HubConnection): IOscilloscopeHub => {
 | 
			
		||||
        return new IOscilloscopeHub_HubProxy(connection);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class IOscilloscopeHub_HubProxy implements IOscilloscopeHub {
 | 
			
		||||
 | 
			
		||||
    public constructor(private connection: HubConnection) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public readonly initialize = async (config: OscilloscopeFullConfig): Promise<boolean> => {
 | 
			
		||||
        return await this.connection.invoke("Initialize", config);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public readonly startCapture = async (): Promise<boolean> => {
 | 
			
		||||
        return await this.connection.invoke("StartCapture");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public readonly stopCapture = async (): Promise<boolean> => {
 | 
			
		||||
        return await this.connection.invoke("StopCapture");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public readonly getData = async (): Promise<OscilloscopeDataResponse> => {
 | 
			
		||||
        return await this.connection.invoke("GetData");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public readonly setTrigger = async (level: number): Promise<boolean> => {
 | 
			
		||||
        return await this.connection.invoke("SetTrigger", level);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public readonly setRisingEdge = async (risingEdge: boolean): Promise<boolean> => {
 | 
			
		||||
        return await this.connection.invoke("SetRisingEdge", risingEdge);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public readonly setSampling = async (decimationRate: number): Promise<boolean> => {
 | 
			
		||||
        return await this.connection.invoke("SetSampling", decimationRate);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public readonly setFrequency = async (frequency: number): Promise<boolean> => {
 | 
			
		||||
        return await this.connection.invoke("SetFrequency", frequency);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class IProgressHub_HubProxyFactory implements HubProxyFactory<IProgressHub> {
 | 
			
		||||
    public static Instance = new IProgressHub_HubProxyFactory();
 | 
			
		||||
 | 
			
		||||
@@ -258,6 +315,27 @@ class IJtagReceiver_Binder implements ReceiverRegister<IJtagReceiver> {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class IOscilloscopeReceiver_Binder implements ReceiverRegister<IOscilloscopeReceiver> {
 | 
			
		||||
 | 
			
		||||
    public static Instance = new IOscilloscopeReceiver_Binder();
 | 
			
		||||
 | 
			
		||||
    private constructor() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public readonly register = (connection: HubConnection, receiver: IOscilloscopeReceiver): Disposable => {
 | 
			
		||||
 | 
			
		||||
        const __onDataReceived = (...args: [OscilloscopeDataResponse]) => receiver.onDataReceived(...args);
 | 
			
		||||
 | 
			
		||||
        connection.on("OnDataReceived", __onDataReceived);
 | 
			
		||||
 | 
			
		||||
        const methodList: ReceiverMethod[] = [
 | 
			
		||||
            { methodName: "OnDataReceived", method: __onDataReceived }
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        return new ReceiverMethodSubscription(connection, methodList);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class IProgressReceiver_Binder implements ReceiverRegister<IProgressReceiver> {
 | 
			
		||||
 | 
			
		||||
    public static Instance = new IProgressReceiver_Binder();
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
/* tslint:disable */
 | 
			
		||||
// @ts-nocheck
 | 
			
		||||
import type { IStreamResult, Subject } from '@microsoft/signalr';
 | 
			
		||||
import type { DigitalTubeTaskStatus, ProgressInfo } from '../server.Hubs';
 | 
			
		||||
import type { DigitalTubeTaskStatus, OscilloscopeFullConfig, OscilloscopeDataResponse, ProgressInfo } from '../server.Hubs';
 | 
			
		||||
import type { RotaryEncoderDirection } from '../Peripherals.RotaryEncoderClient';
 | 
			
		||||
 | 
			
		||||
export type IDigitalTubesHub = {
 | 
			
		||||
@@ -43,6 +43,46 @@ export type IJtagHub = {
 | 
			
		||||
    stopBoundaryScan(): Promise<boolean>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type IOscilloscopeHub = {
 | 
			
		||||
    /**
 | 
			
		||||
    * @param config Transpiled from server.Hubs.OscilloscopeFullConfig
 | 
			
		||||
    * @returns Transpiled from System.Threading.Tasks.Task<bool>
 | 
			
		||||
    */
 | 
			
		||||
    initialize(config: OscilloscopeFullConfig): Promise<boolean>;
 | 
			
		||||
    /**
 | 
			
		||||
    * @returns Transpiled from System.Threading.Tasks.Task<bool>
 | 
			
		||||
    */
 | 
			
		||||
    startCapture(): Promise<boolean>;
 | 
			
		||||
    /**
 | 
			
		||||
    * @returns Transpiled from System.Threading.Tasks.Task<bool>
 | 
			
		||||
    */
 | 
			
		||||
    stopCapture(): Promise<boolean>;
 | 
			
		||||
    /**
 | 
			
		||||
    * @returns Transpiled from System.Threading.Tasks.Task<server.Hubs.OscilloscopeDataResponse?>
 | 
			
		||||
    */
 | 
			
		||||
    getData(): Promise<OscilloscopeDataResponse>;
 | 
			
		||||
    /**
 | 
			
		||||
    * @param level Transpiled from byte
 | 
			
		||||
    * @returns Transpiled from System.Threading.Tasks.Task<bool>
 | 
			
		||||
    */
 | 
			
		||||
    setTrigger(level: number): Promise<boolean>;
 | 
			
		||||
    /**
 | 
			
		||||
    * @param risingEdge Transpiled from bool
 | 
			
		||||
    * @returns Transpiled from System.Threading.Tasks.Task<bool>
 | 
			
		||||
    */
 | 
			
		||||
    setRisingEdge(risingEdge: boolean): Promise<boolean>;
 | 
			
		||||
    /**
 | 
			
		||||
    * @param decimationRate Transpiled from ushort
 | 
			
		||||
    * @returns Transpiled from System.Threading.Tasks.Task<bool>
 | 
			
		||||
    */
 | 
			
		||||
    setSampling(decimationRate: number): Promise<boolean>;
 | 
			
		||||
    /**
 | 
			
		||||
    * @param frequency Transpiled from int
 | 
			
		||||
    * @returns Transpiled from System.Threading.Tasks.Task<bool>
 | 
			
		||||
    */
 | 
			
		||||
    setFrequency(frequency: number): Promise<boolean>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type IProgressHub = {
 | 
			
		||||
    /**
 | 
			
		||||
    * @param taskId Transpiled from string
 | 
			
		||||
@@ -102,6 +142,14 @@ export type IJtagReceiver = {
 | 
			
		||||
    onReceiveBoundaryScanData(msg: Partial<Record<string, boolean>>): Promise<void>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type IOscilloscopeReceiver = {
 | 
			
		||||
    /**
 | 
			
		||||
    * @param data Transpiled from server.Hubs.OscilloscopeDataResponse
 | 
			
		||||
    * @returns Transpiled from System.Threading.Tasks.Task
 | 
			
		||||
    */
 | 
			
		||||
    onDataReceived(data: OscilloscopeDataResponse): Promise<void>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type IProgressReceiver = {
 | 
			
		||||
    /**
 | 
			
		||||
    * @param message Transpiled from server.Hubs.ProgressInfo
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,36 @@ export type DigitalTubeTaskStatus = {
 | 
			
		||||
    isRunning: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Transpiled from server.Hubs.OscilloscopeDataResponse */
 | 
			
		||||
export type OscilloscopeDataResponse = {
 | 
			
		||||
    /** Transpiled from uint */
 | 
			
		||||
    aDFrequency: number;
 | 
			
		||||
    /** Transpiled from byte */
 | 
			
		||||
    aDVpp: number;
 | 
			
		||||
    /** Transpiled from byte */
 | 
			
		||||
    aDMax: number;
 | 
			
		||||
    /** Transpiled from byte */
 | 
			
		||||
    aDMin: number;
 | 
			
		||||
    /** Transpiled from string */
 | 
			
		||||
    waveformData: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Transpiled from server.Hubs.OscilloscopeFullConfig */
 | 
			
		||||
export type OscilloscopeFullConfig = {
 | 
			
		||||
    /** Transpiled from bool */
 | 
			
		||||
    captureEnabled: boolean;
 | 
			
		||||
    /** Transpiled from byte */
 | 
			
		||||
    triggerLevel: number;
 | 
			
		||||
    /** Transpiled from bool */
 | 
			
		||||
    triggerRisingEdge: boolean;
 | 
			
		||||
    /** Transpiled from ushort */
 | 
			
		||||
    horizontalShift: number;
 | 
			
		||||
    /** Transpiled from ushort */
 | 
			
		||||
    decimationRate: number;
 | 
			
		||||
    /** Transpiled from int */
 | 
			
		||||
    captureFrequency: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Transpiled from server.Hubs.ProgressStatus */
 | 
			
		||||
export enum ProgressStatus {
 | 
			
		||||
    Running = 0,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,109 +1,336 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="bg-base-100 flex flex-col gap-4">
 | 
			
		||||
    <!-- 波形展示 -->
 | 
			
		||||
    <div class="card bg-base-200 shadow-xl mx-5">
 | 
			
		||||
      <div class="card-body">
 | 
			
		||||
        <h2 class="card-title flex flex-row justify-between">
 | 
			
		||||
          <div class="flex items-center gap-2">
 | 
			
		||||
            <Activity class="w-5 h-5" />
 | 
			
		||||
            波形显示
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="flex items-center gap-2">
 | 
			
		||||
            <button class="btn btn-sm btn-warning" @click="osc.stopCapture" :disabled="!osc.isCapturing.value">
 | 
			
		||||
              停止捕获
 | 
			
		||||
            </button>
 | 
			
		||||
            <div class="flex items-center gap-2">
 | 
			
		||||
              <button class="btn btn-sm btn-error" @click="osc.clearOscilloscopeData">
 | 
			
		||||
                清空
 | 
			
		||||
  <div
 | 
			
		||||
    class="oscilloscope-container min-h-screen bg-gradient-to-br from-slate-50 to-blue-50 dark:from-slate-900 dark:to-slate-800 p-4"
 | 
			
		||||
  >
 | 
			
		||||
    <!-- 顶部状态栏 -->
 | 
			
		||||
    <div class="status-bar mb-6">
 | 
			
		||||
      <div
 | 
			
		||||
        class="card bg-white/80 dark:bg-slate-800/80 backdrop-blur-lg shadow-xl border border-white/20"
 | 
			
		||||
      >
 | 
			
		||||
        <div class="card-body p-4">
 | 
			
		||||
          <div class="flex items-center justify-between">
 | 
			
		||||
            <div class="flex items-center gap-4">
 | 
			
		||||
              <div class="status-indicator flex items-center gap-2">
 | 
			
		||||
                <div class="relative">
 | 
			
		||||
                  <Activity class="w-6 h-6 text-blue-600" />
 | 
			
		||||
                  <div
 | 
			
		||||
                    v-if="osc.isCapturing.value"
 | 
			
		||||
                    class="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full animate-pulse"
 | 
			
		||||
                  ></div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div>
 | 
			
		||||
                  <h1
 | 
			
		||||
                    class="text-xl font-bold text-slate-800 dark:text-slate-200"
 | 
			
		||||
                  >
 | 
			
		||||
                    数字示波器
 | 
			
		||||
                  </h1>
 | 
			
		||||
                  <p class="text-sm text-slate-600 dark:text-slate-400">
 | 
			
		||||
                    {{ osc.isCapturing.value ? "正在采集数据..." : "待机状态" }}
 | 
			
		||||
                  </p>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div class="control-buttons flex items-center gap-3">
 | 
			
		||||
              <button
 | 
			
		||||
                class="btn-gradient"
 | 
			
		||||
                :class="osc.isCapturing.value ? 'btn-stop' : 'btn-start'"
 | 
			
		||||
                @click="toggleCapture"
 | 
			
		||||
              >
 | 
			
		||||
                <component
 | 
			
		||||
                  :is="osc.isCapturing.value ? Square : Play"
 | 
			
		||||
                  class="w-5 h-5"
 | 
			
		||||
                />
 | 
			
		||||
                {{ osc.isCapturing.value ? "停止采集" : "开始采集" }}
 | 
			
		||||
              </button>
 | 
			
		||||
 | 
			
		||||
              <button
 | 
			
		||||
                class="btn-clear"
 | 
			
		||||
                @click="osc.clearOscilloscopeData"
 | 
			
		||||
                :disabled="osc.isCapturing.value"
 | 
			
		||||
              >
 | 
			
		||||
                <Trash2 class="w-4 h-4" />
 | 
			
		||||
                清空数据
 | 
			
		||||
              </button>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </h2>
 | 
			
		||||
        <OscilloscopeWaveformDisplay />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- 示波器配置 -->
 | 
			
		||||
    <div class="card bg-base-200 shadow-xl mx-5">
 | 
			
		||||
      <div class="card-body">
 | 
			
		||||
        <h2 class="card-title">示波器配置</h2>
 | 
			
		||||
        <form class="flex flex-col gap-2" @submit.prevent="applyConfiguration">
 | 
			
		||||
          <div class="flex flex-row items-center justify-between gap-4">
 | 
			
		||||
            <label>
 | 
			
		||||
              边沿触发:
 | 
			
		||||
              <select v-model="osc.config.triggerRisingEdge" class="select select-bordered w-24">
 | 
			
		||||
                <option :value="true">上升沿</option>
 | 
			
		||||
                <option :value="false">下降沿</option>
 | 
			
		||||
              </select>
 | 
			
		||||
            </label>
 | 
			
		||||
            <label>
 | 
			
		||||
              触发电平:
 | 
			
		||||
              <div class="flex items-center gap-2">
 | 
			
		||||
                <input type="range" min="0" max="255" step="1" v-model="osc.config.triggerLevel"
 | 
			
		||||
                  class="range range-sm w-50" />
 | 
			
		||||
                <input type="number" v-model="osc.config.triggerLevel" min="0" max="255"
 | 
			
		||||
                  class="input input-bordered w-24" />
 | 
			
		||||
    <!-- 主要内容区域 -->
 | 
			
		||||
    <div class="main-content grid grid-cols-1 xl:grid-cols-4 gap-6">
 | 
			
		||||
      <!-- 波形显示区域 - 占据大部分空间 -->
 | 
			
		||||
      <div class="waveform-section xl:col-span-3">
 | 
			
		||||
        <div
 | 
			
		||||
          class="card bg-white/80 dark:bg-slate-800/80 backdrop-blur-lg shadow-xl border border-white/20 h-full"
 | 
			
		||||
        >
 | 
			
		||||
          <div class="card-body p-6">
 | 
			
		||||
            <div class="waveform-header flex items-center justify-between mb-4">
 | 
			
		||||
              <h2
 | 
			
		||||
                class="text-lg font-semibold text-slate-800 dark:text-slate-200 flex items-center gap-2"
 | 
			
		||||
              >
 | 
			
		||||
                <Zap class="w-5 h-5 text-yellow-500" />
 | 
			
		||||
                波形显示
 | 
			
		||||
              </h2>
 | 
			
		||||
              <div class="waveform-controls flex items-center gap-2">
 | 
			
		||||
                <div
 | 
			
		||||
                  class="refresh-indicator flex items-center gap-2 text-sm text-slate-600 dark:text-slate-400"
 | 
			
		||||
                >
 | 
			
		||||
                  <div
 | 
			
		||||
                    class="w-2 h-2 bg-green-500 rounded-full animate-pulse"
 | 
			
		||||
                  ></div>
 | 
			
		||||
                  {{ refreshCycle }}ms 刷新
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </label>
 | 
			
		||||
            <label>
 | 
			
		||||
              水平偏移:
 | 
			
		||||
              <div class="flex items-center gap-2">
 | 
			
		||||
                <input type="range" min="0" max="1000" step="1" v-model="osc.config.horizontalShift"
 | 
			
		||||
                  class="range range-sm w-50" />
 | 
			
		||||
                <input type="number" v-model="osc.config.horizontalShift" min="0" max="1000"
 | 
			
		||||
                  class="input input-bordered w-24" />
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div
 | 
			
		||||
              class="waveform-display h-96 lg:h-[500px] relative overflow-hidden rounded-lg border border-slate-200 dark:border-slate-700"
 | 
			
		||||
            >
 | 
			
		||||
              <OscilloscopeWaveformDisplay class="w-full h-full" />
 | 
			
		||||
 | 
			
		||||
              <!-- 数据覆盖层 -->
 | 
			
		||||
              <div
 | 
			
		||||
                v-if="osc.isCapturing.value && !hasWaveformData"
 | 
			
		||||
                class="absolute inset-0 flex items-center justify-center bg-slate-50/50 dark:bg-slate-900/50 backdrop-blur-sm"
 | 
			
		||||
              >
 | 
			
		||||
                <div class="text-center space-y-4">
 | 
			
		||||
                  <div class="w-16 h-16 mx-auto text-slate-400">
 | 
			
		||||
                    <Activity class="w-full h-full" />
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <p class="text-slate-600 dark:text-slate-400">
 | 
			
		||||
                    等待波形数据...
 | 
			
		||||
                  </p>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </label>
 | 
			
		||||
            <label>
 | 
			
		||||
              抽取率:
 | 
			
		||||
              <div class="flex items-center gap-2">
 | 
			
		||||
                <input type="range" min="0" max="100" step="1" v-model="osc.config.decimationRate"
 | 
			
		||||
                  class="range range-sm w-50" />
 | 
			
		||||
                <input type="number" v-model="osc.config.decimationRate" min="0" max="100"
 | 
			
		||||
                  class="input input-bordered w-24" />
 | 
			
		||||
              </div>
 | 
			
		||||
            </label>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="flex gap-4">
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="flex items-center justify-between gap-2 mt-2">
 | 
			
		||||
            <label>
 | 
			
		||||
              刷新间隔(ms):
 | 
			
		||||
              <div class="flex items-center gap-2">
 | 
			
		||||
                <input type="range" min="100" max="3000" step="100" v-model="osc.refreshIntervalMs.value"
 | 
			
		||||
                  class="range range-sm w-50" />
 | 
			
		||||
                <input type="number" min="100" max="3000" step="100" v-model="osc.refreshIntervalMs.value"
 | 
			
		||||
                  class="input input-bordered w-24" />
 | 
			
		||||
              </div>
 | 
			
		||||
            </label>
 | 
			
		||||
            <div class="flex items-center gap-2">
 | 
			
		||||
              <button class="btn btn-primary" type="submit" :disabled="osc.isApplying.value || osc.isCapturing.value">
 | 
			
		||||
                应用配置
 | 
			
		||||
              </button>
 | 
			
		||||
              <button class="btn btn-secondary" type="button" @click="osc.resetConfiguration"
 | 
			
		||||
                :disabled="osc.isApplying.value || osc.isCapturing.value">
 | 
			
		||||
                重置
 | 
			
		||||
              </button>
 | 
			
		||||
              <button class="btn btn-outline" @click="osc.refreshRAM" :disabled="osc.isApplying.value || osc.isCapturing.value">
 | 
			
		||||
                刷新RAM
 | 
			
		||||
              </button>
 | 
			
		||||
              <!-- <button class="btn btn-accent" @click="osc.generateTestData" :disabled="osc.isOperationInProgress.value">
 | 
			
		||||
              生成测试数据
 | 
			
		||||
            </button> -->
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </form>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <!-- 控制面板 -->
 | 
			
		||||
      <div class="control-panel xl:col-span-1">
 | 
			
		||||
        <div class="space-y-6">
 | 
			
		||||
          <!-- 触发设置 -->
 | 
			
		||||
          <div
 | 
			
		||||
            class="card bg-white/80 dark:bg-slate-800/80 backdrop-blur-lg shadow-xl border border-white/20"
 | 
			
		||||
          >
 | 
			
		||||
            <div class="card-body p-4">
 | 
			
		||||
              <h3
 | 
			
		||||
                class="text-lg font-semibold text-slate-800 dark:text-slate-200 flex items-center gap-2 mb-4"
 | 
			
		||||
              >
 | 
			
		||||
                <Target class="w-5 h-5 text-red-500" />
 | 
			
		||||
                触发设置
 | 
			
		||||
              </h3>
 | 
			
		||||
 | 
			
		||||
              <div class="space-y-4">
 | 
			
		||||
                <div class="form-control">
 | 
			
		||||
                  <label class="label">
 | 
			
		||||
                    <span class="label-text font-medium">触发边沿</span>
 | 
			
		||||
                  </label>
 | 
			
		||||
                  <select
 | 
			
		||||
                    v-model="osc.config.triggerRisingEdge"
 | 
			
		||||
                    class="select select-bordered w-full focus:border-blue-500 transition-colors"
 | 
			
		||||
                  >
 | 
			
		||||
                    <option :value="true">上升沿 ↗</option>
 | 
			
		||||
                    <option :value="false">下降沿 ↘</option>
 | 
			
		||||
                  </select>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="form-control">
 | 
			
		||||
                  <label class="label">
 | 
			
		||||
                    <span class="label-text font-medium">触发电平</span>
 | 
			
		||||
                    <span class="label-text-alt">{{
 | 
			
		||||
                      osc.config.triggerLevel
 | 
			
		||||
                    }}</span>
 | 
			
		||||
                  </label>
 | 
			
		||||
                  <input
 | 
			
		||||
                    type="range"
 | 
			
		||||
                    min="0"
 | 
			
		||||
                    max="255"
 | 
			
		||||
                    step="1"
 | 
			
		||||
                    v-model="osc.config.triggerLevel"
 | 
			
		||||
                    class="range range-primary [--range-bg:#2b7fff]"
 | 
			
		||||
                  />
 | 
			
		||||
                  <div
 | 
			
		||||
                    class="range-labels flex justify-between text-xs text-slate-500 mt-1"
 | 
			
		||||
                  >
 | 
			
		||||
                    <span>0</span>
 | 
			
		||||
                    <span>128</span>
 | 
			
		||||
                    <span>255</span>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <!-- 时基设置 -->
 | 
			
		||||
          <div
 | 
			
		||||
            class="card bg-white/80 dark:bg-slate-800/80 backdrop-blur-lg shadow-xl border border-white/20"
 | 
			
		||||
          >
 | 
			
		||||
            <div class="card-body p-4">
 | 
			
		||||
              <h3
 | 
			
		||||
                class="text-lg font-semibold text-slate-800 dark:text-slate-200 flex items-center gap-2 mb-4"
 | 
			
		||||
              >
 | 
			
		||||
                <Clock class="w-5 h-5 text-blue-500" />
 | 
			
		||||
                时基控制
 | 
			
		||||
              </h3>
 | 
			
		||||
 | 
			
		||||
              <div class="space-y-4">
 | 
			
		||||
                <div class="form-control">
 | 
			
		||||
                  <label class="label">
 | 
			
		||||
                    <span class="label-text font-medium">水平偏移</span>
 | 
			
		||||
                    <span class="label-text-alt">{{
 | 
			
		||||
                      osc.config.horizontalShift
 | 
			
		||||
                    }}</span>
 | 
			
		||||
                  </label>
 | 
			
		||||
                  <input
 | 
			
		||||
                    type="range"
 | 
			
		||||
                    min="0"
 | 
			
		||||
                    max="1000"
 | 
			
		||||
                    step="1"
 | 
			
		||||
                    v-model="osc.config.horizontalShift"
 | 
			
		||||
                    class="range range-secondary [--range-bg:#c27aff]"
 | 
			
		||||
                  />
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="form-control">
 | 
			
		||||
                  <label class="label">
 | 
			
		||||
                    <span class="label-text font-medium">抽取率</span>
 | 
			
		||||
                    <span class="label-text-alt"
 | 
			
		||||
                      >{{ osc.config.decimationRate }}%</span
 | 
			
		||||
                    >
 | 
			
		||||
                  </label>
 | 
			
		||||
                  <input
 | 
			
		||||
                    type="range"
 | 
			
		||||
                    min="0"
 | 
			
		||||
                    max="100"
 | 
			
		||||
                    step="1"
 | 
			
		||||
                    v-model="osc.config.decimationRate"
 | 
			
		||||
                    class="range range-accent [--range-bg:#fb64b6]"
 | 
			
		||||
                  />
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="form-control">
 | 
			
		||||
                  <label class="label">
 | 
			
		||||
                    <span class="label-text font-medium">刷新间隔</span>
 | 
			
		||||
                    <span class="label-text-alt">{{ refreshCycle }}ms</span>
 | 
			
		||||
                  </label>
 | 
			
		||||
                  <input
 | 
			
		||||
                    type="range"
 | 
			
		||||
                    min="1"
 | 
			
		||||
                    max="1000"
 | 
			
		||||
                    step="1"
 | 
			
		||||
                    v-model="refreshCycle"
 | 
			
		||||
                    class="range range-info [--range-bg:#51a2ff]"
 | 
			
		||||
                  />
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <!-- 系统控制 -->
 | 
			
		||||
          <div
 | 
			
		||||
            class="card bg-white/80 dark:bg-slate-800/80 backdrop-blur-lg shadow-xl border border-white/20"
 | 
			
		||||
          >
 | 
			
		||||
            <div class="card-body p-4">
 | 
			
		||||
              <div
 | 
			
		||||
                class="card-title flex flex-row justify-between items-center mb-4"
 | 
			
		||||
              >
 | 
			
		||||
                <h3
 | 
			
		||||
                  class="text-lg font-semibold text-slate-800 dark:text-slate-200 flex items-center gap-2"
 | 
			
		||||
                >
 | 
			
		||||
                  <Settings class="w-5 h-5 text-purple-500" />
 | 
			
		||||
                  系统控制
 | 
			
		||||
                </h3>
 | 
			
		||||
 | 
			
		||||
                <!-- 自动应用开关 -->
 | 
			
		||||
                <div class="form-control">
 | 
			
		||||
                  <label class="label cursor-pointer">
 | 
			
		||||
                    <span class="label-text text-sm font-medium"
 | 
			
		||||
                      >自动应用设置</span
 | 
			
		||||
                    >
 | 
			
		||||
                    <input
 | 
			
		||||
                      type="checkbox"
 | 
			
		||||
                      class="toggle toggle-primary"
 | 
			
		||||
                      v-model="osc.isAutoApplying"
 | 
			
		||||
                    />
 | 
			
		||||
                  </label>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
 | 
			
		||||
              <div class="space-y-4">
 | 
			
		||||
                <!-- 控制按钮组 -->
 | 
			
		||||
                <div class="space-y-2">
 | 
			
		||||
                  <button
 | 
			
		||||
                    class="btn-primary-full"
 | 
			
		||||
                    @click="applyConfiguration"
 | 
			
		||||
                    :disabled="osc.isApplying.value || osc.isCapturing.value"
 | 
			
		||||
                  >
 | 
			
		||||
                    <CheckCircle class="w-4 h-4" />
 | 
			
		||||
                    应用配置
 | 
			
		||||
                  </button>
 | 
			
		||||
 | 
			
		||||
                  <button
 | 
			
		||||
                    class="btn-secondary-full"
 | 
			
		||||
                    @click="resetConfiguration"
 | 
			
		||||
                    :disabled="osc.isApplying.value || osc.isCapturing.value"
 | 
			
		||||
                  >
 | 
			
		||||
                    <RotateCcw class="w-4 h-4" />
 | 
			
		||||
                    重置配置
 | 
			
		||||
                  </button>
 | 
			
		||||
 | 
			
		||||
                  <button
 | 
			
		||||
                    class="btn-outline-full"
 | 
			
		||||
                    @click="osc.refreshRAM"
 | 
			
		||||
                    :disabled="osc.isApplying.value || osc.isCapturing.value"
 | 
			
		||||
                  >
 | 
			
		||||
                    <RefreshCw class="w-4 h-4" />
 | 
			
		||||
                    刷新RAM
 | 
			
		||||
                  </button>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- 状态提示 -->
 | 
			
		||||
    <div
 | 
			
		||||
      v-if="osc.isApplying.value"
 | 
			
		||||
      class="fixed bottom-4 right-4 alert alert-info shadow-lg max-w-sm animate-slide-in-right"
 | 
			
		||||
    >
 | 
			
		||||
      <div class="flex items-center gap-2">
 | 
			
		||||
        <div
 | 
			
		||||
          class="animate-spin w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full"
 | 
			
		||||
        ></div>
 | 
			
		||||
        <span>正在应用配置...</span>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { Activity } from "lucide-vue-next";
 | 
			
		||||
import {
 | 
			
		||||
  Activity,
 | 
			
		||||
  Settings,
 | 
			
		||||
  Play,
 | 
			
		||||
  Square,
 | 
			
		||||
  Trash2,
 | 
			
		||||
  Zap,
 | 
			
		||||
  Target,
 | 
			
		||||
  Clock,
 | 
			
		||||
  CheckCircle,
 | 
			
		||||
  RotateCcw,
 | 
			
		||||
  RefreshCw,
 | 
			
		||||
} from "lucide-vue-next";
 | 
			
		||||
import { OscilloscopeWaveformDisplay } from "@/components/Oscilloscope";
 | 
			
		||||
import { useEquipments } from "@/stores/equipments";
 | 
			
		||||
import { useOscilloscopeState } from "@/components/Oscilloscope/OscilloscopeManager";
 | 
			
		||||
import { useRequiredInjection } from "@/utils/Common";
 | 
			
		||||
import { ref, computed } from "vue";
 | 
			
		||||
 | 
			
		||||
// 使用全局设备配置
 | 
			
		||||
const equipments = useEquipments();
 | 
			
		||||
@@ -111,6 +338,178 @@ const equipments = useEquipments();
 | 
			
		||||
// 获取示波器状态和操作
 | 
			
		||||
const osc = useRequiredInjection(useOscilloscopeState);
 | 
			
		||||
 | 
			
		||||
const refreshCycle = ref(10);
 | 
			
		||||
 | 
			
		||||
// 计算是否有波形数据
 | 
			
		||||
const hasWaveformData = computed(() => {
 | 
			
		||||
  const data = osc.oscData.value;
 | 
			
		||||
  return data && data.x && data.y && data.x.length > 0;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 应用配置
 | 
			
		||||
const applyConfiguration = () => osc.applyConfiguration();
 | 
			
		||||
function applyConfiguration() {
 | 
			
		||||
  osc.applyConfiguration();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function toggleCapture() {
 | 
			
		||||
  osc.toggleCapture();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function resetConfiguration() {
 | 
			
		||||
  osc.resetConfiguration();
 | 
			
		||||
  refreshCycle.value = 1000 / osc.config.captureFrequency;
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="postcss">
 | 
			
		||||
@import "@/assets/main.css";
 | 
			
		||||
/* 渐变按钮样式 */
 | 
			
		||||
.btn-gradient {
 | 
			
		||||
  @apply px-6 py-3 rounded-lg font-medium transition-all duration-300 transform hover:scale-105 active:scale-95 shadow-lg flex items-center gap-2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.btn-start {
 | 
			
		||||
  @apply bg-gradient-to-r from-green-500 to-blue-600 hover:from-green-600 hover:to-blue-700 text-white shadow-green-200 hover:shadow-green-300;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.btn-stop {
 | 
			
		||||
  @apply bg-gradient-to-r from-red-500 to-pink-600 hover:from-red-600 hover:to-pink-700 text-white shadow-red-200 hover:shadow-red-300;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.btn-clear {
 | 
			
		||||
  @apply px-4 py-3 bg-gradient-to-r from-orange-500 to-red-500 hover:from-orange-600 hover:to-red-600 text-white rounded-lg font-medium transition-all duration-300 transform hover:scale-105 active:scale-95 shadow-lg flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 全宽按钮样式 */
 | 
			
		||||
.btn-primary-full {
 | 
			
		||||
  @apply w-full px-4 py-3 bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white rounded-lg font-medium transition-all duration-300 transform hover:scale-[1.02] active:scale-[0.98] shadow-lg flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.btn-secondary-full {
 | 
			
		||||
  @apply w-full px-4 py-3 bg-gradient-to-r from-gray-500 to-gray-600 hover:from-gray-600 hover:to-gray-700 text-white rounded-lg font-medium transition-all duration-300 transform hover:scale-[1.02] active:scale-[0.98] shadow-lg flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.btn-outline-full {
 | 
			
		||||
  @apply w-full px-4 py-3 border-2 border-slate-300 dark:border-slate-600 hover:border-blue-500 dark:hover:border-blue-400 text-slate-700 dark:text-slate-300 hover:text-blue-600 dark:hover:text-blue-400 rounded-lg font-medium transition-all duration-300 transform hover:scale-[1.02] active:scale-[0.98] shadow-sm hover:shadow-md flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 滑块样式美化 */
 | 
			
		||||
.range {
 | 
			
		||||
  @apply rounded-lg appearance-none cursor-pointer;
 | 
			
		||||
  --range-fill: 0;
 | 
			
		||||
  --range-thumb: white;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.range::-webkit-slider-thumb {
 | 
			
		||||
  @apply appearance-none bg-white border-2 border-current rounded-full cursor-pointer shadow-lg hover:shadow-xl transition-shadow duration-200;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.range::-moz-range-thumb {
 | 
			
		||||
  @apply bg-white border-2 border-current rounded-full cursor-pointer shadow-lg hover:shadow-xl transition-shadow duration-200;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 范围标签 */
 | 
			
		||||
.range-labels {
 | 
			
		||||
  margin-top: 0.25rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 卡片悬停效果 */
 | 
			
		||||
.card {
 | 
			
		||||
  @apply transition-all duration-300 hover:shadow-2xl hover:scale-[1.01];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 自定义动画 */
 | 
			
		||||
@keyframes slide-in-right {
 | 
			
		||||
  from {
 | 
			
		||||
    transform: translateX(100%);
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
  }
 | 
			
		||||
  to {
 | 
			
		||||
    transform: translateX(0);
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.animate-slide-in-right {
 | 
			
		||||
  animation: slide-in-right 0.3s ease-out;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 玻璃态效果增强 */
 | 
			
		||||
.backdrop-blur-lg {
 | 
			
		||||
  backdrop-filter: blur(16px);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 状态指示器脉动效果 */
 | 
			
		||||
@keyframes pulse-glow {
 | 
			
		||||
  0%,
 | 
			
		||||
  100% {
 | 
			
		||||
    box-shadow: 0 0 5px currentColor;
 | 
			
		||||
  }
 | 
			
		||||
  50% {
 | 
			
		||||
    box-shadow:
 | 
			
		||||
      0 0 20px currentColor,
 | 
			
		||||
      0 0 30px currentColor;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.status-indicator .animate-pulse {
 | 
			
		||||
  animation: pulse-glow 2s infinite;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 响应式调整 */
 | 
			
		||||
@media (max-width: 1280px) {
 | 
			
		||||
  .main-content {
 | 
			
		||||
    @apply grid-cols-1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .control-panel {
 | 
			
		||||
    @apply order-first;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .control-panel .space-y-6 {
 | 
			
		||||
    @apply grid grid-cols-1 md:grid-cols-3 gap-4 space-y-0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (max-width: 768px) {
 | 
			
		||||
  .control-panel .space-y-6 {
 | 
			
		||||
    @apply grid-cols-1 space-y-4;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .control-buttons {
 | 
			
		||||
    @apply flex-col gap-2;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .status-bar .card-body {
 | 
			
		||||
    @apply flex-col items-start gap-4;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 滚动条美化 */
 | 
			
		||||
::-webkit-scrollbar {
 | 
			
		||||
  @apply w-2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
::-webkit-scrollbar-track {
 | 
			
		||||
  @apply bg-slate-100 dark:bg-slate-800 rounded-full;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
::-webkit-scrollbar-thumb {
 | 
			
		||||
  @apply bg-slate-300 dark:bg-slate-600 rounded-full hover:bg-slate-400 dark:hover:bg-slate-500;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 输入焦点效果 */
 | 
			
		||||
.select:focus,
 | 
			
		||||
.input:focus {
 | 
			
		||||
  @apply ring-2 ring-blue-500 opacity-50 border-blue-500;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 切换开关样式 */
 | 
			
		||||
.toggle {
 | 
			
		||||
  @apply transition-all duration-300;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.toggle:checked {
 | 
			
		||||
  @apply shadow-lg;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user