Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab
This commit is contained in:
		
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -29,7 +29,7 @@ DebuggerCmd.md
 | 
				
			|||||||
*.ntvs*
 | 
					*.ntvs*
 | 
				
			||||||
*.njsproj
 | 
					*.njsproj
 | 
				
			||||||
*.sw?
 | 
					*.sw?
 | 
				
			||||||
 | 
					prompt.md
 | 
				
			||||||
*.tsbuildinfo
 | 
					*.tsbuildinfo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Generated Files
 | 
					# Generated Files
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										13
									
								
								TODO.md
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								TODO.md
									
									
									
									
									
								
							@@ -1,13 +0,0 @@
 | 
				
			|||||||
# TODO
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
1. 后端HTTP视频流
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
640*480, RGB565
 | 
					 | 
				
			||||||
0x0000_0000 + 25800
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
2. 信号发生器界面导入.dat文件
 | 
					 | 
				
			||||||
3. 示波器后端交互、前端界面
 | 
					 | 
				
			||||||
4. 逻辑分析仪后端交互、前端界面
 | 
					 | 
				
			||||||
5. 前端重构
 | 
					 | 
				
			||||||
6. 数据库 —— 用户登录、板卡资源分配、板卡IP地址分配
 | 
					 | 
				
			||||||
@@ -180,8 +180,7 @@ try
 | 
				
			|||||||
    builder.Services.AddHostedService(provider => provider.GetRequiredService<HttpHdmiVideoStreamService>());
 | 
					    builder.Services.AddHostedService(provider => provider.GetRequiredService<HttpHdmiVideoStreamService>());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // 添加进度跟踪服务
 | 
					    // 添加进度跟踪服务
 | 
				
			||||||
    builder.Services.AddSingleton<ProgressTrackerService>();
 | 
					    builder.Services.AddSingleton<ProgressTracker>();
 | 
				
			||||||
    builder.Services.AddHostedService(provider => provider.GetRequiredService<ProgressTrackerService>());
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Application Settings
 | 
					    // Application Settings
 | 
				
			||||||
    var app = builder.Build();
 | 
					    var app = builder.Build();
 | 
				
			||||||
@@ -258,6 +257,8 @@ try
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // Setup Program
 | 
					    // Setup Program
 | 
				
			||||||
    MsgBus.Init();
 | 
					    MsgBus.Init();
 | 
				
			||||||
 | 
					    var progressTracker = app.Services.GetRequiredService<ProgressTracker>();
 | 
				
			||||||
 | 
					    MsgBus.SetProgressTracker(progressTracker);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Generate API Client
 | 
					    // Generate API Client
 | 
				
			||||||
    app.MapGet("GetAPIClientCode", async (HttpContext context) =>
 | 
					    app.MapGet("GetAPIClientCode", async (HttpContext context) =>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -368,11 +368,8 @@ public class ExamController : ControllerBase
 | 
				
			|||||||
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 | 
					    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 | 
				
			||||||
    [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
					    [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
				
			||||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
					    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
				
			||||||
    public IActionResult DeleteCommit(string commitId)
 | 
					    public IActionResult DeleteCommit(Guid commitId)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if (!Guid.TryParse(commitId, out _))
 | 
					 | 
				
			||||||
            return BadRequest("提交记录ID格式不正确");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        try
 | 
					        try
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            // 获取当前用户信息
 | 
					            // 获取当前用户信息
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,17 +16,12 @@ public class JtagController : ControllerBase
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
    private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
					    private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private readonly ProgressTrackerService _tracker;
 | 
					    private readonly ProgressTracker _tracker = MsgBus.ProgressTracker;
 | 
				
			||||||
    private readonly UserManager _userManager = new();
 | 
					    private readonly UserManager _userManager = new();
 | 
				
			||||||
    private readonly ResourceManager _resourceManager = new();
 | 
					    private readonly ResourceManager _resourceManager = new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private const string BITSTREAM_PATH = "bitstream/Jtag";
 | 
					    private const string BITSTREAM_PATH = "bitstream/Jtag";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public JtagController(ProgressTrackerService tracker)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        _tracker = tracker;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// <summary>
 | 
					    /// <summary>
 | 
				
			||||||
    /// 控制器首页信息
 | 
					    /// 控制器首页信息
 | 
				
			||||||
    /// </summary>
 | 
					    /// </summary>
 | 
				
			||||||
@@ -137,7 +132,7 @@ public class JtagController : ControllerBase
 | 
				
			|||||||
    [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
 | 
					    [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
 | 
				
			||||||
    [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
 | 
					    [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
 | 
				
			||||||
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
 | 
					    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
 | 
				
			||||||
    public IResult DownloadBitstream(string address, int port, string bitstreamId, CancellationToken cancelToken)
 | 
					    public IResult DownloadBitstream(string address, int port, Guid bitstreamId, CancellationToken cancelToken)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        logger.Info($"User {User.Identity?.Name} initiating bitstream download to device {address}:{port} using bitstream ID: {bitstreamId}");
 | 
					        logger.Info($"User {User.Identity?.Name} initiating bitstream download to device {address}:{port} using bitstream ID: {bitstreamId}");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -188,8 +183,8 @@ public class JtagController : ControllerBase
 | 
				
			|||||||
            logger.Info($"User {username} processing bitstream file of size: {fileBytes.Length} bytes");
 | 
					            logger.Info($"User {username} processing bitstream file of size: {fileBytes.Length} bytes");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // 定义进度跟踪
 | 
					            // 定义进度跟踪
 | 
				
			||||||
            var (taskId, progress) = _tracker.CreateTask(cancelToken);
 | 
					            var taskId = _tracker.CreateTask(10000);
 | 
				
			||||||
            progress.Report(10);
 | 
					            _tracker.AdvanceProgress(taskId, 10);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            _ = Task.Run(async () =>
 | 
					            _ = Task.Run(async () =>
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@@ -210,7 +205,8 @@ public class JtagController : ControllerBase
 | 
				
			|||||||
                        if (!retBuffer.IsSuccessful)
 | 
					                        if (!retBuffer.IsSuccessful)
 | 
				
			||||||
                        {
 | 
					                        {
 | 
				
			||||||
                            logger.Error($"User {username} failed to reverse bytes: {retBuffer.Error}");
 | 
					                            logger.Error($"User {username} failed to reverse bytes: {retBuffer.Error}");
 | 
				
			||||||
                            progress.Error($"User {username} failed to reverse bytes: {retBuffer.Error}");
 | 
					                            _tracker.FailProgress(taskId,
 | 
				
			||||||
 | 
					                                $"User {username} failed to reverse bytes: {retBuffer.Error}");
 | 
				
			||||||
                            return;
 | 
					                            return;
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                        revBuffer = retBuffer.Value;
 | 
					                        revBuffer = retBuffer.Value;
 | 
				
			||||||
@@ -228,21 +224,22 @@ public class JtagController : ControllerBase
 | 
				
			|||||||
                    var processedBytes = outputStream.ToArray();
 | 
					                    var processedBytes = outputStream.ToArray();
 | 
				
			||||||
                    logger.Info($"User {username} processed {totalBytesProcessed} bytes for device {address}");
 | 
					                    logger.Info($"User {username} processed {totalBytesProcessed} bytes for device {address}");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    progress.Report(20);
 | 
					                    _tracker.AdvanceProgress(taskId, 20);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    // 下载比特流
 | 
					                    // 下载比特流
 | 
				
			||||||
                    var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
 | 
					                    var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
 | 
				
			||||||
                    var ret = await jtagCtrl.DownloadBitstream(processedBytes);
 | 
					                    var ret = await jtagCtrl.DownloadBitstream(processedBytes, taskId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if (ret.IsSuccessful)
 | 
					                    if (ret.IsSuccessful)
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        logger.Info($"User {username} successfully downloaded bitstream '{resource.ResourceName}' to device {address}");
 | 
					                        logger.Info($"User {username} successfully downloaded bitstream '{resource.ResourceName}' to device {address}");
 | 
				
			||||||
                        progress.Finish();
 | 
					                        _tracker.CompleteProgress(taskId);
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    else
 | 
					                    else
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        logger.Error($"User {username} failed to download bitstream to device {address}: {ret.Error}");
 | 
					                        logger.Error($"User {username} failed to download bitstream to device {address}: {ret.Error}");
 | 
				
			||||||
                        progress.Error($"User {username} failed to download bitstream to device {address}: {ret.Error}");
 | 
					                        _tracker.FailProgress(taskId,
 | 
				
			||||||
 | 
					                            $"User {username} failed to download bitstream to device {address}: {ret.Error}");
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -78,7 +78,7 @@ public class ResourceController : ControllerBase
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            var resourceInfo = new ResourceInfo(result.Value);
 | 
					            var resourceInfo = new ResourceInfo(result.Value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            logger.Info($"成功添加资源: {request.ResourceType}/{request.ResourcePurpose}/{file.FileName}");
 | 
					            logger.Info($"成功添加资源: {request.ResourceType}/{request.ResourcePurpose}/{file.FileName} ID: {resourceInfo.ID}");
 | 
				
			||||||
            return CreatedAtAction(nameof(GetResourceById), new { resourceId = resourceInfo.ID }, resourceInfo);
 | 
					            return CreatedAtAction(nameof(GetResourceById), new { resourceId = resourceInfo.ID }, resourceInfo);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        catch (Exception ex)
 | 
					        catch (Exception ex)
 | 
				
			||||||
@@ -187,7 +187,7 @@ public class ResourceController : ControllerBase
 | 
				
			|||||||
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
					    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
				
			||||||
    [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
					    [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
				
			||||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
					    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
				
			||||||
    public IActionResult GetResourceById(string resourceId)
 | 
					    public IActionResult GetResourceById(Guid resourceId)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        try
 | 
					        try
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
@@ -231,7 +231,7 @@ public class ResourceController : ControllerBase
 | 
				
			|||||||
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 | 
					    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 | 
				
			||||||
    [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
					    [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
				
			||||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
					    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
				
			||||||
    public IActionResult DeleteResource(string resourceId)
 | 
					    public IActionResult DeleteResource(Guid resourceId)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        try
 | 
					        try
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
@@ -293,7 +293,7 @@ public class ResourceInfo
 | 
				
			|||||||
    /// <summary>
 | 
					    /// <summary>
 | 
				
			||||||
    /// 资源ID
 | 
					    /// 资源ID
 | 
				
			||||||
    /// </summary>
 | 
					    /// </summary>
 | 
				
			||||||
    public string ID { get; set; } = string.Empty;
 | 
					    public Guid ID { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// <summary>
 | 
					    /// <summary>
 | 
				
			||||||
    /// 资源名称
 | 
					    /// 资源名称
 | 
				
			||||||
@@ -327,7 +327,7 @@ public class ResourceInfo
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public ResourceInfo(Resource resource)
 | 
					    public ResourceInfo(Resource resource)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        ID = resource.ID.ToString();
 | 
					        ID = resource.ID;
 | 
				
			||||||
        Name = resource.ResourceName;
 | 
					        Name = resource.ResourceName;
 | 
				
			||||||
        Type = resource.ResourceType;
 | 
					        Type = resource.ResourceType;
 | 
				
			||||||
        Purpose = resource.Purpose;
 | 
					        Purpose = resource.Purpose;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										127
									
								
								server/src/Controllers/SwitchController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								server/src/Controllers/SwitchController.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,127 @@
 | 
				
			|||||||
 | 
					using Microsoft.AspNetCore.Mvc;
 | 
				
			||||||
 | 
					using Microsoft.AspNetCore.Cors;
 | 
				
			||||||
 | 
					using Peripherals.SwitchClient;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace server.Controllers;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[ApiController]
 | 
				
			||||||
 | 
					[Route("api/[controller]")]
 | 
				
			||||||
 | 
					public class SwitchController : ControllerBase
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private readonly Database.UserManager _userManager = new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// <summary>
 | 
				
			||||||
 | 
					    /// 获取示波器实例
 | 
				
			||||||
 | 
					    /// </summary>
 | 
				
			||||||
 | 
					    private SwitchCtrl? GetSwitchCtrl()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        var userName = User.Identity?.Name;
 | 
				
			||||||
 | 
					        if (string.IsNullOrEmpty(userName))
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var userRet = _userManager.GetUserByName(userName);
 | 
				
			||||||
 | 
					        if (!userRet.IsSuccessful || !userRet.Value.HasValue)
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var user = userRet.Value.Value;
 | 
				
			||||||
 | 
					        if (user.BoardID == Guid.Empty)
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var boardRet = _userManager.GetBoardByID(user.BoardID);
 | 
				
			||||||
 | 
					        if (!boardRet.IsSuccessful || !boardRet.Value.HasValue)
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var board = boardRet.Value.Value;
 | 
				
			||||||
 | 
					        return new SwitchCtrl(board.IpAddr, board.Port, 0);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// <summary>
 | 
				
			||||||
 | 
					    /// 启用或禁用 Switch 外设
 | 
				
			||||||
 | 
					    /// </summary>
 | 
				
			||||||
 | 
					    /// <param name="enable">是否启用</param>
 | 
				
			||||||
 | 
					    /// <returns>操作结果</returns>
 | 
				
			||||||
 | 
					    [HttpPost("enable")]
 | 
				
			||||||
 | 
					    [EnableCors("Users")]
 | 
				
			||||||
 | 
					    [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
 | 
				
			||||||
 | 
					    [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
 | 
				
			||||||
 | 
					    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
 | 
				
			||||||
 | 
					    public async Task<IActionResult> SetEnable([FromQuery] bool enable)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        var switchCtrl = GetSwitchCtrl();
 | 
				
			||||||
 | 
					        if (switchCtrl == null)
 | 
				
			||||||
 | 
					            return BadRequest("Can't get user or board info");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var result = await switchCtrl.SetEnable(enable);
 | 
				
			||||||
 | 
					        if (!result.IsSuccessful)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            logger.Error(result.Error, "SetEnable failed");
 | 
				
			||||||
 | 
					            return StatusCode(500, result.Error);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return Ok(result.Value);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// <summary>
 | 
				
			||||||
 | 
					    /// 控制指定编号的 Switch 开关
 | 
				
			||||||
 | 
					    /// </summary>
 | 
				
			||||||
 | 
					    /// <param name="num">开关编号</param>
 | 
				
			||||||
 | 
					    /// <param name="onOff">开/关</param>
 | 
				
			||||||
 | 
					    /// <returns>操作结果</returns>
 | 
				
			||||||
 | 
					    [HttpPost("switch")]
 | 
				
			||||||
 | 
					    [EnableCors("Users")]
 | 
				
			||||||
 | 
					    [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
 | 
				
			||||||
 | 
					    [ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
 | 
				
			||||||
 | 
					    [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
 | 
				
			||||||
 | 
					    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
 | 
				
			||||||
 | 
					    public async Task<IActionResult> SetSwitchOnOff([FromQuery] int num, [FromQuery] bool onOff)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if (num <= 0 || num > 6)
 | 
				
			||||||
 | 
					            return BadRequest(new ArgumentException($"Switch num should be 1~5, instead of {num}"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var switchCtrl = GetSwitchCtrl();
 | 
				
			||||||
 | 
					        if (switchCtrl == null)
 | 
				
			||||||
 | 
					            return BadRequest("Can't get user or board info");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var result = await switchCtrl.SetSwitchOnOff(num, onOff);
 | 
				
			||||||
 | 
					        if (!result.IsSuccessful)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            logger.Error(result.Error, $"SetSwitchOnOff({num}, {onOff}) failed");
 | 
				
			||||||
 | 
					            return StatusCode(500, result.Error);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return Ok(result.Value);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// <summary>
 | 
				
			||||||
 | 
					    /// 控制 Switch 开关
 | 
				
			||||||
 | 
					    /// </summary>
 | 
				
			||||||
 | 
					    /// <param name="keyStatus">开关状态</param>
 | 
				
			||||||
 | 
					    /// <returns>操作结果</returns>
 | 
				
			||||||
 | 
					    [HttpPost("MultiSwitch")]
 | 
				
			||||||
 | 
					    [EnableCors("Users")]
 | 
				
			||||||
 | 
					    [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
 | 
				
			||||||
 | 
					    [ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
 | 
				
			||||||
 | 
					    [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
 | 
				
			||||||
 | 
					    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
 | 
				
			||||||
 | 
					    public async Task<IActionResult> SetMultiSwitchsOnOff(bool[] keyStatus)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if (keyStatus.Length == 0 || keyStatus.Length > 6) return BadRequest(
 | 
				
			||||||
 | 
					                new ArgumentException($"Switch num should be 1~5, instead of {keyStatus.Length}"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var switchCtrl = GetSwitchCtrl();
 | 
				
			||||||
 | 
					        if (switchCtrl == null)
 | 
				
			||||||
 | 
					            return BadRequest("Can't get user or board info");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for (int i = 0; i < keyStatus.Length; i++)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            var result = await switchCtrl.SetSwitchOnOff(i + 1, keyStatus[i]);
 | 
				
			||||||
 | 
					            if (!result.IsSuccessful)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                logger.Error(result.Error, $"SetSwitchOnOff({i}, {keyStatus[i]}) failed");
 | 
				
			||||||
 | 
					                return StatusCode(500, result.Error);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (!result.Value) return Ok(false);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return Ok(true);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -159,7 +159,7 @@ public class ResourceManager
 | 
				
			|||||||
            var duplicateResource = _db.ResourceTable.Where(r => r.SHA256 == sha256).FirstOrDefault();
 | 
					            var duplicateResource = _db.ResourceTable.Where(r => r.SHA256 == sha256).FirstOrDefault();
 | 
				
			||||||
            if (duplicateResource != null && duplicateResource.ResourceName == resourceName)
 | 
					            if (duplicateResource != null && duplicateResource.ResourceName == resourceName)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                logger.Info($"资源已存在: {resourceName}");
 | 
					                logger.Info($"资源已存在: {resourceName}, ID: {duplicateResource.ID}, UserID: {duplicateResource.UserID}");
 | 
				
			||||||
                return duplicateResource;
 | 
					                return duplicateResource;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -311,9 +311,9 @@ public class ResourceManager
 | 
				
			|||||||
    /// </summary>
 | 
					    /// </summary>
 | 
				
			||||||
    /// <param name="resourceId">资源ID</param>
 | 
					    /// <param name="resourceId">资源ID</param>
 | 
				
			||||||
    /// <returns>资源数据</returns>
 | 
					    /// <returns>资源数据</returns>
 | 
				
			||||||
    public Optional<Resource> GetResourceById(string resourceId)
 | 
					    public Optional<Resource> GetResourceById(Guid resourceId)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        var resource = _db.ResourceTable.Where(r => r.ID.ToString() == resourceId).FirstOrDefault();
 | 
					        var resource = _db.ResourceTable.Where(r => r.ID == resourceId).FirstOrDefault();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (resource == null)
 | 
					        if (resource == null)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
@@ -330,11 +330,11 @@ public class ResourceManager
 | 
				
			|||||||
    /// </summary>
 | 
					    /// </summary>
 | 
				
			||||||
    /// <param name="resourceId">资源ID</param>
 | 
					    /// <param name="resourceId">资源ID</param>
 | 
				
			||||||
    /// <returns>删除的记录数</returns>
 | 
					    /// <returns>删除的记录数</returns>
 | 
				
			||||||
    public Result<int> DeleteResource(string resourceId)
 | 
					    public Result<int> DeleteResource(Guid resourceId)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        try
 | 
					        try
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var result = _db.ResourceTable.Where(r => r.ID.ToString() == resourceId).Delete();
 | 
					            var result = _db.ResourceTable.Where(r => r.ID == resourceId).Delete();
 | 
				
			||||||
            logger.Info($"资源已删除: {resourceId},删除记录数: {result}");
 | 
					            logger.Info($"资源已删除: {resourceId},删除记录数: {result}");
 | 
				
			||||||
            return new(result);
 | 
					            return new(result);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,6 +8,8 @@ using DotNext;
 | 
				
			|||||||
using Peripherals.SevenDigitalTubesClient;
 | 
					using Peripherals.SevenDigitalTubesClient;
 | 
				
			||||||
using System.Collections.Concurrent;
 | 
					using System.Collections.Concurrent;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#pragma warning disable 1998
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace server.Hubs;
 | 
					namespace server.Hubs;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[Hub]
 | 
					[Hub]
 | 
				
			||||||
@@ -16,7 +18,7 @@ public interface IDigitalTubesHub
 | 
				
			|||||||
    Task<bool> StartScan();
 | 
					    Task<bool> StartScan();
 | 
				
			||||||
    Task<bool> StopScan();
 | 
					    Task<bool> StopScan();
 | 
				
			||||||
    Task<bool> SetFrequency(int frequency);
 | 
					    Task<bool> SetFrequency(int frequency);
 | 
				
			||||||
    Task<DigitalTubeTaskStatus> GetStatus();
 | 
					    Task<DigitalTubeTaskStatus?> GetStatus();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[Receiver]
 | 
					[Receiver]
 | 
				
			||||||
@@ -31,23 +33,27 @@ public class DigitalTubeTaskStatus
 | 
				
			|||||||
    public int Frequency { get; set; } = 100;
 | 
					    public int Frequency { get; set; } = 100;
 | 
				
			||||||
    public bool IsRunning { get; set; } = false;
 | 
					    public bool IsRunning { get; set; } = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public DigitalTubeTaskStatus(DigitalTubeInfo info)
 | 
					    public DigitalTubeTaskStatus(ScanTaskInfo info)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        Frequency = info.Frequency;
 | 
					        Frequency = info.Frequency;
 | 
				
			||||||
        IsRunning = info.IsRunning;
 | 
					        IsRunning = info.IsRunning;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public class DigitalTubeInfo
 | 
					public class ScanTaskInfo
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
 | 
					    public string BoardID { get; set; }
 | 
				
			||||||
    public string ClientID { get; set; }
 | 
					    public string ClientID { get; set; }
 | 
				
			||||||
 | 
					    public Task? ScanTask { get; set; }
 | 
				
			||||||
    public SevenDigitalTubesCtrl TubeClient { get; set; }
 | 
					    public SevenDigitalTubesCtrl TubeClient { get; set; }
 | 
				
			||||||
    public CancellationTokenSource CTS { get; set; } = new CancellationTokenSource();
 | 
					    public CancellationTokenSource CTS { get; set; } = new();
 | 
				
			||||||
    public int Frequency { get; set; } = 100;
 | 
					    public int Frequency { get; set; } = 100;
 | 
				
			||||||
    public bool IsRunning { get; set; } = false;
 | 
					    public bool IsRunning { get; set; } = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public DigitalTubeInfo(string clientID, SevenDigitalTubesCtrl client)
 | 
					    public ScanTaskInfo(
 | 
				
			||||||
 | 
					        string boardID, string clientID, SevenDigitalTubesCtrl client)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					        BoardID = boardID;
 | 
				
			||||||
        ClientID = clientID;
 | 
					        ClientID = clientID;
 | 
				
			||||||
        TubeClient = client;
 | 
					        TubeClient = client;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -58,11 +64,10 @@ public class DigitalTubeInfo
 | 
				
			|||||||
public class DigitalTubesHub : Hub<IDigitalTubesReceiver>, IDigitalTubesHub
 | 
					public class DigitalTubesHub : Hub<IDigitalTubesReceiver>, IDigitalTubesHub
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
					    private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
				
			||||||
 | 
					 | 
				
			||||||
    private readonly IHubContext<DigitalTubesHub, IDigitalTubesReceiver> _hubContext;
 | 
					    private readonly IHubContext<DigitalTubesHub, IDigitalTubesReceiver> _hubContext;
 | 
				
			||||||
    private readonly Database.UserManager _userManager = new();
 | 
					    private readonly Database.UserManager _userManager = new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private ConcurrentDictionary<string, DigitalTubeInfo> _infoDict = new();
 | 
					    private ConcurrentDictionary<(string, string), ScanTaskInfo> _scanTasks = new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public DigitalTubesHub(IHubContext<DigitalTubesHub, IDigitalTubesReceiver> hubContext)
 | 
					    public DigitalTubesHub(IHubContext<DigitalTubesHub, IDigitalTubesReceiver> hubContext)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
@@ -95,17 +100,18 @@ public class DigitalTubesHub : Hub<IDigitalTubesReceiver>, IDigitalTubesHub
 | 
				
			|||||||
        return boardRet.Value.Value;
 | 
					        return boardRet.Value.Value;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private Task ScanAllTubes(DigitalTubeInfo info)
 | 
					    private Task ScanAllTubes(ScanTaskInfo scanInfo)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					        var token = scanInfo.CTS.Token;
 | 
				
			||||||
        return Task.Run(async () =>
 | 
					        return Task.Run(async () =>
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var cntError = 0;
 | 
					            var cntError = 0;
 | 
				
			||||||
            while (!info.CTS.IsCancellationRequested)
 | 
					            while (!token.IsCancellationRequested)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                var beginTime = DateTime.Now;
 | 
					                var beginTime = DateTime.Now;
 | 
				
			||||||
                var waitTime = TimeSpan.FromMilliseconds(1000 / info.Frequency);
 | 
					                var waitTime = TimeSpan.FromMilliseconds(1000 / scanInfo.Frequency);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                var dataRet = await info.TubeClient.ScanAllTubes();
 | 
					                var dataRet = await scanInfo.TubeClient.ScanAllTubes();
 | 
				
			||||||
                if (!dataRet.IsSuccessful)
 | 
					                if (!dataRet.IsSuccessful)
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    logger.Error($"Failed to scan tubes: {dataRet.Error}");
 | 
					                    logger.Error($"Failed to scan tubes: {dataRet.Error}");
 | 
				
			||||||
@@ -113,126 +119,138 @@ public class DigitalTubesHub : Hub<IDigitalTubesReceiver>, IDigitalTubesHub
 | 
				
			|||||||
                    if (cntError > 3)
 | 
					                    if (cntError > 3)
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        logger.Error($"Too many errors, stopping scan");
 | 
					                        logger.Error($"Too many errors, stopping scan");
 | 
				
			||||||
                        info.IsRunning = false;
 | 
					                        break;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                await _hubContext.Clients.Client(info.ClientID).OnReceive(dataRet.Value);
 | 
					                await _hubContext.Clients.Client(scanInfo.ClientID).OnReceive(dataRet.Value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                var processTime = DateTime.Now - beginTime;
 | 
					                var processTime = DateTime.Now - beginTime;
 | 
				
			||||||
                if (processTime < waitTime)
 | 
					                if (processTime < waitTime)
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    await Task.Delay(waitTime - processTime);
 | 
					                    await Task.Delay(waitTime - processTime, token);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }, info.CTS.Token);
 | 
					            scanInfo.IsRunning = false;
 | 
				
			||||||
 | 
					        }, token)
 | 
				
			||||||
 | 
					        .ContinueWith((task) =>
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            if (task.IsFaulted)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                logger.Error(
 | 
				
			||||||
 | 
					                    $"Digital tubes scan operation failesj for board {task.Exception}");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            else if (task.IsCanceled)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                logger.Info(
 | 
				
			||||||
 | 
					                    $"Digital tubes scan operation cancelled for board {scanInfo.BoardID}");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            else
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                logger.Info(
 | 
				
			||||||
 | 
					                    $"Digital tubes scan completed successfully for board {scanInfo.BoardID}");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public Task<bool> StartScan()
 | 
					    public async Task<bool> StartScan()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        try
 | 
					        try
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
 | 
					            var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
 | 
				
			||||||
            if (_infoDict.GetOrAdd(
 | 
					            var key = (board.ID.ToString(), Context.ConnectionId);
 | 
				
			||||||
                board.ID.ToString(),
 | 
					 | 
				
			||||||
                (_) => new DigitalTubeInfo(
 | 
					 | 
				
			||||||
                    Context.ConnectionId,
 | 
					 | 
				
			||||||
                    new SevenDigitalTubesCtrl(board.IpAddr, board.Port, 2))
 | 
					 | 
				
			||||||
            ) is DigitalTubeInfo info)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                if (!info.IsRunning)
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    info.IsRunning = true;
 | 
					 | 
				
			||||||
                    if (info.CTS.IsCancellationRequested)
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        info.CTS.Dispose();
 | 
					 | 
				
			||||||
                        info.CTS = new CancellationTokenSource();
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    _ = ScanAllTubes(info);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return Task.FromResult(true);
 | 
					            if (_scanTasks.TryGetValue(key, out var existing) && existing.IsRunning)
 | 
				
			||||||
 | 
					                return true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            var cts = new CancellationTokenSource();
 | 
				
			||||||
 | 
					            var scanTaskInfo = new ScanTaskInfo(
 | 
				
			||||||
 | 
					                board.ID.ToString(), Context.ConnectionId,
 | 
				
			||||||
 | 
					                new SevenDigitalTubesCtrl(board.IpAddr, board.Port, 0)
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            scanTaskInfo.ScanTask = ScanAllTubes(scanTaskInfo);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            _scanTasks[key] = scanTaskInfo;
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        catch (Exception ex)
 | 
					        catch (Exception ex)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            logger.Error(ex, "Failed to start scan");
 | 
					            logger.Error(ex, "Failed to start scan");
 | 
				
			||||||
            return Task.FromResult(false);
 | 
					            return false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public Task<bool> StopScan()
 | 
					    public async Task<bool> StopScan()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        try
 | 
					        try
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
 | 
					            var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
 | 
				
			||||||
            if (_infoDict.GetOrAdd(
 | 
					            var key = (board.ID.ToString(), Context.ConnectionId);
 | 
				
			||||||
                board.ID.ToString(),
 | 
					 | 
				
			||||||
                (_) => new DigitalTubeInfo(
 | 
					 | 
				
			||||||
                    Context.ConnectionId,
 | 
					 | 
				
			||||||
                    new SevenDigitalTubesCtrl(board.IpAddr, board.Port, 2))
 | 
					 | 
				
			||||||
            ) is DigitalTubeInfo info)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                info.IsRunning = false;
 | 
					 | 
				
			||||||
                info.CTS.Cancel();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return Task.FromResult(true);
 | 
					            if (_scanTasks.TryRemove(key, out var scanInfo))
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                scanInfo.IsRunning = false;
 | 
				
			||||||
 | 
					                scanInfo.CTS.Cancel();
 | 
				
			||||||
 | 
					                if (scanInfo.ScanTask != null)
 | 
				
			||||||
 | 
					                    await scanInfo.ScanTask;
 | 
				
			||||||
 | 
					                scanInfo.CTS.Dispose();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        catch (Exception ex)
 | 
					        catch (Exception ex)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            logger.Error(ex, "Failed to stop scan");
 | 
					            logger.Error(ex, "Failed to stop scan");
 | 
				
			||||||
            return Task.FromResult(false);
 | 
					            return false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public Task<bool> SetFrequency(int frequency)
 | 
					    public async Task<bool> SetFrequency(int frequency)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        try
 | 
					        try
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            if (frequency < 1 || frequency > 1000) return Task.FromException<bool>(
 | 
					            if (frequency < 1 || frequency > 1000)
 | 
				
			||||||
                new ArgumentException("Frequency must be between 1 and 1000"));
 | 
					                return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
 | 
					            var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
 | 
				
			||||||
            if (_infoDict.GetOrAdd(
 | 
					            var key = (board.ID.ToString(), Context.ConnectionId);
 | 
				
			||||||
                board.ID.ToString(),
 | 
					 | 
				
			||||||
                (_) => new DigitalTubeInfo(
 | 
					 | 
				
			||||||
                    Context.ConnectionId,
 | 
					 | 
				
			||||||
                    new SevenDigitalTubesCtrl(board.IpAddr, board.Port, 2))
 | 
					 | 
				
			||||||
            ) is DigitalTubeInfo info)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                info.Frequency = frequency;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return Task.FromResult(true);
 | 
					            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)
 | 
					        catch (Exception ex)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            logger.Error(ex, "Failed to set frequency");
 | 
					            logger.Error(ex, "Failed to set frequency");
 | 
				
			||||||
            return Task.FromResult(false);
 | 
					            return false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public Task<DigitalTubeTaskStatus> GetStatus()
 | 
					    public async Task<DigitalTubeTaskStatus?> GetStatus()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        try
 | 
					        try
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
 | 
					            var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
 | 
				
			||||||
            if (_infoDict.GetOrAdd(
 | 
					            var key = (board.ID.ToString(), Context.ConnectionId);
 | 
				
			||||||
                board.ID.ToString(),
 | 
					 | 
				
			||||||
                (_) => new DigitalTubeInfo(
 | 
					 | 
				
			||||||
                    Context.ConnectionId,
 | 
					 | 
				
			||||||
                    new SevenDigitalTubesCtrl(board.IpAddr, board.Port, 2))
 | 
					 | 
				
			||||||
            ) is DigitalTubeInfo info)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                return Task.FromResult(new DigitalTubeTaskStatus(info));
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return Task.FromException<DigitalTubeTaskStatus>(new ArgumentException("Wrong argument"));
 | 
					            if (_scanTasks.TryGetValue(key, out var scanInfo))
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return new DigitalTubeTaskStatus(scanInfo);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            else
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return null;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        catch (Exception ex)
 | 
					        catch (Exception ex)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            logger.Error(ex, "Failed to get status");
 | 
					            logger.Error(ex, "Failed to get status");
 | 
				
			||||||
            return Task.FromException<DigitalTubeTaskStatus>(new Exception("Failed to get status"));
 | 
					            throw new Exception("Failed to get status", ex);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,17 +1,20 @@
 | 
				
			|||||||
using Microsoft.AspNetCore.Authorization;
 | 
					using Microsoft.AspNetCore.Authorization;
 | 
				
			||||||
using System.Security.Claims;
 | 
					 | 
				
			||||||
using Microsoft.AspNetCore.SignalR;
 | 
					using Microsoft.AspNetCore.SignalR;
 | 
				
			||||||
using Microsoft.AspNetCore.Cors;
 | 
					using Microsoft.AspNetCore.Cors;
 | 
				
			||||||
using TypedSignalR.Client;
 | 
					using TypedSignalR.Client;
 | 
				
			||||||
using Tapper;
 | 
					using Tapper;
 | 
				
			||||||
using server.Services;
 | 
					using server.Services;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#pragma warning disable 1998
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace server.Hubs;
 | 
					namespace server.Hubs;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[Hub]
 | 
					[Hub]
 | 
				
			||||||
public interface IProgressHub
 | 
					public interface IProgressHub
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    Task<bool> Join(string taskId);
 | 
					    Task<bool> Join(string taskId);
 | 
				
			||||||
 | 
					    Task<bool> Leave(string taskId);
 | 
				
			||||||
 | 
					    Task<ProgressInfo?> GetProgress(string taskId);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[Receiver]
 | 
					[Receiver]
 | 
				
			||||||
@@ -23,8 +26,7 @@ public interface IProgressReceiver
 | 
				
			|||||||
[TranspilationSource]
 | 
					[TranspilationSource]
 | 
				
			||||||
public enum ProgressStatus
 | 
					public enum ProgressStatus
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    Pending,
 | 
					    Running,
 | 
				
			||||||
    InProgress,
 | 
					 | 
				
			||||||
    Completed,
 | 
					    Completed,
 | 
				
			||||||
    Canceled,
 | 
					    Canceled,
 | 
				
			||||||
    Failed
 | 
					    Failed
 | 
				
			||||||
@@ -33,10 +35,10 @@ public enum ProgressStatus
 | 
				
			|||||||
[TranspilationSource]
 | 
					[TranspilationSource]
 | 
				
			||||||
public class ProgressInfo
 | 
					public class ProgressInfo
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public virtual string TaskId { get; } = string.Empty;
 | 
					    public required string TaskId { get; set; }
 | 
				
			||||||
    public virtual ProgressStatus Status { get; }
 | 
					    public required ProgressStatus Status { get; set; }
 | 
				
			||||||
    public virtual int ProgressPercent { get; } = 0;
 | 
					    public required double ProgressPercent { get; set; }
 | 
				
			||||||
    public virtual string ErrorMessage { get; } = string.Empty;
 | 
					    public required string ErrorMessage { get; set; }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[Authorize]
 | 
					[Authorize]
 | 
				
			||||||
@@ -44,18 +46,32 @@ public class ProgressInfo
 | 
				
			|||||||
public class ProgressHub : Hub<IProgressReceiver>, IProgressHub
 | 
					public class ProgressHub : Hub<IProgressReceiver>, IProgressHub
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
					    private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
				
			||||||
 | 
					    private readonly ProgressTracker _progressTracker = MsgBus.ProgressTracker;
 | 
				
			||||||
    private readonly IHubContext<ProgressHub, IProgressReceiver> _hubContext;
 | 
					 | 
				
			||||||
    private readonly ProgressTrackerService _tracker;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public ProgressHub(IHubContext<ProgressHub, IProgressReceiver> hubContext, ProgressTrackerService tracker)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        _hubContext = hubContext;
 | 
					 | 
				
			||||||
        _tracker = tracker;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public async Task<bool> Join(string taskId)
 | 
					    public async Task<bool> Join(string taskId)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return await Task.Run(() => _tracker.BindTask(taskId, Context.ConnectionId));
 | 
					        await Groups.AddToGroupAsync(Context.ConnectionId, taskId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 发送当前状态(如果存在)
 | 
				
			||||||
 | 
					        var task = _progressTracker.GetTask(taskId);
 | 
				
			||||||
 | 
					        if (task != null)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            await Clients.Caller.OnReceiveProgress(task.Value.ToProgressInfo());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        logger.Info($"Client {Context.ConnectionId} joined task {taskId}");
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public async Task<bool> Leave(string taskId)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        await Groups.RemoveFromGroupAsync(Context.ConnectionId, taskId);
 | 
				
			||||||
 | 
					        logger.Info($"Client {Context.ConnectionId} left task {taskId}");
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public async Task<ProgressInfo?> GetProgress(string taskId)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return _progressTracker.GetTask(taskId)?.ToProgressInfo();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,8 @@
 | 
				
			|||||||
 | 
					using server.Services;
 | 
				
			||||||
/// <summary>
 | 
					/// <summary>
 | 
				
			||||||
/// 多线程通信总线
 | 
					/// 多线程通信总线
 | 
				
			||||||
/// </summary>
 | 
					/// </summary>
 | 
				
			||||||
public static class MsgBus
 | 
					public sealed class MsgBus
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
					    private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -11,12 +12,39 @@ public static class MsgBus
 | 
				
			|||||||
    /// </summary>
 | 
					    /// </summary>
 | 
				
			||||||
    public static UDPServer UDPServer { get { return udpServer; } }
 | 
					    public static UDPServer UDPServer { get { return udpServer; } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 添加静态ProgressTracker引用
 | 
				
			||||||
 | 
					    private static ProgressTracker? _progressTracker;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// <summary>
 | 
				
			||||||
 | 
					    /// 设置全局ProgressTracker实例
 | 
				
			||||||
 | 
					    /// </summary>
 | 
				
			||||||
 | 
					    public static void SetProgressTracker(ProgressTracker progressTracker)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        _progressTracker = progressTracker;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static ProgressTracker ProgressTracker
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        get
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            if (_progressTracker == null)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                throw new InvalidOperationException("ProgressTracker is not set.");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return _progressTracker;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private static bool isRunning = false;
 | 
					    private static bool isRunning = false;
 | 
				
			||||||
    /// <summary>
 | 
					    /// <summary>
 | 
				
			||||||
    /// 获取通信总线运行状态
 | 
					    /// 获取通信总线运行状态
 | 
				
			||||||
    /// </summary>
 | 
					    /// </summary>
 | 
				
			||||||
    public static bool IsRunning { get { return isRunning; } }
 | 
					    public static bool IsRunning { get { return isRunning; } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private MsgBus() { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    static MsgBus() { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// <summary>
 | 
					    /// <summary>
 | 
				
			||||||
    /// 通信总线初始化
 | 
					    /// 通信总线初始化
 | 
				
			||||||
    /// </summary>
 | 
					    /// </summary>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -380,6 +380,7 @@ public class JtagStatusReg
 | 
				
			|||||||
public class Jtag
 | 
					public class Jtag
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
					    private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
				
			||||||
 | 
					    private readonly ProgressTracker _progressTracker = MsgBus.ProgressTracker;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private const int CLOCK_FREQ = 50; // MHz
 | 
					    private const int CLOCK_FREQ = 50; // MHz
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -392,6 +393,7 @@ public class Jtag
 | 
				
			|||||||
    public readonly string address;
 | 
					    public readonly string address;
 | 
				
			||||||
    private IPEndPoint ep;
 | 
					    private IPEndPoint ep;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// <summary>
 | 
					    /// <summary>
 | 
				
			||||||
    /// Jtag 构造函数
 | 
					    /// Jtag 构造函数
 | 
				
			||||||
    /// </summary>
 | 
					    /// </summary>
 | 
				
			||||||
@@ -444,10 +446,10 @@ public class Jtag
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    async ValueTask<Result<bool>> WriteFIFO(
 | 
					    async ValueTask<Result<bool>> WriteFIFO(
 | 
				
			||||||
        UInt32 devAddr, UInt32 data, UInt32 result,
 | 
					        UInt32 devAddr, UInt32 data, UInt32 result,
 | 
				
			||||||
        UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0, ProgressReporter? progress = null)
 | 
					        UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0, string progressId = "")
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var ret = await UDPClientPool.WriteAddr(this.ep, 0, devAddr, data, this.timeout, progress?.CreateChild(80));
 | 
					            var ret = await UDPClientPool.WriteAddr(this.ep, 0, devAddr, data, this.timeout, progressId);
 | 
				
			||||||
            if (!ret.IsSuccessful) return new(ret.Error);
 | 
					            if (!ret.IsSuccessful) return new(ret.Error);
 | 
				
			||||||
            if (!ret.Value) return new(new Exception("Write FIFO failed"));
 | 
					            if (!ret.Value) return new(new Exception("Write FIFO failed"));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -458,17 +460,18 @@ public class Jtag
 | 
				
			|||||||
        {
 | 
					        {
 | 
				
			||||||
            var ret = await UDPClientPool.ReadAddrWithWait(this.ep, 0, JtagAddr.STATE, result, resultMask, 0, this.timeout);
 | 
					            var ret = await UDPClientPool.ReadAddrWithWait(this.ep, 0, JtagAddr.STATE, result, resultMask, 0, this.timeout);
 | 
				
			||||||
            if (!ret.IsSuccessful) return new(ret.Error);
 | 
					            if (!ret.IsSuccessful) return new(ret.Error);
 | 
				
			||||||
            progress?.Finish();
 | 
					            _progressTracker?.AdvanceProgress(progressId, 10);
 | 
				
			||||||
            return ret.Value;
 | 
					            return ret.Value;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async ValueTask<Result<bool>> WriteFIFO(
 | 
					    async ValueTask<Result<bool>> WriteFIFO(
 | 
				
			||||||
        UInt32 devAddr, byte[] data, UInt32 result,
 | 
					        UInt32 devAddr, byte[] data, UInt32 result,
 | 
				
			||||||
        UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0, ProgressReporter? progress = null)
 | 
					        UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0, string progressId = "")
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var ret = await UDPClientPool.WriteAddr(this.ep, 0, devAddr, data, this.timeout, progress?.CreateChild(80));
 | 
					            var ret = await UDPClientPool.WriteAddr(
 | 
				
			||||||
 | 
					                this.ep, 0, devAddr, data, this.timeout, progressId);
 | 
				
			||||||
            if (!ret.IsSuccessful) return new(ret.Error);
 | 
					            if (!ret.IsSuccessful) return new(ret.Error);
 | 
				
			||||||
            if (!ret.Value) return new(new Exception("Write FIFO failed"));
 | 
					            if (!ret.Value) return new(new Exception("Write FIFO failed"));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -479,7 +482,7 @@ public class Jtag
 | 
				
			|||||||
        {
 | 
					        {
 | 
				
			||||||
            var ret = await UDPClientPool.ReadAddrWithWait(this.ep, 0, JtagAddr.STATE, result, resultMask, 0, this.timeout);
 | 
					            var ret = await UDPClientPool.ReadAddrWithWait(this.ep, 0, JtagAddr.STATE, result, resultMask, 0, this.timeout);
 | 
				
			||||||
            if (!ret.IsSuccessful) return new(ret.Error);
 | 
					            if (!ret.IsSuccessful) return new(ret.Error);
 | 
				
			||||||
            progress?.Finish();
 | 
					            _progressTracker.AdvanceProgress(progressId, 10);
 | 
				
			||||||
            return ret.Value;
 | 
					            return ret.Value;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -564,7 +567,7 @@ public class Jtag
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async ValueTask<Result<bool>> LoadDRCareInput(
 | 
					    async ValueTask<Result<bool>> LoadDRCareInput(
 | 
				
			||||||
        byte[] bytesArray, UInt32 timeout = 10_000, UInt32 cycle = 500, ProgressReporter? progress = null)
 | 
					        byte[] bytesArray, UInt32 timeout = 10_000, UInt32 cycle = 500, string progressId = "")
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        var bytesLen = ((uint)(bytesArray.Length * 8));
 | 
					        var bytesLen = ((uint)(bytesArray.Length * 8));
 | 
				
			||||||
        if (bytesLen > Math.Pow(2, 28)) return new(new Exception("Length is over 2^(28 - 3)"));
 | 
					        if (bytesLen > Math.Pow(2, 28)) return new(new Exception("Length is over 2^(28 - 3)"));
 | 
				
			||||||
@@ -579,14 +582,15 @@ public class Jtag
 | 
				
			|||||||
            else if (!ret.Value) return new(new Exception("Write CMD_JTAG_LOAD_DR_CAREI Failed"));
 | 
					            else if (!ret.Value) return new(new Exception("Write CMD_JTAG_LOAD_DR_CAREI Failed"));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        progress?.Report(10);
 | 
					        _progressTracker.AdvanceProgress(progressId, 10);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var ret = await WriteFIFO(
 | 
					            var ret = await WriteFIFO(
 | 
				
			||||||
                JtagAddr.WRITE_DATA,
 | 
					                JtagAddr.WRITE_DATA,
 | 
				
			||||||
                bytesArray, 0x01_00_00_00,
 | 
					                bytesArray, 0x01_00_00_00,
 | 
				
			||||||
                JtagState.CMD_EXEC_FINISH,
 | 
					                JtagState.CMD_EXEC_FINISH,
 | 
				
			||||||
                progress: progress?.CreateChild(90)
 | 
					                0,
 | 
				
			||||||
 | 
					                progressId
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (!ret.IsSuccessful) return new(ret.Error);
 | 
					            if (!ret.IsSuccessful) return new(ret.Error);
 | 
				
			||||||
@@ -709,58 +713,55 @@ public class Jtag
 | 
				
			|||||||
    /// 下载比特流到 JTAG 设备
 | 
					    /// 下载比特流到 JTAG 设备
 | 
				
			||||||
    /// </summary>
 | 
					    /// </summary>
 | 
				
			||||||
    /// <param name="bitstream">比特流数据</param>
 | 
					    /// <param name="bitstream">比特流数据</param>
 | 
				
			||||||
    /// <param name="progress">进度报告器</param>
 | 
					    /// <param name="progressId">进度ID</param>
 | 
				
			||||||
    /// <returns>指示下载是否成功的异步结果</returns>
 | 
					    /// <returns>指示下载是否成功的异步结果</returns>
 | 
				
			||||||
    public async ValueTask<Result<bool>> DownloadBitstream(
 | 
					    public async ValueTask<Result<bool>> DownloadBitstream(
 | 
				
			||||||
        byte[] bitstream, ProgressReporter? progress = null)
 | 
					        byte[] bitstream, string progressId = "")
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        // Clear Data
 | 
					        // Clear Data
 | 
				
			||||||
        MsgBus.UDPServer.ClearUDPData(this.address, 0);
 | 
					        MsgBus.UDPServer.ClearUDPData(this.address, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        logger.Trace($"Clear up udp server {this.address,0} receive data");
 | 
					        logger.Trace($"Clear up udp server {this.address,0} receive data");
 | 
				
			||||||
        if (progress != null)
 | 
					
 | 
				
			||||||
        {
 | 
					        _progressTracker.AdvanceProgress(progressId, 10);
 | 
				
			||||||
            progress.ExpectedSteps = 25;
 | 
					 | 
				
			||||||
            progress.Increase();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Result<bool> ret;
 | 
					        Result<bool> ret;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ret = await CloseTest();
 | 
					        ret = await CloseTest();
 | 
				
			||||||
        if (!ret.IsSuccessful) return new(ret.Error);
 | 
					        if (!ret.IsSuccessful) return new(ret.Error);
 | 
				
			||||||
        else if (!ret.Value) return new(new Exception("Jtag Close Test Failed"));
 | 
					        else if (!ret.Value) return new(new Exception("Jtag Close Test Failed"));
 | 
				
			||||||
        progress?.Increase();
 | 
					        _progressTracker.AdvanceProgress(progressId, 10);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ret = await RunTest();
 | 
					        ret = await RunTest();
 | 
				
			||||||
        if (!ret.IsSuccessful) return new(ret.Error);
 | 
					        if (!ret.IsSuccessful) return new(ret.Error);
 | 
				
			||||||
        else if (!ret.Value) return new(new Exception("Jtag Run Test Failed"));
 | 
					        else if (!ret.Value) return new(new Exception("Jtag Run Test Failed"));
 | 
				
			||||||
        progress?.Increase();
 | 
					        _progressTracker.AdvanceProgress(progressId, 10);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        logger.Trace("Jtag initialize");
 | 
					        logger.Trace("Jtag initialize");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ret = await ExecRDCmd(JtagCmd.JTAG_DR_JRST);
 | 
					        ret = await ExecRDCmd(JtagCmd.JTAG_DR_JRST);
 | 
				
			||||||
        if (!ret.IsSuccessful) return new(ret.Error);
 | 
					        if (!ret.IsSuccessful) return new(ret.Error);
 | 
				
			||||||
        else if (!ret.Value) return new(new Exception("Jtag Execute Command JTAG_DR_JRST Failed"));
 | 
					        else if (!ret.Value) return new(new Exception("Jtag Execute Command JTAG_DR_JRST Failed"));
 | 
				
			||||||
        progress?.Increase();
 | 
					        _progressTracker.AdvanceProgress(progressId, 10);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ret = await RunTest();
 | 
					        ret = await RunTest();
 | 
				
			||||||
        if (!ret.IsSuccessful) return new(ret.Error);
 | 
					        if (!ret.IsSuccessful) return new(ret.Error);
 | 
				
			||||||
        else if (!ret.Value) return new(new Exception("Jtag Run Test Failed"));
 | 
					        else if (!ret.Value) return new(new Exception("Jtag Run Test Failed"));
 | 
				
			||||||
        progress?.Increase();
 | 
					        _progressTracker.AdvanceProgress(progressId, 10);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ret = await ExecRDCmd(JtagCmd.JTAG_DR_CFGI);
 | 
					        ret = await ExecRDCmd(JtagCmd.JTAG_DR_CFGI);
 | 
				
			||||||
        if (!ret.IsSuccessful) return new(ret.Error);
 | 
					        if (!ret.IsSuccessful) return new(ret.Error);
 | 
				
			||||||
        else if (!ret.Value) return new(new Exception("Jtag Execute Command JTAG_DR_CFGI Failed"));
 | 
					        else if (!ret.Value) return new(new Exception("Jtag Execute Command JTAG_DR_CFGI Failed"));
 | 
				
			||||||
        progress?.Increase();
 | 
					        _progressTracker.AdvanceProgress(progressId, 10);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        logger.Trace("Jtag ready to write bitstream");
 | 
					        logger.Trace("Jtag ready to write bitstream");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ret = await IdleDelay(100000);
 | 
					        ret = await IdleDelay(100000);
 | 
				
			||||||
        if (!ret.IsSuccessful) return new(ret.Error);
 | 
					        if (!ret.IsSuccessful) return new(ret.Error);
 | 
				
			||||||
        else if (!ret.Value) return new(new Exception("Jtag IDLE Delay Failed"));
 | 
					        else if (!ret.Value) return new(new Exception("Jtag IDLE Delay Failed"));
 | 
				
			||||||
        progress?.Increase();
 | 
					        _progressTracker.AdvanceProgress(progressId, 10);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ret = await LoadDRCareInput(bitstream, progress: progress?.CreateChild(50));
 | 
					        ret = await LoadDRCareInput(bitstream, progressId: progressId);
 | 
				
			||||||
        if (!ret.IsSuccessful) return new(ret.Error);
 | 
					        if (!ret.IsSuccessful) return new(ret.Error);
 | 
				
			||||||
        else if (!ret.Value) return new(new Exception("Jtag Load Data Failed"));
 | 
					        else if (!ret.Value) return new(new Exception("Jtag Load Data Failed"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -769,40 +770,40 @@ public class Jtag
 | 
				
			|||||||
        ret = await CloseTest();
 | 
					        ret = await CloseTest();
 | 
				
			||||||
        if (!ret.IsSuccessful) return new(ret.Error);
 | 
					        if (!ret.IsSuccessful) return new(ret.Error);
 | 
				
			||||||
        else if (!ret.Value) return new(new Exception("Jtag Close Test Failed"));
 | 
					        else if (!ret.Value) return new(new Exception("Jtag Close Test Failed"));
 | 
				
			||||||
        progress?.Increase();
 | 
					        _progressTracker.AdvanceProgress(progressId, 10);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ret = await RunTest();
 | 
					        ret = await RunTest();
 | 
				
			||||||
        if (!ret.IsSuccessful) return new(ret.Error);
 | 
					        if (!ret.IsSuccessful) return new(ret.Error);
 | 
				
			||||||
        else if (!ret.Value) return new(new Exception("Jtag Run Test Failed"));
 | 
					        else if (!ret.Value) return new(new Exception("Jtag Run Test Failed"));
 | 
				
			||||||
        progress?.Increase();
 | 
					        _progressTracker.AdvanceProgress(progressId, 10);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ret = await ExecRDCmd(JtagCmd.JTAG_DR_JWAKEUP);
 | 
					        ret = await ExecRDCmd(JtagCmd.JTAG_DR_JWAKEUP);
 | 
				
			||||||
        if (!ret.IsSuccessful) return new(ret.Error);
 | 
					        if (!ret.IsSuccessful) return new(ret.Error);
 | 
				
			||||||
        else if (!ret.Value) return new(new Exception("Jtag Execute Command JTAG_DR_JWAKEUP Failed"));
 | 
					        else if (!ret.Value) return new(new Exception("Jtag Execute Command JTAG_DR_JWAKEUP Failed"));
 | 
				
			||||||
        progress?.Increase();
 | 
					        _progressTracker.AdvanceProgress(progressId, 10);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        logger.Trace("Jtag reset device");
 | 
					        logger.Trace("Jtag reset device");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ret = await IdleDelay(10000);
 | 
					        ret = await IdleDelay(10000);
 | 
				
			||||||
        if (!ret.IsSuccessful) return new(ret.Error);
 | 
					        if (!ret.IsSuccessful) return new(ret.Error);
 | 
				
			||||||
        else if (!ret.Value) return new(new Exception("Jtag IDLE Delay Failed"));
 | 
					        else if (!ret.Value) return new(new Exception("Jtag IDLE Delay Failed"));
 | 
				
			||||||
        progress?.Increase();
 | 
					        _progressTracker.AdvanceProgress(progressId, 10);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var retCode = await ReadStatusReg();
 | 
					        var retCode = await ReadStatusReg();
 | 
				
			||||||
        if (!retCode.IsSuccessful) return new(retCode.Error);
 | 
					        if (!retCode.IsSuccessful) return new(retCode.Error);
 | 
				
			||||||
        var jtagStatus = new JtagStatusReg(retCode.Value);
 | 
					        var jtagStatus = new JtagStatusReg(retCode.Value);
 | 
				
			||||||
        if (!(jtagStatus.done && jtagStatus.wakeup_over && jtagStatus.init_complete))
 | 
					        if (!(jtagStatus.done && jtagStatus.wakeup_over && jtagStatus.init_complete))
 | 
				
			||||||
            return new(new Exception("Jtag download bitstream failed"));
 | 
					            return new(new Exception("Jtag download bitstream failed"));
 | 
				
			||||||
        progress?.Increase();
 | 
					        _progressTracker.AdvanceProgress(progressId, 10);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ret = await CloseTest();
 | 
					        ret = await CloseTest();
 | 
				
			||||||
        if (!ret.IsSuccessful) return new(ret.Error);
 | 
					        if (!ret.IsSuccessful) return new(ret.Error);
 | 
				
			||||||
        else if (!ret.Value) return new(new Exception("Jtag Close Test Failed"));
 | 
					        else if (!ret.Value) return new(new Exception("Jtag Close Test Failed"));
 | 
				
			||||||
        logger.Trace("Jtag download bitstream successfully");
 | 
					        logger.Trace("Jtag download bitstream successfully");
 | 
				
			||||||
        progress?.Increase();
 | 
					        _progressTracker.AdvanceProgress(progressId, 10);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Finish
 | 
					        // Finish
 | 
				
			||||||
        progress?.Finish();
 | 
					        _progressTracker.AdvanceProgress(progressId, 10);
 | 
				
			||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,7 @@ namespace Peripherals.SevenDigitalTubesClient;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
static class SevenDigitalTubesAddr
 | 
					static class SevenDigitalTubesAddr
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public const UInt32 BASE = 0x0000_0000;
 | 
					    public const UInt32 BASE = 0xB000_0000;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public class SevenDigitalTubesCtrl
 | 
					public class SevenDigitalTubesCtrl
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										65
									
								
								server/src/Peripherals/SwitchClient.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								server/src/Peripherals/SwitchClient.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,65 @@
 | 
				
			|||||||
 | 
					using System.Collections;
 | 
				
			||||||
 | 
					using System.Net;
 | 
				
			||||||
 | 
					using DotNext;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Peripherals.SwitchClient;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SwitchCtrlAddr
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    public const UInt32 BASE = 0xB0_00_00_20;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public const UInt32 ENABLE = BASE;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// <summary>
 | 
				
			||||||
 | 
					/// 矩阵键盘外设类,用于控制和管理矩阵键盘的功能。
 | 
				
			||||||
 | 
					/// </summary>
 | 
				
			||||||
 | 
					public class SwitchCtrl
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    readonly int timeout = 500;
 | 
				
			||||||
 | 
					    readonly int taskID;
 | 
				
			||||||
 | 
					    readonly int port;
 | 
				
			||||||
 | 
					    readonly string address;
 | 
				
			||||||
 | 
					    private IPEndPoint ep;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public SwitchCtrl(string address, int port, int taskID, int timeout = 500)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if (timeout < 0)
 | 
				
			||||||
 | 
					            throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
 | 
				
			||||||
 | 
					        this.address = address;
 | 
				
			||||||
 | 
					        this.port = port;
 | 
				
			||||||
 | 
					        this.ep = new IPEndPoint(IPAddress.Parse(address), port);
 | 
				
			||||||
 | 
					        this.taskID = taskID;
 | 
				
			||||||
 | 
					        this.timeout = timeout;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public async ValueTask<Result<bool>> SetEnable(bool enable)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if (MsgBus.IsRunning)
 | 
				
			||||||
 | 
					            MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
 | 
				
			||||||
 | 
					        else return new(new Exception("Message Bus not work!"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var ret = await UDPClientPool.WriteAddr(
 | 
				
			||||||
 | 
					            this.ep, this.taskID, SwitchCtrlAddr.ENABLE, enable ? 0x1U : 0x0U, this.timeout);
 | 
				
			||||||
 | 
					        if (!ret.IsSuccessful) return new(ret.Error);
 | 
				
			||||||
 | 
					        return ret.Value;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public async ValueTask<Result<bool>> SetSwitchOnOff(int num, bool onOff)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if (MsgBus.IsRunning)
 | 
				
			||||||
 | 
					            MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
 | 
				
			||||||
 | 
					        else return new(new Exception("Message Bus not work!"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var ret = await UDPClientPool.WriteAddr(
 | 
				
			||||||
 | 
					            this.ep, this.taskID, SwitchCtrlAddr.BASE + (UInt32)num, onOff ? 0x1U : 0x0U, this.timeout);
 | 
				
			||||||
 | 
					        if (!ret.IsSuccessful)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            logger.Error($"Set Switch {onOff} failed: {ret.Error}");
 | 
				
			||||||
 | 
					            return new(ret.Error);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return ret.Value;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										147
									
								
								server/src/Services/ProgressTracker.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								server/src/Services/ProgressTracker.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,147 @@
 | 
				
			|||||||
 | 
					using System.Collections.Concurrent;
 | 
				
			||||||
 | 
					using Microsoft.AspNetCore.SignalR;
 | 
				
			||||||
 | 
					using server.Hubs;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace server.Services;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public enum TaskState { Running, Completed, Failed, Cancelled }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public readonly struct TaskProgress
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    public string Id { get; }
 | 
				
			||||||
 | 
					    public int Current { get; }
 | 
				
			||||||
 | 
					    public int Total { get; }
 | 
				
			||||||
 | 
					    public TaskState State { get; }
 | 
				
			||||||
 | 
					    public long Timestamp { get; }
 | 
				
			||||||
 | 
					    public string? Error { get; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public TaskProgress(string id, int current, int total, TaskState state, long timestamp, string? error = null)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Id = id;
 | 
				
			||||||
 | 
					        Current = current;
 | 
				
			||||||
 | 
					        Total = total;
 | 
				
			||||||
 | 
					        State = state;
 | 
				
			||||||
 | 
					        Timestamp = timestamp;
 | 
				
			||||||
 | 
					        Error = error;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public TaskProgress WithUpdate(int? current = null, TaskState? state = null, string? error = null)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return new TaskProgress(
 | 
				
			||||||
 | 
					            Id,
 | 
				
			||||||
 | 
					            current ?? Current,
 | 
				
			||||||
 | 
					            Total,
 | 
				
			||||||
 | 
					            state ?? State,
 | 
				
			||||||
 | 
					            DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
 | 
				
			||||||
 | 
					            error ?? Error
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public ProgressInfo ToProgressInfo()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return new ProgressInfo
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            TaskId = Id,
 | 
				
			||||||
 | 
					            Status = State switch
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                TaskState.Running => ProgressStatus.Running,
 | 
				
			||||||
 | 
					                TaskState.Completed => ProgressStatus.Completed,
 | 
				
			||||||
 | 
					                TaskState.Failed => ProgressStatus.Failed,
 | 
				
			||||||
 | 
					                TaskState.Cancelled => ProgressStatus.Canceled,
 | 
				
			||||||
 | 
					                _ => ProgressStatus.Failed
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            ProgressPercent = Total > 0 ? ((double)Current * 100) / (double)Total : 0,
 | 
				
			||||||
 | 
					            ErrorMessage = Error ?? string.Empty
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public sealed class ProgressTracker
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    private readonly ConcurrentDictionary<string, TaskProgress> _tasks = new();
 | 
				
			||||||
 | 
					    private readonly Timer _cleaner;
 | 
				
			||||||
 | 
					    private readonly IHubContext<ProgressHub, IProgressReceiver> _hubContext;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 构造器支持可选的Hub注入
 | 
				
			||||||
 | 
					    public ProgressTracker(IHubContext<ProgressHub, IProgressReceiver> hubContext)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        _hubContext = hubContext;
 | 
				
			||||||
 | 
					        _cleaner = new Timer(CleanExpiredTasks, null,
 | 
				
			||||||
 | 
					            TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public void CleanExpiredTasks(object? obj)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        var cutoff = DateTimeOffset.Now.AddMinutes(-3).ToUnixTimeSeconds();
 | 
				
			||||||
 | 
					        var expired = _tasks.Where(kvp => kvp.Value.Timestamp < cutoff).Select(kvp => kvp.Key).ToList();
 | 
				
			||||||
 | 
					        foreach (var id in expired)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            _tasks.TryRemove(id, out _);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public string CreateTask(int total)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        var id = Guid.NewGuid().ToString();
 | 
				
			||||||
 | 
					        var task = new TaskProgress(id, 0, total, TaskState.Running, DateTimeOffset.UtcNow.ToUnixTimeSeconds());
 | 
				
			||||||
 | 
					        _tasks[id] = task;
 | 
				
			||||||
 | 
					        NotifyIfNeeded(task);
 | 
				
			||||||
 | 
					        return id;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 核心更新方法,现在包含自动通知
 | 
				
			||||||
 | 
					    public bool UpdateTask(string id, Func<TaskProgress, TaskProgress> updater)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if (!_tasks.TryGetValue(id, out var current))
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var updated = updater(current);
 | 
				
			||||||
 | 
					        if (_tasks.TryUpdate(id, updated, current))
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            NotifyIfNeeded(updated);
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 自动通知逻辑 - 简单直接
 | 
				
			||||||
 | 
					    private void NotifyIfNeeded(TaskProgress task)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        _hubContext.Clients.Group(task.Id).OnReceiveProgress(task.ToProgressInfo());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public bool UpdateProgress(string id, int current)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return UpdateTask(id, p => p.WithUpdate(
 | 
				
			||||||
 | 
					            current: Math.Min(current, p.Total)));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public bool AdvanceProgress(string id, int steps)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return UpdateTask(id, p => p.WithUpdate(
 | 
				
			||||||
 | 
					            current: Math.Min(p.Current + steps, p.Total)));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public bool CancelProgress(string id)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return UpdateTask(id, p => p.WithUpdate(state: TaskState.Cancelled));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public bool CompleteProgress(string id)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return UpdateTask(id, p => p.WithUpdate(
 | 
				
			||||||
 | 
					            current: p.Total, state: TaskState.Completed));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public bool FailProgress(string id, string? error)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return UpdateTask(id, p => p.WithUpdate(
 | 
				
			||||||
 | 
					            state: TaskState.Failed, error: error));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public TaskProgress? GetTask(string id)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        _tasks.TryGetValue(id, out var task);
 | 
				
			||||||
 | 
					        return task.Id == null ? null : task;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,294 +0,0 @@
 | 
				
			|||||||
using Microsoft.AspNetCore.SignalR;
 | 
					 | 
				
			||||||
using System.Collections.Concurrent;
 | 
					 | 
				
			||||||
using DotNext;
 | 
					 | 
				
			||||||
using Common;
 | 
					 | 
				
			||||||
using server.Hubs;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace server.Services;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
public class ProgressReporter : ProgressInfo, IProgress<int>
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    private int _progress = 0;
 | 
					 | 
				
			||||||
    private int _stepProgress = 1;
 | 
					 | 
				
			||||||
    private int _expectedSteps = 100;
 | 
					 | 
				
			||||||
    private int _parentProportion = 100;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public int Progress => _progress;
 | 
					 | 
				
			||||||
    public int MaxProgress { get; set; } = 100;
 | 
					 | 
				
			||||||
    public int StepProgress
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        get => _stepProgress;
 | 
					 | 
				
			||||||
        set
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            _stepProgress = value;
 | 
					 | 
				
			||||||
            _expectedSteps = MaxProgress / value;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    public int ExpectedSteps
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        get => _expectedSteps;
 | 
					 | 
				
			||||||
        set
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            _expectedSteps = value;
 | 
					 | 
				
			||||||
            MaxProgress = Number.IntPow(10, Number.GetLength(value));
 | 
					 | 
				
			||||||
            _stepProgress = MaxProgress / value;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    public Func<int, Task>? ReporterFunc { get; set; } = null;
 | 
					 | 
				
			||||||
    public ProgressReporter? Parent { get; set; }
 | 
					 | 
				
			||||||
    public ProgressReporter? Child { get; set; }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private ProgressStatus _status = ProgressStatus.Pending;
 | 
					 | 
				
			||||||
    private string _errorMessage = string.Empty;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public override string TaskId { get; } = Guid.NewGuid().ToString();
 | 
					 | 
				
			||||||
    public override int ProgressPercent => _progress * 100 / MaxProgress;
 | 
					 | 
				
			||||||
    public override ProgressStatus Status => _status;
 | 
					 | 
				
			||||||
    public override string ErrorMessage => _errorMessage;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public ProgressReporter(Func<int, Task>? reporter = null, int initProgress = 0, int maxProgress = 100, int step = 1)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        _progress = initProgress;
 | 
					 | 
				
			||||||
        MaxProgress = maxProgress;
 | 
					 | 
				
			||||||
        StepProgress = step;
 | 
					 | 
				
			||||||
        ReporterFunc = reporter;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public ProgressReporter(int parentProportion, int expectedSteps = 100, Func<int, Task>? reporter = null)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        this._parentProportion = parentProportion;
 | 
					 | 
				
			||||||
        MaxProgress = Number.IntPow(10, Number.GetLength(expectedSteps));
 | 
					 | 
				
			||||||
        StepProgress = MaxProgress / expectedSteps;
 | 
					 | 
				
			||||||
        ReporterFunc = reporter;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private async void ForceReport(int value)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        try
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            if (ReporterFunc != null)
 | 
					 | 
				
			||||||
                await ReporterFunc(value);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (Parent != null)
 | 
					 | 
				
			||||||
                Parent.Increase((value - _progress) / StepProgress * _parentProportion / (MaxProgress / StepProgress));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            _progress = value;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        catch (OperationCanceledException ex)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            _errorMessage = ex.Message;
 | 
					 | 
				
			||||||
            this._status = ProgressStatus.Canceled;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        catch (Exception ex)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            _errorMessage = ex.Message;
 | 
					 | 
				
			||||||
            this._status = ProgressStatus.Failed;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public void Report(int value)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        if (this._status == ProgressStatus.Pending)
 | 
					 | 
				
			||||||
            this._status = ProgressStatus.InProgress;
 | 
					 | 
				
			||||||
        else if (this.Status != ProgressStatus.InProgress)
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (value > MaxProgress) return;
 | 
					 | 
				
			||||||
        ForceReport(value);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public void Increase(int? value = null)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        if (this._status == ProgressStatus.Pending)
 | 
					 | 
				
			||||||
            this._status = ProgressStatus.InProgress;
 | 
					 | 
				
			||||||
        else if (this.Status != ProgressStatus.InProgress)
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (value.HasValue)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            if (_progress + value.Value >= MaxProgress) return;
 | 
					 | 
				
			||||||
            this.Report(_progress + value.Value);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        else
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            if (_progress + StepProgress >= MaxProgress) return;
 | 
					 | 
				
			||||||
            this.Report(_progress + StepProgress);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public void Finish()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        this._status = ProgressStatus.Completed;
 | 
					 | 
				
			||||||
        this.ForceReport(MaxProgress);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public void Cancel()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        this._status = ProgressStatus.Canceled;
 | 
					 | 
				
			||||||
        this._errorMessage = "User Cancelled";
 | 
					 | 
				
			||||||
        this.ForceReport(_progress);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public void Error(string message)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        this._status = ProgressStatus.Failed;
 | 
					 | 
				
			||||||
        this._errorMessage = message;
 | 
					 | 
				
			||||||
        this.ForceReport(_progress);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public ProgressReporter CreateChild(int proportion, int expectedSteps = 100)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        var child = new ProgressReporter(proportion, expectedSteps);
 | 
					 | 
				
			||||||
        child.Parent = this;
 | 
					 | 
				
			||||||
        this.Child = child;
 | 
					 | 
				
			||||||
        return child;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
public class ProgressTrackerService : BackgroundService
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
					 | 
				
			||||||
    private readonly ConcurrentDictionary<string, TaskProgressInfo> _taskMap = new();
 | 
					 | 
				
			||||||
    private readonly IHubContext<ProgressHub, IProgressReceiver> _hubContext;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private class TaskProgressInfo
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        public ProgressReporter? Reporter { get; set; }
 | 
					 | 
				
			||||||
        public string? ConnectionId { get; set; }
 | 
					 | 
				
			||||||
        public required CancellationToken CancellationToken { get; set; }
 | 
					 | 
				
			||||||
        public required CancellationTokenSource CancellationTokenSource { get; set; }
 | 
					 | 
				
			||||||
        public required DateTime UpdatedAt { get; set; }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public ProgressTrackerService(IHubContext<ProgressHub, IProgressReceiver> hubContext)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        _hubContext = hubContext;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        while (!stoppingToken.IsCancellationRequested)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            try
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                var now = DateTime.UtcNow;
 | 
					 | 
				
			||||||
                foreach (var kvp in _taskMap)
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    var info = kvp.Value;
 | 
					 | 
				
			||||||
                    // 超过 1 分钟且任务已完成/失败/取消
 | 
					 | 
				
			||||||
                    if (
 | 
					 | 
				
			||||||
                        (now - info.UpdatedAt).TotalMinutes > 1 &&
 | 
					 | 
				
			||||||
                        info.Reporter != null &&
 | 
					 | 
				
			||||||
                        (
 | 
					 | 
				
			||||||
                            info.Reporter.Status == ProgressStatus.Completed ||
 | 
					 | 
				
			||||||
                            info.Reporter.Status == ProgressStatus.Failed ||
 | 
					 | 
				
			||||||
                            info.Reporter.Status == ProgressStatus.Canceled
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        _taskMap.TryRemove(kvp.Key, out _);
 | 
					 | 
				
			||||||
                        logger.Info($"Cleaned up task {kvp.Key}");
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            catch (Exception ex)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                logger.Error(ex, "Error during ProgressTracker cleanup");
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public (string, ProgressReporter) CreateTask(CancellationToken? cancellationToken = null)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        CancellationTokenSource? cancellationTokenSource;
 | 
					 | 
				
			||||||
        if (cancellationToken.HasValue)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken.Value);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        else
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            cancellationTokenSource = new CancellationTokenSource();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        var progressInfo = new TaskProgressInfo
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            ConnectionId = null,
 | 
					 | 
				
			||||||
            UpdatedAt = DateTime.UtcNow,
 | 
					 | 
				
			||||||
            CancellationToken = cancellationTokenSource.Token,
 | 
					 | 
				
			||||||
            CancellationTokenSource = cancellationTokenSource,
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        var progress = new ProgressReporter(async value =>
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            cancellationTokenSource.Token.ThrowIfCancellationRequested();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // 通过 SignalR 推送进度
 | 
					 | 
				
			||||||
            if (progressInfo.ConnectionId != null && progressInfo.Reporter != null)
 | 
					 | 
				
			||||||
                await _hubContext.Clients.Client(progressInfo.ConnectionId).OnReceiveProgress(progressInfo.Reporter);
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        progressInfo.Reporter = progress;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        _taskMap.TryAdd(progressInfo.Reporter.TaskId, progressInfo);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return (progressInfo.Reporter.TaskId, progress);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public Optional<ProgressReporter> GetReporter(string taskId)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        if (_taskMap.TryGetValue(taskId, out var info))
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            return info.Reporter;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return Optional<ProgressReporter>.None;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public Optional<ProgressStatus> GetProgressStatus(string taskId)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        if (_taskMap.TryGetValue(taskId, out var info))
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            if (info.Reporter != null)
 | 
					 | 
				
			||||||
                return info.Reporter.Status;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return Optional<ProgressStatus>.None;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public bool BindTask(string taskId, string connectionId)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        if (_taskMap.TryGetValue(taskId, out var info) && info != null)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            lock (info)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                info.ConnectionId = connectionId;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            return true;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public bool CancelTask(string taskId)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        try
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            if (_taskMap.TryGetValue(taskId, out var info) && info != null && info.Reporter != null)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                lock (info)
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    info.CancellationTokenSource.Cancel();
 | 
					 | 
				
			||||||
                    info.Reporter.Cancel();
 | 
					 | 
				
			||||||
                    info.UpdatedAt = DateTime.UtcNow;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                return true;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        catch (Exception ex)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            logger.Error(ex, $"Failed to cancel task {taskId}");
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -8,11 +8,11 @@ using server.Services;
 | 
				
			|||||||
/// <summary>
 | 
					/// <summary>
 | 
				
			||||||
/// UDP客户端发送池
 | 
					/// UDP客户端发送池
 | 
				
			||||||
/// </summary>
 | 
					/// </summary>
 | 
				
			||||||
public class UDPClientPool
 | 
					public sealed class UDPClientPool
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
					    private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private static IPAddress localhost = IPAddress.Parse("127.0.0.1");
 | 
					    private static ProgressTracker _progressTracker = MsgBus.ProgressTracker;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// <summary>
 | 
					    /// <summary>
 | 
				
			||||||
    /// 发送字符串
 | 
					    /// 发送字符串
 | 
				
			||||||
@@ -183,40 +183,6 @@ public class UDPClientPool
 | 
				
			|||||||
        return await Task.Run(() => { return SendDataPack(endPoint, pkg); });
 | 
					        return await Task.Run(() => { return SendDataPack(endPoint, pkg); });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// <summary>
 | 
					 | 
				
			||||||
    /// 发送字符串到本地
 | 
					 | 
				
			||||||
    /// </summary>
 | 
					 | 
				
			||||||
    /// <param name="port">端口</param>
 | 
					 | 
				
			||||||
    /// <param name="stringArray">字符串数组</param>
 | 
					 | 
				
			||||||
    /// <returns>是否成功</returns>
 | 
					 | 
				
			||||||
    public static bool SendStringLocalHost(int port, string[] stringArray)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return SendString(new IPEndPoint(localhost, port), stringArray);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// <summary>
 | 
					 | 
				
			||||||
    /// 循环发送字符串到本地
 | 
					 | 
				
			||||||
    /// </summary>
 | 
					 | 
				
			||||||
    /// <param name="times">发送总次数</param>
 | 
					 | 
				
			||||||
    /// <param name="sleepMilliSeconds">间隔时间</param>
 | 
					 | 
				
			||||||
    /// <param name="port">端口</param>
 | 
					 | 
				
			||||||
    /// <param name="stringArray">字符串数组</param>
 | 
					 | 
				
			||||||
    /// <returns>是否成功</returns>
 | 
					 | 
				
			||||||
    public static bool CycleSendStringLocalHost(int times, int sleepMilliSeconds, int port, string[] stringArray)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        var isSuccessful = true;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        while (times-- >= 0)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            isSuccessful = SendStringLocalHost(port, stringArray);
 | 
					 | 
				
			||||||
            if (!isSuccessful) break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Thread.Sleep(sleepMilliSeconds);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return isSuccessful;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// <summary>
 | 
					    /// <summary>
 | 
				
			||||||
    /// 读取设备地址数据
 | 
					    /// 读取设备地址数据
 | 
				
			||||||
    /// </summary>
 | 
					    /// </summary>
 | 
				
			||||||
@@ -607,11 +573,11 @@ public class UDPClientPool
 | 
				
			|||||||
    /// <param name="devAddr">设备地址</param>
 | 
					    /// <param name="devAddr">设备地址</param>
 | 
				
			||||||
    /// <param name="data">要写入的32位数据</param>
 | 
					    /// <param name="data">要写入的32位数据</param>
 | 
				
			||||||
    /// <param name="timeout">超时时间(毫秒)</param>
 | 
					    /// <param name="timeout">超时时间(毫秒)</param>
 | 
				
			||||||
    /// <param name="progress">进度报告器</param>
 | 
					    /// <param name="progressId">进度报告器</param>
 | 
				
			||||||
    /// <returns>写入结果,true表示写入成功</returns>
 | 
					    /// <returns>写入结果,true表示写入成功</returns>
 | 
				
			||||||
    public static async ValueTask<Result<bool>> WriteAddr(
 | 
					    public static async ValueTask<Result<bool>> WriteAddr(
 | 
				
			||||||
            IPEndPoint endPoint, int taskID, UInt32 devAddr,
 | 
					            IPEndPoint endPoint, int taskID, UInt32 devAddr,
 | 
				
			||||||
            UInt32 data, int timeout = 1000, ProgressReporter? progress = null)
 | 
					            UInt32 data, int timeout = 1000, string progressId = "")
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        var ret = false;
 | 
					        var ret = false;
 | 
				
			||||||
        var opts = new SendAddrPackOptions()
 | 
					        var opts = new SendAddrPackOptions()
 | 
				
			||||||
@@ -622,17 +588,18 @@ public class UDPClientPool
 | 
				
			|||||||
            Address = devAddr,
 | 
					            Address = devAddr,
 | 
				
			||||||
            IsWrite = true,
 | 
					            IsWrite = true,
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
        progress?.Report(20);
 | 
					        _progressTracker.AdvanceProgress(progressId, 10);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Write Register
 | 
					        // Write Register
 | 
				
			||||||
        ret = await UDPClientPool.SendAddrPackAsync(endPoint, new SendAddrPackage(opts));
 | 
					        ret = await UDPClientPool.SendAddrPackAsync(endPoint, new SendAddrPackage(opts));
 | 
				
			||||||
        if (!ret) return new(new Exception("Send 1st address package failed!"));
 | 
					        if (!ret) return new(new Exception("Send 1st address package failed!"));
 | 
				
			||||||
        progress?.Report(40);
 | 
					        _progressTracker.AdvanceProgress(progressId, 10);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Send Data Package
 | 
					        // Send Data Package
 | 
				
			||||||
        ret = await UDPClientPool.SendDataPackAsync(endPoint,
 | 
					        ret = await UDPClientPool.SendDataPackAsync(endPoint,
 | 
				
			||||||
                new SendDataPackage(Common.Number.NumberToBytes(data, 4).Value));
 | 
					                new SendDataPackage(Common.Number.NumberToBytes(data, 4).Value));
 | 
				
			||||||
        if (!ret) return new(new Exception("Send data package failed!"));
 | 
					        if (!ret) return new(new Exception("Send data package failed!"));
 | 
				
			||||||
        progress?.Report(60);
 | 
					        _progressTracker.AdvanceProgress(progressId, 10);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Check Msg Bus
 | 
					        // Check Msg Bus
 | 
				
			||||||
        if (!MsgBus.IsRunning)
 | 
					        if (!MsgBus.IsRunning)
 | 
				
			||||||
@@ -642,7 +609,7 @@ public class UDPClientPool
 | 
				
			|||||||
        var udpWriteAck = await MsgBus.UDPServer.WaitForAckAsync(
 | 
					        var udpWriteAck = await MsgBus.UDPServer.WaitForAckAsync(
 | 
				
			||||||
                endPoint.Address.ToString(), taskID, endPoint.Port, timeout);
 | 
					                endPoint.Address.ToString(), taskID, endPoint.Port, timeout);
 | 
				
			||||||
        if (!udpWriteAck.IsSuccessful) return new(udpWriteAck.Error);
 | 
					        if (!udpWriteAck.IsSuccessful) return new(udpWriteAck.Error);
 | 
				
			||||||
        progress?.Finish();
 | 
					        _progressTracker.AdvanceProgress(progressId, 10);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return udpWriteAck.Value.IsSuccessful;
 | 
					        return udpWriteAck.Value.IsSuccessful;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -655,11 +622,11 @@ public class UDPClientPool
 | 
				
			|||||||
    /// <param name="devAddr">设备地址</param>
 | 
					    /// <param name="devAddr">设备地址</param>
 | 
				
			||||||
    /// <param name="dataArray">要写入的字节数组</param>
 | 
					    /// <param name="dataArray">要写入的字节数组</param>
 | 
				
			||||||
    /// <param name="timeout">超时时间(毫秒)</param>
 | 
					    /// <param name="timeout">超时时间(毫秒)</param>
 | 
				
			||||||
    /// <param name="progress">进度报告器</param>
 | 
					    /// <param name="progressId">进度报告器</param>
 | 
				
			||||||
    /// <returns>写入结果,true表示写入成功</returns>
 | 
					    /// <returns>写入结果,true表示写入成功</returns>
 | 
				
			||||||
    public static async ValueTask<Result<bool>> WriteAddr(
 | 
					    public static async ValueTask<Result<bool>> WriteAddr(
 | 
				
			||||||
        IPEndPoint endPoint, int taskID, UInt32 devAddr,
 | 
					        IPEndPoint endPoint, int taskID, UInt32 devAddr,
 | 
				
			||||||
        byte[] dataArray, int timeout = 1000, ProgressReporter? progress = null)
 | 
					        byte[] dataArray, int timeout = 1000, string progressId = "")
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        var ret = false;
 | 
					        var ret = false;
 | 
				
			||||||
        var opts = new SendAddrPackOptions()
 | 
					        var opts = new SendAddrPackOptions()
 | 
				
			||||||
@@ -681,8 +648,6 @@ public class UDPClientPool
 | 
				
			|||||||
        var writeTimes = hasRest ?
 | 
					        var writeTimes = hasRest ?
 | 
				
			||||||
            dataArray.Length / (max4BytesPerRead * (32 / 8)) + 1 :
 | 
					            dataArray.Length / (max4BytesPerRead * (32 / 8)) + 1 :
 | 
				
			||||||
            dataArray.Length / (max4BytesPerRead * (32 / 8));
 | 
					            dataArray.Length / (max4BytesPerRead * (32 / 8));
 | 
				
			||||||
        if (progress != null)
 | 
					 | 
				
			||||||
            progress.ExpectedSteps = writeTimes;
 | 
					 | 
				
			||||||
        for (var i = 0; i < writeTimes; i++)
 | 
					        for (var i = 0; i < writeTimes; i++)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            // Sperate Data Array
 | 
					            // Sperate Data Array
 | 
				
			||||||
@@ -712,10 +677,9 @@ public class UDPClientPool
 | 
				
			|||||||
            if (!udpWriteAck.Value.IsSuccessful)
 | 
					            if (!udpWriteAck.Value.IsSuccessful)
 | 
				
			||||||
                return false;
 | 
					                return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            progress?.Increase();
 | 
					            _progressTracker.AdvanceProgress(progressId, 1);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        progress?.Finish();
 | 
					 | 
				
			||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										249
									
								
								src/APIClient.ts
									
									
									
									
									
								
							
							
						
						
									
										249
									
								
								src/APIClient.ts
									
									
									
									
									
								
							@@ -6936,6 +6936,255 @@ export class ResourceClient {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class SwitchClient {
 | 
				
			||||||
 | 
					    protected instance: AxiosInstance;
 | 
				
			||||||
 | 
					    protected baseUrl: string;
 | 
				
			||||||
 | 
					    protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructor(baseUrl?: string, instance?: AxiosInstance) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.instance = instance || axios.create();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.baseUrl = baseUrl ?? "http://127.0.0.1:5000";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 启用或禁用 Switch 外设
 | 
				
			||||||
 | 
					     * @param enable (optional) 是否启用
 | 
				
			||||||
 | 
					     * @return 操作结果
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    setEnable(enable: boolean | undefined, cancelToken?: CancelToken): Promise<boolean> {
 | 
				
			||||||
 | 
					        let url_ = this.baseUrl + "/api/Switch/enable?";
 | 
				
			||||||
 | 
					        if (enable === null)
 | 
				
			||||||
 | 
					            throw new Error("The parameter 'enable' cannot be null.");
 | 
				
			||||||
 | 
					        else if (enable !== undefined)
 | 
				
			||||||
 | 
					            url_ += "enable=" + encodeURIComponent("" + enable) + "&";
 | 
				
			||||||
 | 
					        url_ = url_.replace(/[?&]$/, "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let options_: AxiosRequestConfig = {
 | 
				
			||||||
 | 
					            method: "POST",
 | 
				
			||||||
 | 
					            url: url_,
 | 
				
			||||||
 | 
					            headers: {
 | 
				
			||||||
 | 
					                "Accept": "application/json"
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            cancelToken
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return this.instance.request(options_).catch((_error: any) => {
 | 
				
			||||||
 | 
					            if (isAxiosError(_error) && _error.response) {
 | 
				
			||||||
 | 
					                return _error.response;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                throw _error;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }).then((_response: AxiosResponse) => {
 | 
				
			||||||
 | 
					            return this.processSetEnable(_response);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected processSetEnable(response: AxiosResponse): Promise<boolean> {
 | 
				
			||||||
 | 
					        const status = response.status;
 | 
				
			||||||
 | 
					        let _headers: any = {};
 | 
				
			||||||
 | 
					        if (response.headers && typeof response.headers === "object") {
 | 
				
			||||||
 | 
					            for (const k in response.headers) {
 | 
				
			||||||
 | 
					                if (response.headers.hasOwnProperty(k)) {
 | 
				
			||||||
 | 
					                    _headers[k] = response.headers[k];
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (status === 200) {
 | 
				
			||||||
 | 
					            const _responseText = response.data;
 | 
				
			||||||
 | 
					            let result200: any = null;
 | 
				
			||||||
 | 
					            let resultData200  = _responseText;
 | 
				
			||||||
 | 
					                result200 = resultData200 !== undefined ? resultData200 : <any>null;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					            return Promise.resolve<boolean>(result200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        } else if (status === 500) {
 | 
				
			||||||
 | 
					            const _responseText = response.data;
 | 
				
			||||||
 | 
					            let result500: any = null;
 | 
				
			||||||
 | 
					            let resultData500  = _responseText;
 | 
				
			||||||
 | 
					            result500 = Exception.fromJS(resultData500);
 | 
				
			||||||
 | 
					            return throwException("A server side error occurred.", status, _responseText, _headers, result500);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        } else if (status === 401) {
 | 
				
			||||||
 | 
					            const _responseText = response.data;
 | 
				
			||||||
 | 
					            let result401: any = null;
 | 
				
			||||||
 | 
					            let resultData401  = _responseText;
 | 
				
			||||||
 | 
					            result401 = ProblemDetails.fromJS(resultData401);
 | 
				
			||||||
 | 
					            return throwException("A server side error occurred.", status, _responseText, _headers, result401);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        } else if (status !== 200 && status !== 204) {
 | 
				
			||||||
 | 
					            const _responseText = response.data;
 | 
				
			||||||
 | 
					            return throwException("An unexpected server error occurred.", status, _responseText, _headers);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return Promise.resolve<boolean>(null as any);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 控制指定编号的 Switch 开关
 | 
				
			||||||
 | 
					     * @param num (optional) 开关编号
 | 
				
			||||||
 | 
					     * @param onOff (optional) 开/关
 | 
				
			||||||
 | 
					     * @return 操作结果
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    setSwitchOnOff(num: number | undefined, onOff: boolean | undefined, cancelToken?: CancelToken): Promise<boolean> {
 | 
				
			||||||
 | 
					        let url_ = this.baseUrl + "/api/Switch/switch?";
 | 
				
			||||||
 | 
					        if (num === null)
 | 
				
			||||||
 | 
					            throw new Error("The parameter 'num' cannot be null.");
 | 
				
			||||||
 | 
					        else if (num !== undefined)
 | 
				
			||||||
 | 
					            url_ += "num=" + encodeURIComponent("" + num) + "&";
 | 
				
			||||||
 | 
					        if (onOff === null)
 | 
				
			||||||
 | 
					            throw new Error("The parameter 'onOff' cannot be null.");
 | 
				
			||||||
 | 
					        else if (onOff !== undefined)
 | 
				
			||||||
 | 
					            url_ += "onOff=" + encodeURIComponent("" + onOff) + "&";
 | 
				
			||||||
 | 
					        url_ = url_.replace(/[?&]$/, "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let options_: AxiosRequestConfig = {
 | 
				
			||||||
 | 
					            method: "POST",
 | 
				
			||||||
 | 
					            url: url_,
 | 
				
			||||||
 | 
					            headers: {
 | 
				
			||||||
 | 
					                "Accept": "application/json"
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            cancelToken
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return this.instance.request(options_).catch((_error: any) => {
 | 
				
			||||||
 | 
					            if (isAxiosError(_error) && _error.response) {
 | 
				
			||||||
 | 
					                return _error.response;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                throw _error;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }).then((_response: AxiosResponse) => {
 | 
				
			||||||
 | 
					            return this.processSetSwitchOnOff(_response);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected processSetSwitchOnOff(response: AxiosResponse): Promise<boolean> {
 | 
				
			||||||
 | 
					        const status = response.status;
 | 
				
			||||||
 | 
					        let _headers: any = {};
 | 
				
			||||||
 | 
					        if (response.headers && typeof response.headers === "object") {
 | 
				
			||||||
 | 
					            for (const k in response.headers) {
 | 
				
			||||||
 | 
					                if (response.headers.hasOwnProperty(k)) {
 | 
				
			||||||
 | 
					                    _headers[k] = response.headers[k];
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (status === 200) {
 | 
				
			||||||
 | 
					            const _responseText = response.data;
 | 
				
			||||||
 | 
					            let result200: any = null;
 | 
				
			||||||
 | 
					            let resultData200  = _responseText;
 | 
				
			||||||
 | 
					                result200 = resultData200 !== undefined ? resultData200 : <any>null;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					            return Promise.resolve<boolean>(result200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        } else if (status === 400) {
 | 
				
			||||||
 | 
					            const _responseText = response.data;
 | 
				
			||||||
 | 
					            let result400: any = null;
 | 
				
			||||||
 | 
					            let resultData400  = _responseText;
 | 
				
			||||||
 | 
					            result400 = ArgumentException.fromJS(resultData400);
 | 
				
			||||||
 | 
					            return throwException("A server side error occurred.", status, _responseText, _headers, result400);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        } else if (status === 500) {
 | 
				
			||||||
 | 
					            const _responseText = response.data;
 | 
				
			||||||
 | 
					            let result500: any = null;
 | 
				
			||||||
 | 
					            let resultData500  = _responseText;
 | 
				
			||||||
 | 
					            result500 = Exception.fromJS(resultData500);
 | 
				
			||||||
 | 
					            return throwException("A server side error occurred.", status, _responseText, _headers, result500);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        } else if (status === 401) {
 | 
				
			||||||
 | 
					            const _responseText = response.data;
 | 
				
			||||||
 | 
					            let result401: any = null;
 | 
				
			||||||
 | 
					            let resultData401  = _responseText;
 | 
				
			||||||
 | 
					            result401 = ProblemDetails.fromJS(resultData401);
 | 
				
			||||||
 | 
					            return throwException("A server side error occurred.", status, _responseText, _headers, result401);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        } else if (status !== 200 && status !== 204) {
 | 
				
			||||||
 | 
					            const _responseText = response.data;
 | 
				
			||||||
 | 
					            return throwException("An unexpected server error occurred.", status, _responseText, _headers);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return Promise.resolve<boolean>(null as any);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 控制 Switch 开关
 | 
				
			||||||
 | 
					     * @param keyStatus 开关状态
 | 
				
			||||||
 | 
					     * @return 操作结果
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    setMultiSwitchsOnOff(keyStatus: boolean[], cancelToken?: CancelToken): Promise<boolean> {
 | 
				
			||||||
 | 
					        let url_ = this.baseUrl + "/api/Switch/MultiSwitch";
 | 
				
			||||||
 | 
					        url_ = url_.replace(/[?&]$/, "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const content_ = JSON.stringify(keyStatus);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let options_: AxiosRequestConfig = {
 | 
				
			||||||
 | 
					            data: content_,
 | 
				
			||||||
 | 
					            method: "POST",
 | 
				
			||||||
 | 
					            url: url_,
 | 
				
			||||||
 | 
					            headers: {
 | 
				
			||||||
 | 
					                "Content-Type": "application/json",
 | 
				
			||||||
 | 
					                "Accept": "application/json"
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            cancelToken
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return this.instance.request(options_).catch((_error: any) => {
 | 
				
			||||||
 | 
					            if (isAxiosError(_error) && _error.response) {
 | 
				
			||||||
 | 
					                return _error.response;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                throw _error;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }).then((_response: AxiosResponse) => {
 | 
				
			||||||
 | 
					            return this.processSetMultiSwitchsOnOff(_response);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected processSetMultiSwitchsOnOff(response: AxiosResponse): Promise<boolean> {
 | 
				
			||||||
 | 
					        const status = response.status;
 | 
				
			||||||
 | 
					        let _headers: any = {};
 | 
				
			||||||
 | 
					        if (response.headers && typeof response.headers === "object") {
 | 
				
			||||||
 | 
					            for (const k in response.headers) {
 | 
				
			||||||
 | 
					                if (response.headers.hasOwnProperty(k)) {
 | 
				
			||||||
 | 
					                    _headers[k] = response.headers[k];
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (status === 200) {
 | 
				
			||||||
 | 
					            const _responseText = response.data;
 | 
				
			||||||
 | 
					            let result200: any = null;
 | 
				
			||||||
 | 
					            let resultData200  = _responseText;
 | 
				
			||||||
 | 
					                result200 = resultData200 !== undefined ? resultData200 : <any>null;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					            return Promise.resolve<boolean>(result200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        } else if (status === 400) {
 | 
				
			||||||
 | 
					            const _responseText = response.data;
 | 
				
			||||||
 | 
					            let result400: any = null;
 | 
				
			||||||
 | 
					            let resultData400  = _responseText;
 | 
				
			||||||
 | 
					            result400 = ArgumentException.fromJS(resultData400);
 | 
				
			||||||
 | 
					            return throwException("A server side error occurred.", status, _responseText, _headers, result400);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        } else if (status === 500) {
 | 
				
			||||||
 | 
					            const _responseText = response.data;
 | 
				
			||||||
 | 
					            let result500: any = null;
 | 
				
			||||||
 | 
					            let resultData500  = _responseText;
 | 
				
			||||||
 | 
					            result500 = Exception.fromJS(resultData500);
 | 
				
			||||||
 | 
					            return throwException("A server side error occurred.", status, _responseText, _headers, result500);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        } else if (status === 401) {
 | 
				
			||||||
 | 
					            const _responseText = response.data;
 | 
				
			||||||
 | 
					            let result401: any = null;
 | 
				
			||||||
 | 
					            let resultData401  = _responseText;
 | 
				
			||||||
 | 
					            result401 = ProblemDetails.fromJS(resultData401);
 | 
				
			||||||
 | 
					            return throwException("A server side error occurred.", status, _responseText, _headers, result401);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        } else if (status !== 200 && status !== 204) {
 | 
				
			||||||
 | 
					            const _responseText = response.data;
 | 
				
			||||||
 | 
					            return throwException("An unexpected server error occurred.", status, _responseText, _headers);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return Promise.resolve<boolean>(null as any);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class TutorialClient {
 | 
					export class TutorialClient {
 | 
				
			||||||
    protected instance: AxiosInstance;
 | 
					    protected instance: AxiosInstance;
 | 
				
			||||||
    protected baseUrl: string;
 | 
					    protected baseUrl: string;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,6 @@
 | 
				
			|||||||
 | 
					import { ResourceClient, ResourcePurpose } from "@/APIClient";
 | 
				
			||||||
 | 
					import { AuthManager } from "@/utils/AuthManager";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 定义 diagram.json 的类型结构
 | 
					// 定义 diagram.json 的类型结构
 | 
				
			||||||
export interface DiagramData {
 | 
					export interface DiagramData {
 | 
				
			||||||
  version: number;
 | 
					  version: number;
 | 
				
			||||||
@@ -26,40 +29,43 @@ export interface DiagramPart {
 | 
				
			|||||||
// 连接类型定义 - 使用元组类型表示四元素数组
 | 
					// 连接类型定义 - 使用元组类型表示四元素数组
 | 
				
			||||||
export type ConnectionArray = [string, string, number, string[]];
 | 
					export type ConnectionArray = [string, string, number, string[]];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { AuthManager } from '@/utils/AuthManager';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 解析连接字符串为组件ID和引脚ID
 | 
					// 解析连接字符串为组件ID和引脚ID
 | 
				
			||||||
export function parseConnectionPin(connectionPin: string): { componentId: string; pinId: string } {
 | 
					export function parseConnectionPin(connectionPin: string): {
 | 
				
			||||||
  const [componentId, pinId] = connectionPin.split(':');
 | 
					  componentId: string;
 | 
				
			||||||
 | 
					  pinId: string;
 | 
				
			||||||
 | 
					} {
 | 
				
			||||||
 | 
					  const [componentId, pinId] = connectionPin.split(":");
 | 
				
			||||||
  return { componentId, pinId };
 | 
					  return { componentId, pinId };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 将连接数组转换为适用于渲染的格式
 | 
					// 将连接数组转换为适用于渲染的格式
 | 
				
			||||||
export function connectionArrayToWireItem(
 | 
					export function connectionArrayToWireItem(
 | 
				
			||||||
  connection: ConnectionArray, 
 | 
					  connection: ConnectionArray,
 | 
				
			||||||
  index: number, 
 | 
					  index: number,
 | 
				
			||||||
  startPos = { x: 0, y: 0 }, 
 | 
					  startPos = { x: 0, y: 0 },
 | 
				
			||||||
  endPos = { x: 0, y: 0 }
 | 
					  endPos = { x: 0, y: 0 },
 | 
				
			||||||
): WireItem {
 | 
					): WireItem {
 | 
				
			||||||
  const [startPinStr, endPinStr, width, path] = connection;
 | 
					  const [startPinStr, endPinStr, width, path] = connection;
 | 
				
			||||||
  const { componentId: startComponentId, pinId: startPinId } = parseConnectionPin(startPinStr);
 | 
					  const { componentId: startComponentId, pinId: startPinId } =
 | 
				
			||||||
  const { componentId: endComponentId, pinId: endPinId } = parseConnectionPin(endPinStr);
 | 
					    parseConnectionPin(startPinStr);
 | 
				
			||||||
  
 | 
					  const { componentId: endComponentId, pinId: endPinId } =
 | 
				
			||||||
 | 
					    parseConnectionPin(endPinStr);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    id: `wire-${index}`,
 | 
					    id: `wire-${index}`,
 | 
				
			||||||
    startX: startPos.x,
 | 
					    startX: startPos.x,
 | 
				
			||||||
    startY: startPos.y,
 | 
					    startY: startPos.y,
 | 
				
			||||||
    endX: endPos.x, 
 | 
					    endX: endPos.x,
 | 
				
			||||||
    endY: endPos.y,
 | 
					    endY: endPos.y,
 | 
				
			||||||
    startComponentId,
 | 
					    startComponentId,
 | 
				
			||||||
    startPinId,
 | 
					    startPinId,
 | 
				
			||||||
    endComponentId,
 | 
					    endComponentId,
 | 
				
			||||||
    endPinId,
 | 
					    endPinId,
 | 
				
			||||||
    strokeWidth: width,
 | 
					    strokeWidth: width,
 | 
				
			||||||
    color: '#4a5568', // 默认颜色
 | 
					    color: "#4a5568", // 默认颜色
 | 
				
			||||||
    routingMode: 'path',
 | 
					    routingMode: "path",
 | 
				
			||||||
    pathCommands: path,
 | 
					    pathCommands: path,
 | 
				
			||||||
    showLabel: false
 | 
					    showLabel: false,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -76,7 +82,7 @@ export interface WireItem {
 | 
				
			|||||||
  endPinId?: string;
 | 
					  endPinId?: string;
 | 
				
			||||||
  strokeWidth: number;
 | 
					  strokeWidth: number;
 | 
				
			||||||
  color: string;
 | 
					  color: string;
 | 
				
			||||||
  routingMode: 'orthogonal' | 'path';
 | 
					  routingMode: "orthogonal" | "path";
 | 
				
			||||||
  constraint?: string;
 | 
					  constraint?: string;
 | 
				
			||||||
  pathCommands?: string[];
 | 
					  pathCommands?: string[];
 | 
				
			||||||
  showLabel: boolean;
 | 
					  showLabel: boolean;
 | 
				
			||||||
@@ -88,58 +94,64 @@ export async function loadDiagramData(examId?: string): Promise<DiagramData> {
 | 
				
			|||||||
    // 如果提供了examId,优先从API加载实验的diagram
 | 
					    // 如果提供了examId,优先从API加载实验的diagram
 | 
				
			||||||
    if (examId) {
 | 
					    if (examId) {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        const resourceClient = AuthManager.createAuthenticatedResourceClient();
 | 
					        const resourceClient = AuthManager.createClient(ResourceClient);
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        // 获取diagram类型的资源列表
 | 
					        // 获取diagram类型的资源列表
 | 
				
			||||||
        const resources = await resourceClient.getResourceList(examId, 'canvas', 'template');
 | 
					        const resources = await resourceClient.getResourceList(
 | 
				
			||||||
        
 | 
					          examId,
 | 
				
			||||||
 | 
					          "canvas",
 | 
				
			||||||
 | 
					          ResourcePurpose.Template,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (resources && resources.length > 0) {
 | 
					        if (resources && resources.length > 0) {
 | 
				
			||||||
          // 获取第一个diagram资源
 | 
					          // 获取第一个diagram资源
 | 
				
			||||||
          const diagramResource = resources[0];
 | 
					          const diagramResource = resources[0];
 | 
				
			||||||
          
 | 
					
 | 
				
			||||||
          // 使用动态API获取资源文件内容
 | 
					          // 使用动态API获取资源文件内容
 | 
				
			||||||
          const response = await resourceClient.getResourceById(diagramResource.id);
 | 
					          const response = await resourceClient.getResourceById(
 | 
				
			||||||
          
 | 
					            diagramResource.id,
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          if (response && response.data) {
 | 
					          if (response && response.data) {
 | 
				
			||||||
            const text = await response.data.text();
 | 
					            const text = await response.data.text();
 | 
				
			||||||
            const data = JSON.parse(text);
 | 
					            const data = JSON.parse(text);
 | 
				
			||||||
            
 | 
					
 | 
				
			||||||
            // 验证数据格式
 | 
					            // 验证数据格式
 | 
				
			||||||
            const validation = validateDiagramData(data);
 | 
					            const validation = validateDiagramData(data);
 | 
				
			||||||
            if (validation.isValid) {
 | 
					            if (validation.isValid) {
 | 
				
			||||||
              console.log('成功从API加载实验diagram:', examId);
 | 
					              console.log("成功从API加载实验diagram:", examId);
 | 
				
			||||||
              return data;
 | 
					              return data;
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
              console.warn('API返回的diagram数据格式无效:', validation.errors);
 | 
					              console.warn("API返回的diagram数据格式无效:", validation.errors);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          console.log('未找到实验diagram资源,使用默认加载方式');
 | 
					          console.log("未找到实验diagram资源,使用默认加载方式");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      } catch (error) {
 | 
					      } catch (error) {
 | 
				
			||||||
        console.warn('从API加载实验diagram失败,使用默认加载方式:', error);
 | 
					        console.warn("从API加载实验diagram失败,使用默认加载方式:", error);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    // 如果没有examId或API加载失败,尝试从静态文件加载(不再使用本地存储)
 | 
					    // 如果没有examId或API加载失败,尝试从静态文件加载(不再使用本地存储)
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    // 从静态文件加载(作为备选方案)
 | 
					    // 从静态文件加载(作为备选方案)
 | 
				
			||||||
    const response = await fetch('/src/components/diagram.json');
 | 
					    const response = await fetch("/src/components/diagram.json");
 | 
				
			||||||
    if (!response.ok) {
 | 
					    if (!response.ok) {
 | 
				
			||||||
      throw new Error(`Failed to load diagram.json: ${response.statusText}`);
 | 
					      throw new Error(`Failed to load diagram.json: ${response.statusText}`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const data = await response.json();
 | 
					    const data = await response.json();
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    // 验证静态文件数据
 | 
					    // 验证静态文件数据
 | 
				
			||||||
    const validation = validateDiagramData(data);
 | 
					    const validation = validateDiagramData(data);
 | 
				
			||||||
    if (validation.isValid) {
 | 
					    if (validation.isValid) {
 | 
				
			||||||
      return data;
 | 
					      return data;
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      console.warn('静态diagram文件数据格式无效:', validation.errors);
 | 
					      console.warn("静态diagram文件数据格式无效:", validation.errors);
 | 
				
			||||||
      throw new Error('所有diagram数据源都无效');
 | 
					      throw new Error("所有diagram数据源都无效");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  } catch (error) {
 | 
					  } catch (error) {
 | 
				
			||||||
    console.error('Error loading diagram data:', error);
 | 
					    console.error("Error loading diagram data:", error);
 | 
				
			||||||
    // 返回空的默认数据结构
 | 
					    // 返回空的默认数据结构
 | 
				
			||||||
    return createEmptyDiagram();
 | 
					    return createEmptyDiagram();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -149,33 +161,31 @@ export async function loadDiagramData(examId?: string): Promise<DiagramData> {
 | 
				
			|||||||
export function createEmptyDiagram(): DiagramData {
 | 
					export function createEmptyDiagram(): DiagramData {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    version: 1,
 | 
					    version: 1,
 | 
				
			||||||
    author: 'user',
 | 
					    author: "user",
 | 
				
			||||||
    editor: 'user',
 | 
					    editor: "user",
 | 
				
			||||||
    parts: [],
 | 
					    parts: [],
 | 
				
			||||||
    connections: []
 | 
					    connections: [],
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 保存图表数据(已禁用本地存储)
 | 
					// 保存图表数据(已禁用本地存储)
 | 
				
			||||||
export function saveDiagramData(data: DiagramData): void {
 | 
					export function saveDiagramData(data: DiagramData): void {
 | 
				
			||||||
  // 本地存储功能已禁用 - 不再保存到localStorage
 | 
					  // 本地存储功能已禁用 - 不再保存到localStorage
 | 
				
			||||||
  console.debug('saveDiagramData called but localStorage saving is disabled');
 | 
					  console.debug("saveDiagramData called but localStorage saving is disabled");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 更新组件位置
 | 
					// 更新组件位置
 | 
				
			||||||
export function updatePartPosition(
 | 
					export function updatePartPosition(
 | 
				
			||||||
  data: DiagramData, 
 | 
					  data: DiagramData,
 | 
				
			||||||
  partId: string, 
 | 
					  partId: string,
 | 
				
			||||||
  x: number, 
 | 
					  x: number,
 | 
				
			||||||
  y: number
 | 
					  y: number,
 | 
				
			||||||
): DiagramData {
 | 
					): DiagramData {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    ...data,
 | 
					    ...data,
 | 
				
			||||||
    parts: data.parts.map(part => 
 | 
					    parts: data.parts.map((part) =>
 | 
				
			||||||
      part.id === partId 
 | 
					      part.id === partId ? { ...part, x, y } : part,
 | 
				
			||||||
        ? { ...part, x, y } 
 | 
					    ),
 | 
				
			||||||
        : part
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -184,21 +194,21 @@ export function updatePartAttribute(
 | 
				
			|||||||
  data: DiagramData,
 | 
					  data: DiagramData,
 | 
				
			||||||
  partId: string,
 | 
					  partId: string,
 | 
				
			||||||
  attrName: string,
 | 
					  attrName: string,
 | 
				
			||||||
  value: any
 | 
					  value: any,
 | 
				
			||||||
): DiagramData {
 | 
					): DiagramData {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    ...data,
 | 
					    ...data,
 | 
				
			||||||
    parts: data.parts.map(part => 
 | 
					    parts: data.parts.map((part) =>
 | 
				
			||||||
      part.id === partId 
 | 
					      part.id === partId
 | 
				
			||||||
        ? { 
 | 
					        ? {
 | 
				
			||||||
            ...part, 
 | 
					            ...part,
 | 
				
			||||||
            attrs: { 
 | 
					            attrs: {
 | 
				
			||||||
              ...part.attrs, 
 | 
					              ...part.attrs,
 | 
				
			||||||
              [attrName]: value 
 | 
					              [attrName]: value,
 | 
				
			||||||
            } 
 | 
					            },
 | 
				
			||||||
          } 
 | 
					          }
 | 
				
			||||||
        : part
 | 
					        : part,
 | 
				
			||||||
    )
 | 
					    ),
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -210,72 +220,79 @@ export function addConnection(
 | 
				
			|||||||
  endComponentId: string,
 | 
					  endComponentId: string,
 | 
				
			||||||
  endPinId: string,
 | 
					  endPinId: string,
 | 
				
			||||||
  width: number = 2,
 | 
					  width: number = 2,
 | 
				
			||||||
  path: string[] = []
 | 
					  path: string[] = [],
 | 
				
			||||||
): DiagramData {
 | 
					): DiagramData {
 | 
				
			||||||
  const newConnection: ConnectionArray = [
 | 
					  const newConnection: ConnectionArray = [
 | 
				
			||||||
    `${startComponentId}:${startPinId}`,
 | 
					    `${startComponentId}:${startPinId}`,
 | 
				
			||||||
    `${endComponentId}:${endPinId}`,
 | 
					    `${endComponentId}:${endPinId}`,
 | 
				
			||||||
    width,
 | 
					    width,
 | 
				
			||||||
    path
 | 
					    path,
 | 
				
			||||||
  ];
 | 
					  ];
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    ...data,
 | 
					    ...data,
 | 
				
			||||||
    connections: [...data.connections, newConnection]
 | 
					    connections: [...data.connections, newConnection],
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 删除连接
 | 
					// 删除连接
 | 
				
			||||||
export function deleteConnection(
 | 
					export function deleteConnection(
 | 
				
			||||||
  data: DiagramData,
 | 
					  data: DiagramData,
 | 
				
			||||||
  connectionIndex: number
 | 
					  connectionIndex: number,
 | 
				
			||||||
): DiagramData {
 | 
					): DiagramData {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    ...data,
 | 
					    ...data,
 | 
				
			||||||
    connections: data.connections.filter((_, index) => index !== connectionIndex)
 | 
					    connections: data.connections.filter(
 | 
				
			||||||
 | 
					      (_, index) => index !== connectionIndex,
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 查找与组件关联的所有连接
 | 
					// 查找与组件关联的所有连接
 | 
				
			||||||
export function findConnectionsByPart(
 | 
					export function findConnectionsByPart(
 | 
				
			||||||
  data: DiagramData,
 | 
					  data: DiagramData,
 | 
				
			||||||
  partId: string
 | 
					  partId: string,
 | 
				
			||||||
): { connection: ConnectionArray; index: number }[] {
 | 
					): { connection: ConnectionArray; index: number }[] {
 | 
				
			||||||
  return data.connections
 | 
					  return data.connections
 | 
				
			||||||
    .map((connection, index) => ({ connection, index }))
 | 
					    .map((connection, index) => ({ connection, index }))
 | 
				
			||||||
    .filter(({ connection }) => {
 | 
					    .filter(({ connection }) => {
 | 
				
			||||||
      const [startPin, endPin] = connection;
 | 
					      const [startPin, endPin] = connection;
 | 
				
			||||||
      const startCompId = startPin.split(':')[0];
 | 
					      const startCompId = startPin.split(":")[0];
 | 
				
			||||||
      const endCompId = endPin.split(':')[0];
 | 
					      const endCompId = endPin.split(":")[0];
 | 
				
			||||||
      return startCompId === partId || endCompId === partId;
 | 
					      return startCompId === partId || endCompId === partId;
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 添加验证diagram.json文件的函数
 | 
					// 添加验证diagram.json文件的函数
 | 
				
			||||||
export function validateDiagramData(data: any): { isValid: boolean; errors: string[] } {
 | 
					export function validateDiagramData(data: any): {
 | 
				
			||||||
 | 
					  isValid: boolean;
 | 
				
			||||||
 | 
					  errors: string[];
 | 
				
			||||||
 | 
					} {
 | 
				
			||||||
  const errors: string[] = [];
 | 
					  const errors: string[] = [];
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  // 检查版本号
 | 
					  // 检查版本号
 | 
				
			||||||
  if (!data.version) {
 | 
					  if (!data.version) {
 | 
				
			||||||
    errors.push('缺少version字段');
 | 
					    errors.push("缺少version字段");
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  // 检查parts数组
 | 
					  // 检查parts数组
 | 
				
			||||||
  if (!Array.isArray(data.parts)) {
 | 
					  if (!Array.isArray(data.parts)) {
 | 
				
			||||||
    errors.push('parts字段不是数组');
 | 
					    errors.push("parts字段不是数组");
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    // 验证parts中的每个对象
 | 
					    // 验证parts中的每个对象
 | 
				
			||||||
    data.parts.forEach((part: any, index: number) => {
 | 
					    data.parts.forEach((part: any, index: number) => {
 | 
				
			||||||
      if (!part.id) errors.push(`parts[${index}]缺少id`);
 | 
					      if (!part.id) errors.push(`parts[${index}]缺少id`);
 | 
				
			||||||
      if (!part.type) errors.push(`parts[${index}]缺少type`);
 | 
					      if (!part.type) errors.push(`parts[${index}]缺少type`);
 | 
				
			||||||
      if (typeof part.x !== 'number') errors.push(`parts[${index}]缺少有效的x坐标`);
 | 
					      if (typeof part.x !== "number")
 | 
				
			||||||
      if (typeof part.y !== 'number') errors.push(`parts[${index}]缺少有效的y坐标`);
 | 
					        errors.push(`parts[${index}]缺少有效的x坐标`);
 | 
				
			||||||
 | 
					      if (typeof part.y !== "number")
 | 
				
			||||||
 | 
					        errors.push(`parts[${index}]缺少有效的y坐标`);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  // 检查connections数组
 | 
					  // 检查connections数组
 | 
				
			||||||
  if (!Array.isArray(data.connections)) {
 | 
					  if (!Array.isArray(data.connections)) {
 | 
				
			||||||
    errors.push('connections字段不是数组');
 | 
					    errors.push("connections字段不是数组");
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    // 验证connections中的每个数组
 | 
					    // 验证connections中的每个数组
 | 
				
			||||||
    data.connections.forEach((conn: any, index: number) => {
 | 
					    data.connections.forEach((conn: any, index: number) => {
 | 
				
			||||||
@@ -283,25 +300,25 @@ export function validateDiagramData(data: any): { isValid: boolean; errors: stri
 | 
				
			|||||||
        errors.push(`connections[${index}]不是有效的连接数组`);
 | 
					        errors.push(`connections[${index}]不是有效的连接数组`);
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      
 | 
					
 | 
				
			||||||
      const [startPin, endPin, width] = conn;
 | 
					      const [startPin, endPin, width] = conn;
 | 
				
			||||||
      
 | 
					
 | 
				
			||||||
      if (typeof startPin !== 'string' || !startPin.includes(':')) {
 | 
					      if (typeof startPin !== "string" || !startPin.includes(":")) {
 | 
				
			||||||
        errors.push(`connections[${index}]的起始针脚格式无效`);
 | 
					        errors.push(`connections[${index}]的起始针脚格式无效`);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      
 | 
					
 | 
				
			||||||
      if (typeof endPin !== 'string' || !endPin.includes(':')) {
 | 
					      if (typeof endPin !== "string" || !endPin.includes(":")) {
 | 
				
			||||||
        errors.push(`connections[${index}]的结束针脚格式无效`);
 | 
					        errors.push(`connections[${index}]的结束针脚格式无效`);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      
 | 
					
 | 
				
			||||||
      if (typeof width !== 'number') {
 | 
					      if (typeof width !== "number") {
 | 
				
			||||||
        errors.push(`connections[${index}]的宽度不是有效的数字`);
 | 
					        errors.push(`connections[${index}]的宽度不是有效的数字`);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    isValid: errors.length === 0,
 | 
					    isValid: errors.length === 0,
 | 
				
			||||||
    errors
 | 
					    errors,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,7 +32,7 @@ export const previewSizes: Record<string, number> = {
 | 
				
			|||||||
  EC11RotaryEncoder: 0.4,
 | 
					  EC11RotaryEncoder: 0.4,
 | 
				
			||||||
  Pin: 0.8,
 | 
					  Pin: 0.8,
 | 
				
			||||||
  SMT_LED: 0.7,
 | 
					  SMT_LED: 0.7,
 | 
				
			||||||
  SevenSegmentDisplay: 0.4,
 | 
					  SevenSegmentDisplayUltimate: 0.4,
 | 
				
			||||||
  HDMI: 0.5,
 | 
					  HDMI: 0.5,
 | 
				
			||||||
  DDR: 0.5,
 | 
					  DDR: 0.5,
 | 
				
			||||||
  ETH: 0.5,
 | 
					  ETH: 0.5,
 | 
				
			||||||
@@ -52,7 +52,7 @@ export const availableComponents: ComponentConfig[] = [
 | 
				
			|||||||
  { type: "EC11RotaryEncoder", name: "EC11旋转编码器" },
 | 
					  { type: "EC11RotaryEncoder", name: "EC11旋转编码器" },
 | 
				
			||||||
  { type: "Pin", name: "引脚" },
 | 
					  { type: "Pin", name: "引脚" },
 | 
				
			||||||
  { type: "SMT_LED", name: "贴片LED" },
 | 
					  { type: "SMT_LED", name: "贴片LED" },
 | 
				
			||||||
  { type: "SevenSegmentDisplay", name: "数码管" },
 | 
					  { type: "SevenSegmentDisplayUltimate", name: "数码管" },
 | 
				
			||||||
  { type: "HDMI", name: "HDMI接口" },
 | 
					  { type: "HDMI", name: "HDMI接口" },
 | 
				
			||||||
  { type: "DDR", name: "DDR内存" },
 | 
					  { type: "DDR", name: "DDR内存" },
 | 
				
			||||||
  { type: "ETH", name: "以太网接口" },
 | 
					  { type: "ETH", name: "以太网接口" },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -31,8 +31,16 @@ export type Channel = {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// 全局模式选项
 | 
					// 全局模式选项
 | 
				
			||||||
const globalModes = [
 | 
					const globalModes = [
 | 
				
			||||||
  {value: GlobalCaptureMode.AND,label: "AND",description: "所有条件都满足时触发",},
 | 
					  {
 | 
				
			||||||
  {value: GlobalCaptureMode.OR,label: "OR",description: "任一条件满足时触发",},
 | 
					    value: GlobalCaptureMode.AND,
 | 
				
			||||||
 | 
					    label: "AND",
 | 
				
			||||||
 | 
					    description: "所有条件都满足时触发",
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    value: GlobalCaptureMode.OR,
 | 
				
			||||||
 | 
					    label: "OR",
 | 
				
			||||||
 | 
					    description: "任一条件满足时触发",
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  { value: GlobalCaptureMode.NAND, label: "NAND", description: "AND的非" },
 | 
					  { value: GlobalCaptureMode.NAND, label: "NAND", description: "AND的非" },
 | 
				
			||||||
  { value: GlobalCaptureMode.NOR, label: "NOR", description: "OR的非" },
 | 
					  { value: GlobalCaptureMode.NOR, label: "NOR", description: "OR的非" },
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
@@ -70,21 +78,53 @@ const channelDivOptions = [
 | 
				
			|||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ClockDivOptions = [
 | 
					const ClockDivOptions = [
 | 
				
			||||||
  { value: AnalyzerClockDiv.DIV1, label: "120MHz", description: "采样频率120MHz" },
 | 
					  {
 | 
				
			||||||
  { value: AnalyzerClockDiv.DIV2, label: "60MHz", description: "采样频率60MHz" },
 | 
					    value: AnalyzerClockDiv.DIV1,
 | 
				
			||||||
  { value: AnalyzerClockDiv.DIV4, label: "30MHz", description: "采样频率30MHz" },
 | 
					    label: "120MHz",
 | 
				
			||||||
  { value: AnalyzerClockDiv.DIV8, label: "15MHz", description: "采样频率15MHz" },
 | 
					    description: "采样频率120MHz",
 | 
				
			||||||
  { value: AnalyzerClockDiv.DIV16, label: "7.5MHz", description: "采样频率7.5MHz" },
 | 
					  },
 | 
				
			||||||
  { value: AnalyzerClockDiv.DIV32, label: "3.75MHz", description: "采样频率3.75MHz" },
 | 
					  {
 | 
				
			||||||
  { value: AnalyzerClockDiv.DIV64, label: "1.875MHz", description: "采样频率1.875MHz" },
 | 
					    value: AnalyzerClockDiv.DIV2,
 | 
				
			||||||
  { value: AnalyzerClockDiv.DIV128, label: "937.5KHz", description: "采样频率937.5KHz" },
 | 
					    label: "60MHz",
 | 
				
			||||||
 | 
					    description: "采样频率60MHz",
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    value: AnalyzerClockDiv.DIV4,
 | 
				
			||||||
 | 
					    label: "30MHz",
 | 
				
			||||||
 | 
					    description: "采样频率30MHz",
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    value: AnalyzerClockDiv.DIV8,
 | 
				
			||||||
 | 
					    label: "15MHz",
 | 
				
			||||||
 | 
					    description: "采样频率15MHz",
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    value: AnalyzerClockDiv.DIV16,
 | 
				
			||||||
 | 
					    label: "7.5MHz",
 | 
				
			||||||
 | 
					    description: "采样频率7.5MHz",
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    value: AnalyzerClockDiv.DIV32,
 | 
				
			||||||
 | 
					    label: "3.75MHz",
 | 
				
			||||||
 | 
					    description: "采样频率3.75MHz",
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    value: AnalyzerClockDiv.DIV64,
 | 
				
			||||||
 | 
					    label: "1.875MHz",
 | 
				
			||||||
 | 
					    description: "采样频率1.875MHz",
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    value: AnalyzerClockDiv.DIV128,
 | 
				
			||||||
 | 
					    label: "937.5KHz",
 | 
				
			||||||
 | 
					    description: "采样频率937.5KHz",
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 捕获深度限制常量
 | 
					// 捕获深度限制常量
 | 
				
			||||||
const CAPTURE_LENGTH_MIN = 1024; // 最小捕获深度 1024
 | 
					const CAPTURE_LENGTH_MIN = 1024; // 最小捕获深度 1024
 | 
				
			||||||
const CAPTURE_LENGTH_MAX = 0x10000000 - 0x01000000; // 最大捕获深度
 | 
					const CAPTURE_LENGTH_MAX = 0x10000000 - 0x01000000; // 最大捕获深度
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 预捕获深度限制常量  
 | 
					// 预捕获深度限制常量
 | 
				
			||||||
const PRE_CAPTURE_LENGTH_MIN = 2; // 最小预捕获深度 2
 | 
					const PRE_CAPTURE_LENGTH_MIN = 2; // 最小预捕获深度 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 默认颜色数组
 | 
					// 默认颜色数组
 | 
				
			||||||
@@ -170,40 +210,64 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
 | 
				
			|||||||
    // 转换通道数字到枚举值
 | 
					    // 转换通道数字到枚举值
 | 
				
			||||||
    const getChannelDivEnum = (channelCount: number): AnalyzerChannelDiv => {
 | 
					    const getChannelDivEnum = (channelCount: number): AnalyzerChannelDiv => {
 | 
				
			||||||
      switch (channelCount) {
 | 
					      switch (channelCount) {
 | 
				
			||||||
        case 1: return AnalyzerChannelDiv.ONE;
 | 
					        case 1:
 | 
				
			||||||
        case 2: return AnalyzerChannelDiv.TWO;
 | 
					          return AnalyzerChannelDiv.ONE;
 | 
				
			||||||
        case 4: return AnalyzerChannelDiv.FOUR;
 | 
					        case 2:
 | 
				
			||||||
        case 8: return AnalyzerChannelDiv.EIGHT;
 | 
					          return AnalyzerChannelDiv.TWO;
 | 
				
			||||||
        case 16: return AnalyzerChannelDiv.XVI;
 | 
					        case 4:
 | 
				
			||||||
        case 32: return AnalyzerChannelDiv.XXXII;
 | 
					          return AnalyzerChannelDiv.FOUR;
 | 
				
			||||||
        default: return AnalyzerChannelDiv.EIGHT;
 | 
					        case 8:
 | 
				
			||||||
 | 
					          return AnalyzerChannelDiv.EIGHT;
 | 
				
			||||||
 | 
					        case 16:
 | 
				
			||||||
 | 
					          return AnalyzerChannelDiv.XVI;
 | 
				
			||||||
 | 
					        case 32:
 | 
				
			||||||
 | 
					          return AnalyzerChannelDiv.XXXII;
 | 
				
			||||||
 | 
					        default:
 | 
				
			||||||
 | 
					          return AnalyzerChannelDiv.EIGHT;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // 验证捕获深度
 | 
					    // 验证捕获深度
 | 
				
			||||||
    const validateCaptureLength = (value: number): { valid: boolean; message?: string } => {
 | 
					    const validateCaptureLength = (
 | 
				
			||||||
 | 
					      value: number,
 | 
				
			||||||
 | 
					    ): { valid: boolean; message?: string } => {
 | 
				
			||||||
      if (!Number.isInteger(value)) {
 | 
					      if (!Number.isInteger(value)) {
 | 
				
			||||||
        return { valid: false, message: "捕获深度必须是整数" };
 | 
					        return { valid: false, message: "捕获深度必须是整数" };
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      if (value < CAPTURE_LENGTH_MIN) {
 | 
					      if (value < CAPTURE_LENGTH_MIN) {
 | 
				
			||||||
        return { valid: false, message: `捕获深度不能小于 ${CAPTURE_LENGTH_MIN}` };
 | 
					        return {
 | 
				
			||||||
 | 
					          valid: false,
 | 
				
			||||||
 | 
					          message: `捕获深度不能小于 ${CAPTURE_LENGTH_MIN}`,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      if (value > CAPTURE_LENGTH_MAX) {
 | 
					      if (value > CAPTURE_LENGTH_MAX) {
 | 
				
			||||||
        return { valid: false, message: `捕获深度不能大于 ${CAPTURE_LENGTH_MAX.toLocaleString()}` };
 | 
					        return {
 | 
				
			||||||
 | 
					          valid: false,
 | 
				
			||||||
 | 
					          message: `捕获深度不能大于 ${CAPTURE_LENGTH_MAX.toLocaleString()}`,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return { valid: true };
 | 
					      return { valid: true };
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // 验证预捕获深度
 | 
					    // 验证预捕获深度
 | 
				
			||||||
    const validatePreCaptureLength = (value: number, currentCaptureLength: number): { valid: boolean; message?: string } => {
 | 
					    const validatePreCaptureLength = (
 | 
				
			||||||
 | 
					      value: number,
 | 
				
			||||||
 | 
					      currentCaptureLength: number,
 | 
				
			||||||
 | 
					    ): { valid: boolean; message?: string } => {
 | 
				
			||||||
      if (!Number.isInteger(value)) {
 | 
					      if (!Number.isInteger(value)) {
 | 
				
			||||||
        return { valid: false, message: "预捕获深度必须是整数" };
 | 
					        return { valid: false, message: "预捕获深度必须是整数" };
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      if (value < PRE_CAPTURE_LENGTH_MIN) {
 | 
					      if (value < PRE_CAPTURE_LENGTH_MIN) {
 | 
				
			||||||
        return { valid: false, message: `预捕获深度不能小于 ${PRE_CAPTURE_LENGTH_MIN}` };
 | 
					        return {
 | 
				
			||||||
 | 
					          valid: false,
 | 
				
			||||||
 | 
					          message: `预捕获深度不能小于 ${PRE_CAPTURE_LENGTH_MIN}`,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      if (value >= currentCaptureLength) {
 | 
					      if (value >= currentCaptureLength) {
 | 
				
			||||||
        return { valid: false, message: `预捕获深度不能大于等于捕获深度 (${currentCaptureLength})` };
 | 
					        return {
 | 
				
			||||||
 | 
					          valid: false,
 | 
				
			||||||
 | 
					          message: `预捕获深度不能大于等于捕获深度 (${currentCaptureLength})`,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return { valid: true };
 | 
					      return { valid: true };
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
@@ -215,13 +279,13 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
 | 
				
			|||||||
        alert?.error(validation.message!, 3000);
 | 
					        alert?.error(validation.message!, 3000);
 | 
				
			||||||
        return false;
 | 
					        return false;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      
 | 
					
 | 
				
			||||||
      // 检查预捕获深度是否仍然有效
 | 
					      // 检查预捕获深度是否仍然有效
 | 
				
			||||||
      if (preCaptureLength.value >= value) {
 | 
					      if (preCaptureLength.value >= value) {
 | 
				
			||||||
        preCaptureLength.value = Math.max(0, value - 1);
 | 
					        preCaptureLength.value = Math.max(0, value - 1);
 | 
				
			||||||
        alert?.warn(`预捕获深度已自动调整为 ${preCaptureLength.value}`, 3000);
 | 
					        alert?.warn(`预捕获深度已自动调整为 ${preCaptureLength.value}`, 3000);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      
 | 
					
 | 
				
			||||||
      captureLength.value = value;
 | 
					      captureLength.value = value;
 | 
				
			||||||
      return true;
 | 
					      return true;
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
@@ -233,7 +297,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
 | 
				
			|||||||
        alert?.error(validation.message!, 3000);
 | 
					        alert?.error(validation.message!, 3000);
 | 
				
			||||||
        return false;
 | 
					        return false;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      
 | 
					
 | 
				
			||||||
      preCaptureLength.value = value;
 | 
					      preCaptureLength.value = value;
 | 
				
			||||||
      return true;
 | 
					      return true;
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
@@ -241,12 +305,12 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
 | 
				
			|||||||
    // 设置通道组
 | 
					    // 设置通道组
 | 
				
			||||||
    const setChannelDiv = (channelCount: number) => {
 | 
					    const setChannelDiv = (channelCount: number) => {
 | 
				
			||||||
      // 验证通道数量是否有效
 | 
					      // 验证通道数量是否有效
 | 
				
			||||||
      if (!channelDivOptions.find(option => option.value === channelCount)) {
 | 
					      if (!channelDivOptions.find((option) => option.value === channelCount)) {
 | 
				
			||||||
        console.error(`无效的通道组设置: ${channelCount}`);
 | 
					        console.error(`无效的通道组设置: ${channelCount}`);
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      currentChannelDiv.value = channelCount;
 | 
					      currentChannelDiv.value = channelCount;
 | 
				
			||||||
      
 | 
					
 | 
				
			||||||
      // 禁用所有通道
 | 
					      // 禁用所有通道
 | 
				
			||||||
      channels.forEach((channel) => {
 | 
					      channels.forEach((channel) => {
 | 
				
			||||||
        channel.enabled = false;
 | 
					        channel.enabled = false;
 | 
				
			||||||
@@ -257,7 +321,9 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
 | 
				
			|||||||
        channels[i].enabled = true;
 | 
					        channels[i].enabled = true;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const option = channelDivOptions.find(opt => opt.value === channelCount);
 | 
					      const option = channelDivOptions.find(
 | 
				
			||||||
 | 
					        (opt) => opt.value === channelCount,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
      alert?.success(`已设置为${option?.label}`, 2000);
 | 
					      alert?.success(`已设置为${option?.label}`, 2000);
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -294,7 +360,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const getCaptureData = async () => {
 | 
					    const getCaptureData = async () => {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        const client = AuthManager.createAuthenticatedLogicAnalyzerClient();
 | 
					        const client = AuthManager.createClient(LogicAnalyzerClient);
 | 
				
			||||||
        // 获取捕获数据,使用当前设置的捕获长度
 | 
					        // 获取捕获数据,使用当前设置的捕获长度
 | 
				
			||||||
        const base64Data = await client.getCaptureData(captureLength.value);
 | 
					        const base64Data = await client.getCaptureData(captureLength.value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -308,7 +374,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
 | 
				
			|||||||
        // 根据当前通道数量解析数据
 | 
					        // 根据当前通道数量解析数据
 | 
				
			||||||
        const channelCount = currentChannelDiv.value;
 | 
					        const channelCount = currentChannelDiv.value;
 | 
				
			||||||
        const timeStepNs = currentSamplePeriodNs.value;
 | 
					        const timeStepNs = currentSamplePeriodNs.value;
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        let sampleCount: number;
 | 
					        let sampleCount: number;
 | 
				
			||||||
        let x: number[];
 | 
					        let x: number[];
 | 
				
			||||||
        let y: number[][];
 | 
					        let y: number[][];
 | 
				
			||||||
@@ -316,19 +382,16 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
 | 
				
			|||||||
        if (channelCount === 1) {
 | 
					        if (channelCount === 1) {
 | 
				
			||||||
          // 1通道:每个字节包含8个时间单位的数据
 | 
					          // 1通道:每个字节包含8个时间单位的数据
 | 
				
			||||||
          sampleCount = bytes.length * 8;
 | 
					          sampleCount = bytes.length * 8;
 | 
				
			||||||
          
 | 
					
 | 
				
			||||||
          // 创建时间轴
 | 
					          // 创建时间轴
 | 
				
			||||||
          x = Array.from(
 | 
					          x = Array.from(
 | 
				
			||||||
            { length: sampleCount },
 | 
					            { length: sampleCount },
 | 
				
			||||||
            (_, i) => (i * timeStepNs) / 1000,
 | 
					            (_, i) => (i * timeStepNs) / 1000,
 | 
				
			||||||
          ); // 转换为微秒
 | 
					          ); // 转换为微秒
 | 
				
			||||||
          
 | 
					
 | 
				
			||||||
          // 创建通道数据数组
 | 
					          // 创建通道数据数组
 | 
				
			||||||
          y = Array.from(
 | 
					          y = Array.from({ length: 1 }, () => new Array(sampleCount));
 | 
				
			||||||
            { length: 1 },
 | 
					
 | 
				
			||||||
            () => new Array(sampleCount),
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
          
 | 
					 | 
				
			||||||
          // 解析数据:每个字节的8个位对应8个时间单位
 | 
					          // 解析数据:每个字节的8个位对应8个时间单位
 | 
				
			||||||
          for (let byteIndex = 0; byteIndex < bytes.length; byteIndex++) {
 | 
					          for (let byteIndex = 0; byteIndex < bytes.length; byteIndex++) {
 | 
				
			||||||
            const byte = bytes[byteIndex];
 | 
					            const byte = bytes[byteIndex];
 | 
				
			||||||
@@ -340,19 +403,16 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
 | 
				
			|||||||
        } else if (channelCount === 2) {
 | 
					        } else if (channelCount === 2) {
 | 
				
			||||||
          // 2通道:每个字节包含4个时间单位的数据
 | 
					          // 2通道:每个字节包含4个时间单位的数据
 | 
				
			||||||
          sampleCount = bytes.length * 4;
 | 
					          sampleCount = bytes.length * 4;
 | 
				
			||||||
          
 | 
					
 | 
				
			||||||
          // 创建时间轴
 | 
					          // 创建时间轴
 | 
				
			||||||
          x = Array.from(
 | 
					          x = Array.from(
 | 
				
			||||||
            { length: sampleCount },
 | 
					            { length: sampleCount },
 | 
				
			||||||
            (_, i) => (i * timeStepNs) / 1000,
 | 
					            (_, i) => (i * timeStepNs) / 1000,
 | 
				
			||||||
          ); // 转换为微秒
 | 
					          ); // 转换为微秒
 | 
				
			||||||
          
 | 
					
 | 
				
			||||||
          // 创建通道数据数组
 | 
					          // 创建通道数据数组
 | 
				
			||||||
          y = Array.from(
 | 
					          y = Array.from({ length: 2 }, () => new Array(sampleCount));
 | 
				
			||||||
            { length: 2 },
 | 
					
 | 
				
			||||||
            () => new Array(sampleCount),
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
          
 | 
					 | 
				
			||||||
          // 解析数据:每个字节的8个位对应4个时间单位的2通道数据
 | 
					          // 解析数据:每个字节的8个位对应4个时间单位的2通道数据
 | 
				
			||||||
          // 位分布:[T3_CH1, T3_CH0, T2_CH1, T2_CH0, T1_CH1, T1_CH0, T0_CH1, T0_CH0]
 | 
					          // 位分布:[T3_CH1, T3_CH0, T2_CH1, T2_CH0, T1_CH1, T1_CH0, T0_CH1, T0_CH0]
 | 
				
			||||||
          for (let byteIndex = 0; byteIndex < bytes.length; byteIndex++) {
 | 
					          for (let byteIndex = 0; byteIndex < bytes.length; byteIndex++) {
 | 
				
			||||||
@@ -360,37 +420,34 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
 | 
				
			|||||||
            for (let timeUnit = 0; timeUnit < 4; timeUnit++) {
 | 
					            for (let timeUnit = 0; timeUnit < 4; timeUnit++) {
 | 
				
			||||||
              const timeIndex = byteIndex * 4 + timeUnit;
 | 
					              const timeIndex = byteIndex * 4 + timeUnit;
 | 
				
			||||||
              const bitOffset = timeUnit * 2;
 | 
					              const bitOffset = timeUnit * 2;
 | 
				
			||||||
              y[0][timeIndex] = (byte >> bitOffset) & 1;       // CH0
 | 
					              y[0][timeIndex] = (byte >> bitOffset) & 1; // CH0
 | 
				
			||||||
              y[1][timeIndex] = (byte >> (bitOffset + 1)) & 1; // CH1
 | 
					              y[1][timeIndex] = (byte >> (bitOffset + 1)) & 1; // CH1
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        } else if (channelCount === 4) {
 | 
					        } else if (channelCount === 4) {
 | 
				
			||||||
          // 4通道:每个字节包含2个时间单位的数据
 | 
					          // 4通道:每个字节包含2个时间单位的数据
 | 
				
			||||||
          sampleCount = bytes.length * 2;
 | 
					          sampleCount = bytes.length * 2;
 | 
				
			||||||
          
 | 
					
 | 
				
			||||||
          // 创建时间轴
 | 
					          // 创建时间轴
 | 
				
			||||||
          x = Array.from(
 | 
					          x = Array.from(
 | 
				
			||||||
            { length: sampleCount },
 | 
					            { length: sampleCount },
 | 
				
			||||||
            (_, i) => (i * timeStepNs) / 1000,
 | 
					            (_, i) => (i * timeStepNs) / 1000,
 | 
				
			||||||
          ); // 转换为微秒
 | 
					          ); // 转换为微秒
 | 
				
			||||||
          
 | 
					
 | 
				
			||||||
          // 创建通道数据数组
 | 
					          // 创建通道数据数组
 | 
				
			||||||
          y = Array.from(
 | 
					          y = Array.from({ length: 4 }, () => new Array(sampleCount));
 | 
				
			||||||
            { length: 4 },
 | 
					
 | 
				
			||||||
            () => new Array(sampleCount),
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
          
 | 
					 | 
				
			||||||
          // 解析数据:每个字节的8个位对应2个时间单位的4通道数据
 | 
					          // 解析数据:每个字节的8个位对应2个时间单位的4通道数据
 | 
				
			||||||
          // 位分布:[T1_CH3, T1_CH2, T1_CH1, T1_CH0, T0_CH3, T0_CH2, T0_CH1, T0_CH0]
 | 
					          // 位分布:[T1_CH3, T1_CH2, T1_CH1, T1_CH0, T0_CH3, T0_CH2, T0_CH1, T0_CH0]
 | 
				
			||||||
          for (let byteIndex = 0; byteIndex < bytes.length; byteIndex++) {
 | 
					          for (let byteIndex = 0; byteIndex < bytes.length; byteIndex++) {
 | 
				
			||||||
            const byte = bytes[byteIndex];
 | 
					            const byte = bytes[byteIndex];
 | 
				
			||||||
            
 | 
					
 | 
				
			||||||
            // 处理第一个时间单位(低4位)
 | 
					            // 处理第一个时间单位(低4位)
 | 
				
			||||||
            const timeIndex1 = byteIndex * 2;
 | 
					            const timeIndex1 = byteIndex * 2;
 | 
				
			||||||
            for (let channel = 0; channel < 4; channel++) {
 | 
					            for (let channel = 0; channel < 4; channel++) {
 | 
				
			||||||
              y[channel][timeIndex1] = (byte >> channel) & 1;
 | 
					              y[channel][timeIndex1] = (byte >> channel) & 1;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            
 | 
					
 | 
				
			||||||
            // 处理第二个时间单位(高4位)
 | 
					            // 处理第二个时间单位(高4位)
 | 
				
			||||||
            const timeIndex2 = byteIndex * 2 + 1;
 | 
					            const timeIndex2 = byteIndex * 2 + 1;
 | 
				
			||||||
            for (let channel = 0; channel < 4; channel++) {
 | 
					            for (let channel = 0; channel < 4; channel++) {
 | 
				
			||||||
@@ -400,19 +457,16 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
 | 
				
			|||||||
        } else if (channelCount === 8) {
 | 
					        } else if (channelCount === 8) {
 | 
				
			||||||
          // 8通道:每个字节包含1个时间单位的8个通道数据
 | 
					          // 8通道:每个字节包含1个时间单位的8个通道数据
 | 
				
			||||||
          sampleCount = bytes.length;
 | 
					          sampleCount = bytes.length;
 | 
				
			||||||
          
 | 
					
 | 
				
			||||||
          // 创建时间轴
 | 
					          // 创建时间轴
 | 
				
			||||||
          x = Array.from(
 | 
					          x = Array.from(
 | 
				
			||||||
            { length: sampleCount },
 | 
					            { length: sampleCount },
 | 
				
			||||||
            (_, i) => (i * timeStepNs) / 1000,
 | 
					            (_, i) => (i * timeStepNs) / 1000,
 | 
				
			||||||
          ); // 转换为微秒
 | 
					          ); // 转换为微秒
 | 
				
			||||||
          
 | 
					
 | 
				
			||||||
          // 创建8个通道的数据
 | 
					          // 创建8个通道的数据
 | 
				
			||||||
          y = Array.from(
 | 
					          y = Array.from({ length: 8 }, () => new Array(sampleCount));
 | 
				
			||||||
            { length: 8 },
 | 
					
 | 
				
			||||||
            () => new Array(sampleCount),
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
          
 | 
					 | 
				
			||||||
          // 解析每个字节的8个位到对应通道
 | 
					          // 解析每个字节的8个位到对应通道
 | 
				
			||||||
          for (let i = 0; i < sampleCount; i++) {
 | 
					          for (let i = 0; i < sampleCount; i++) {
 | 
				
			||||||
            const byte = bytes[i];
 | 
					            const byte = bytes[i];
 | 
				
			||||||
@@ -424,30 +478,27 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
 | 
				
			|||||||
        } else if (channelCount === 16) {
 | 
					        } else if (channelCount === 16) {
 | 
				
			||||||
          // 16通道:每2个字节包含1个时间单位的16个通道数据
 | 
					          // 16通道:每2个字节包含1个时间单位的16个通道数据
 | 
				
			||||||
          sampleCount = bytes.length / 2;
 | 
					          sampleCount = bytes.length / 2;
 | 
				
			||||||
          
 | 
					
 | 
				
			||||||
          // 创建时间轴
 | 
					          // 创建时间轴
 | 
				
			||||||
          x = Array.from(
 | 
					          x = Array.from(
 | 
				
			||||||
            { length: sampleCount },
 | 
					            { length: sampleCount },
 | 
				
			||||||
            (_, i) => (i * timeStepNs) / 1000,
 | 
					            (_, i) => (i * timeStepNs) / 1000,
 | 
				
			||||||
          ); // 转换为微秒
 | 
					          ); // 转换为微秒
 | 
				
			||||||
          
 | 
					
 | 
				
			||||||
          // 创建16个通道的数据
 | 
					          // 创建16个通道的数据
 | 
				
			||||||
          y = Array.from(
 | 
					          y = Array.from({ length: 16 }, () => new Array(sampleCount));
 | 
				
			||||||
            { length: 16 },
 | 
					
 | 
				
			||||||
            () => new Array(sampleCount),
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
          
 | 
					 | 
				
			||||||
          // 解析数据:每2个字节为一个时间单位
 | 
					          // 解析数据:每2个字节为一个时间单位
 | 
				
			||||||
          for (let timeIndex = 0; timeIndex < sampleCount; timeIndex++) {
 | 
					          for (let timeIndex = 0; timeIndex < sampleCount; timeIndex++) {
 | 
				
			||||||
            const byteIndex = timeIndex * 2;
 | 
					            const byteIndex = timeIndex * 2;
 | 
				
			||||||
            const byte1 = bytes[byteIndex];     // [7:0]
 | 
					            const byte1 = bytes[byteIndex]; // [7:0]
 | 
				
			||||||
            const byte2 = bytes[byteIndex + 1]; // [15:8]
 | 
					            const byte2 = bytes[byteIndex + 1]; // [15:8]
 | 
				
			||||||
            
 | 
					
 | 
				
			||||||
            // 处理低8位通道 [7:0]
 | 
					            // 处理低8位通道 [7:0]
 | 
				
			||||||
            for (let channel = 0; channel < 8; channel++) {
 | 
					            for (let channel = 0; channel < 8; channel++) {
 | 
				
			||||||
              y[channel][timeIndex] = (byte1 >> channel) & 1;
 | 
					              y[channel][timeIndex] = (byte1 >> channel) & 1;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            
 | 
					
 | 
				
			||||||
            // 处理高8位通道 [15:8]
 | 
					            // 处理高8位通道 [15:8]
 | 
				
			||||||
            for (let channel = 0; channel < 8; channel++) {
 | 
					            for (let channel = 0; channel < 8; channel++) {
 | 
				
			||||||
              y[channel + 8][timeIndex] = (byte2 >> channel) & 1;
 | 
					              y[channel + 8][timeIndex] = (byte2 >> channel) & 1;
 | 
				
			||||||
@@ -456,42 +507,39 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
 | 
				
			|||||||
        } else if (channelCount === 32) {
 | 
					        } else if (channelCount === 32) {
 | 
				
			||||||
          // 32通道:每4个字节包含1个时间单位的32个通道数据
 | 
					          // 32通道:每4个字节包含1个时间单位的32个通道数据
 | 
				
			||||||
          sampleCount = bytes.length / 4;
 | 
					          sampleCount = bytes.length / 4;
 | 
				
			||||||
          
 | 
					
 | 
				
			||||||
          // 创建时间轴
 | 
					          // 创建时间轴
 | 
				
			||||||
          x = Array.from(
 | 
					          x = Array.from(
 | 
				
			||||||
            { length: sampleCount },
 | 
					            { length: sampleCount },
 | 
				
			||||||
            (_, i) => (i * timeStepNs) / 1000,
 | 
					            (_, i) => (i * timeStepNs) / 1000,
 | 
				
			||||||
          ); // 转换为微秒
 | 
					          ); // 转换为微秒
 | 
				
			||||||
          
 | 
					
 | 
				
			||||||
          // 创建32个通道的数据
 | 
					          // 创建32个通道的数据
 | 
				
			||||||
          y = Array.from(
 | 
					          y = Array.from({ length: 32 }, () => new Array(sampleCount));
 | 
				
			||||||
            { length: 32 },
 | 
					
 | 
				
			||||||
            () => new Array(sampleCount),
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
          
 | 
					 | 
				
			||||||
          // 解析数据:每4个字节为一个时间单位
 | 
					          // 解析数据:每4个字节为一个时间单位
 | 
				
			||||||
          for (let timeIndex = 0; timeIndex < sampleCount; timeIndex++) {
 | 
					          for (let timeIndex = 0; timeIndex < sampleCount; timeIndex++) {
 | 
				
			||||||
            const byteIndex = timeIndex * 4;
 | 
					            const byteIndex = timeIndex * 4;
 | 
				
			||||||
            const byte1 = bytes[byteIndex];     // [7:0]
 | 
					            const byte1 = bytes[byteIndex]; // [7:0]
 | 
				
			||||||
            const byte2 = bytes[byteIndex + 1]; // [15:8]
 | 
					            const byte2 = bytes[byteIndex + 1]; // [15:8]
 | 
				
			||||||
            const byte3 = bytes[byteIndex + 2]; // [23:16]
 | 
					            const byte3 = bytes[byteIndex + 2]; // [23:16]
 | 
				
			||||||
            const byte4 = bytes[byteIndex + 3]; // [31:24]
 | 
					            const byte4 = bytes[byteIndex + 3]; // [31:24]
 | 
				
			||||||
            
 | 
					
 | 
				
			||||||
            // 处理 [7:0]
 | 
					            // 处理 [7:0]
 | 
				
			||||||
            for (let channel = 0; channel < 8; channel++) {
 | 
					            for (let channel = 0; channel < 8; channel++) {
 | 
				
			||||||
              y[channel][timeIndex] = (byte1 >> channel) & 1;
 | 
					              y[channel][timeIndex] = (byte1 >> channel) & 1;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            
 | 
					
 | 
				
			||||||
            // 处理 [15:8]
 | 
					            // 处理 [15:8]
 | 
				
			||||||
            for (let channel = 0; channel < 8; channel++) {
 | 
					            for (let channel = 0; channel < 8; channel++) {
 | 
				
			||||||
              y[channel + 8][timeIndex] = (byte2 >> channel) & 1;
 | 
					              y[channel + 8][timeIndex] = (byte2 >> channel) & 1;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            
 | 
					
 | 
				
			||||||
            // 处理 [23:16]
 | 
					            // 处理 [23:16]
 | 
				
			||||||
            for (let channel = 0; channel < 8; channel++) {
 | 
					            for (let channel = 0; channel < 8; channel++) {
 | 
				
			||||||
              y[channel + 16][timeIndex] = (byte3 >> channel) & 1;
 | 
					              y[channel + 16][timeIndex] = (byte3 >> channel) & 1;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            
 | 
					
 | 
				
			||||||
            // 处理 [31:24]
 | 
					            // 处理 [31:24]
 | 
				
			||||||
            for (let channel = 0; channel < 8; channel++) {
 | 
					            for (let channel = 0; channel < 8; channel++) {
 | 
				
			||||||
              y[channel + 24][timeIndex] = (byte4 >> channel) & 1;
 | 
					              y[channel + 24][timeIndex] = (byte4 >> channel) & 1;
 | 
				
			||||||
@@ -525,11 +573,11 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
 | 
				
			|||||||
      isCapturing.value = true;
 | 
					      isCapturing.value = true;
 | 
				
			||||||
      const release = await operationMutex.acquire();
 | 
					      const release = await operationMutex.acquire();
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        const client = AuthManager.createAuthenticatedLogicAnalyzerClient();
 | 
					        const client = AuthManager.createClient(LogicAnalyzerClient);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // 1. 先应用配置
 | 
					        // 1. 先应用配置
 | 
				
			||||||
        alert?.info("正在应用配置...", 2000);
 | 
					        alert?.info("正在应用配置...", 2000);
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        // 准备配置数据 - 包含所有32个通道,未启用的通道设置为默认值
 | 
					        // 准备配置数据 - 包含所有32个通道,未启用的通道设置为默认值
 | 
				
			||||||
        const allSignals = signalConfigs.map((signal, index) => {
 | 
					        const allSignals = signalConfigs.map((signal, index) => {
 | 
				
			||||||
          if (channels[index].enabled) {
 | 
					          if (channels[index].enabled) {
 | 
				
			||||||
@@ -632,7 +680,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      const release = await operationMutex.acquire();
 | 
					      const release = await operationMutex.acquire();
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        const client = AuthManager.createAuthenticatedLogicAnalyzerClient();
 | 
					        const client = AuthManager.createClient(LogicAnalyzerClient);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // 执行强制捕获来停止当前捕获
 | 
					        // 执行强制捕获来停止当前捕获
 | 
				
			||||||
        const forceSuccess = await client.setCaptureMode(false, false);
 | 
					        const forceSuccess = await client.setCaptureMode(false, false);
 | 
				
			||||||
@@ -661,7 +709,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      const release = await operationMutex.acquire();
 | 
					      const release = await operationMutex.acquire();
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        const client = AuthManager.createAuthenticatedLogicAnalyzerClient();
 | 
					        const client = AuthManager.createClient(LogicAnalyzerClient);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // 执行强制捕获来停止当前捕获
 | 
					        // 执行强制捕获来停止当前捕获
 | 
				
			||||||
        const forceSuccess = await client.setCaptureMode(true, true);
 | 
					        const forceSuccess = await client.setCaptureMode(true, true);
 | 
				
			||||||
@@ -677,7 +725,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
 | 
				
			|||||||
          `强制捕获失败: ${error instanceof Error ? error.message : "未知错误"}`,
 | 
					          `强制捕获失败: ${error instanceof Error ? error.message : "未知错误"}`,
 | 
				
			||||||
          3000,
 | 
					          3000,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      } finally{
 | 
					      } finally {
 | 
				
			||||||
        release();
 | 
					        release();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 
 | 
				
			|||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -145,6 +145,7 @@ import {
 | 
				
			|||||||
  ChevronDownIcon,
 | 
					  ChevronDownIcon,
 | 
				
			||||||
} from "lucide-vue-next";
 | 
					} from "lucide-vue-next";
 | 
				
			||||||
import { AuthManager } from "@/utils/AuthManager";
 | 
					import { AuthManager } from "@/utils/AuthManager";
 | 
				
			||||||
 | 
					import { DataClient } from "@/APIClient";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const router = useRouter();
 | 
					const router = useRouter();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -158,7 +159,7 @@ const loadUserInfo = async () => {
 | 
				
			|||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const authenticated = await AuthManager.isAuthenticated();
 | 
					    const authenticated = await AuthManager.isAuthenticated();
 | 
				
			||||||
    if (authenticated) {
 | 
					    if (authenticated) {
 | 
				
			||||||
      const client = AuthManager.createAuthenticatedDataClient();
 | 
					      const client = AuthManager.createClient(DataClient);
 | 
				
			||||||
      const userInfo = await client.getUserInfo();
 | 
					      const userInfo = await client.getUserInfo();
 | 
				
			||||||
      userName.value = userInfo.name;
 | 
					      userName.value = userInfo.name;
 | 
				
			||||||
      isLoggedIn.value = true;
 | 
					      isLoggedIn.value = true;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,7 @@ import { Mutex } from "async-mutex";
 | 
				
			|||||||
import {
 | 
					import {
 | 
				
			||||||
  OscilloscopeFullConfig,
 | 
					  OscilloscopeFullConfig,
 | 
				
			||||||
  OscilloscopeDataResponse,
 | 
					  OscilloscopeDataResponse,
 | 
				
			||||||
 | 
					  OscilloscopeApiClient,
 | 
				
			||||||
} from "@/APIClient";
 | 
					} from "@/APIClient";
 | 
				
			||||||
import { AuthManager } from "@/utils/AuthManager";
 | 
					import { AuthManager } from "@/utils/AuthManager";
 | 
				
			||||||
import { useAlertStore } from "@/components/Alert";
 | 
					import { useAlertStore } from "@/components/Alert";
 | 
				
			||||||
@@ -31,257 +32,269 @@ const DEFAULT_CONFIG: OscilloscopeFullConfig = new OscilloscopeFullConfig({
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 采样频率常量(后端返回)
 | 
					// 采样频率常量(后端返回)
 | 
				
			||||||
const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(() => {
 | 
					const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(
 | 
				
			||||||
  const oscData = shallowRef<OscilloscopeDataType>();
 | 
					  () => {
 | 
				
			||||||
  const alert = useRequiredInjection(useAlertStore);
 | 
					    const oscData = shallowRef<OscilloscopeDataType>();
 | 
				
			||||||
 | 
					    const alert = useRequiredInjection(useAlertStore);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 互斥锁
 | 
					    // 互斥锁
 | 
				
			||||||
  const operationMutex = new Mutex();
 | 
					    const operationMutex = new Mutex();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 状态
 | 
					    // 状态
 | 
				
			||||||
  const isApplying = ref(false);
 | 
					    const isApplying = ref(false);
 | 
				
			||||||
  const isCapturing = ref(false);
 | 
					    const isCapturing = ref(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 配置
 | 
					    // 配置
 | 
				
			||||||
  const config = reactive<OscilloscopeFullConfig>(new OscilloscopeFullConfig({ ...DEFAULT_CONFIG }));
 | 
					    const config = reactive<OscilloscopeFullConfig>(
 | 
				
			||||||
 | 
					      new OscilloscopeFullConfig({ ...DEFAULT_CONFIG }),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 采样点数(由后端数据决定)
 | 
					    // 采样点数(由后端数据决定)
 | 
				
			||||||
  const sampleCount = ref(0);
 | 
					    const sampleCount = ref(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 采样周期(ns),由adFrequency计算
 | 
					    // 采样周期(ns),由adFrequency计算
 | 
				
			||||||
  const samplePeriodNs = computed(() =>
 | 
					    const samplePeriodNs = computed(() =>
 | 
				
			||||||
    oscData.value?.adFrequency ? 1_000_000_000 / oscData.value.adFrequency : 200
 | 
					      oscData.value?.adFrequency
 | 
				
			||||||
  );
 | 
					        ? 1_000_000_000 / oscData.value.adFrequency
 | 
				
			||||||
 | 
					        : 200,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 应用配置
 | 
					    // 应用配置
 | 
				
			||||||
  const applyConfiguration = async () => {
 | 
					    const applyConfiguration = async () => {
 | 
				
			||||||
    if (operationMutex.isLocked()) {
 | 
					      if (operationMutex.isLocked()) {
 | 
				
			||||||
      alert.warn("有其他操作正在进行中,请稍后再试", 3000);
 | 
					        alert.warn("有其他操作正在进行中,请稍后再试", 3000);
 | 
				
			||||||
      return;
 | 
					        return;
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    const release = await operationMutex.acquire();
 | 
					 | 
				
			||||||
    isApplying.value = true;
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
 | 
					 | 
				
			||||||
      const success = await client.initialize({ ...config });
 | 
					 | 
				
			||||||
      if (success) {
 | 
					 | 
				
			||||||
        alert.success("示波器配置已应用", 2000);
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        throw new Error("应用失败");
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } catch (error) {
 | 
					      const release = await operationMutex.acquire();
 | 
				
			||||||
      alert.error("应用配置失败", 3000);
 | 
					      isApplying.value = true;
 | 
				
			||||||
    } finally {
 | 
					      try {
 | 
				
			||||||
      isApplying.value = false;
 | 
					        const client = AuthManager.createClient(OscilloscopeApiClient);
 | 
				
			||||||
      release();
 | 
					        const success = await client.initialize({ ...config });
 | 
				
			||||||
    }
 | 
					        if (success) {
 | 
				
			||||||
  };
 | 
					          alert.success("示波器配置已应用", 2000);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
  // 重置配置
 | 
					          throw new Error("应用失败");
 | 
				
			||||||
  const resetConfiguration = () => {
 | 
					        }
 | 
				
			||||||
    Object.assign(config, { ...DEFAULT_CONFIG });
 | 
					      } catch (error) {
 | 
				
			||||||
    alert.info("配置已重置", 2000);
 | 
					        alert.error("应用配置失败", 3000);
 | 
				
			||||||
  };
 | 
					      } finally {
 | 
				
			||||||
 | 
					        isApplying.value = false;
 | 
				
			||||||
  const clearOscilloscopeData = () => {
 | 
					        release();
 | 
				
			||||||
    oscData.value = undefined;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 获取数据
 | 
					 | 
				
			||||||
  const getOscilloscopeData = async () => {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
 | 
					 | 
				
			||||||
      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 resetConfiguration = () => {
 | 
				
			||||||
 | 
					      Object.assign(config, { ...DEFAULT_CONFIG });
 | 
				
			||||||
 | 
					      alert.info("配置已重置", 2000);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const clearOscilloscopeData = () => {
 | 
				
			||||||
 | 
					      oscData.value = undefined;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 获取数据
 | 
				
			||||||
 | 
					    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,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      } 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()) {
 | 
				
			||||||
 | 
					        alert.warn("有其他操作正在进行中,请稍后再试", 3000);
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      isCapturing.value = true;
 | 
				
			||||||
 | 
					      const release = await operationMutex.acquire();
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const client = AuthManager.createClient(OscilloscopeApiClient);
 | 
				
			||||||
 | 
					        const started = await client.startCapture();
 | 
				
			||||||
 | 
					        if (!started) throw new Error("无法启动捕获");
 | 
				
			||||||
 | 
					        alert.info("开始捕获...", 2000);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 启动定时刷新
 | 
				
			||||||
 | 
					        startAutoRefresh();
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        alert.error("捕获失败", 3000);
 | 
				
			||||||
 | 
					        isCapturing.value = false;
 | 
				
			||||||
 | 
					        stopAutoRefresh();
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        release();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 停止捕获
 | 
				
			||||||
 | 
					    const stopCapture = async () => {
 | 
				
			||||||
 | 
					      if (!isCapturing.value) {
 | 
				
			||||||
 | 
					        alert.warn("当前没有正在进行的捕获操作", 2000);
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      isCapturing.value = false;
 | 
				
			||||||
 | 
					      stopAutoRefresh();
 | 
				
			||||||
 | 
					      const release = await operationMutex.acquire();
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const client = AuthManager.createClient(OscilloscopeApiClient);
 | 
				
			||||||
 | 
					        const stopped = await client.stopCapture();
 | 
				
			||||||
 | 
					        if (!stopped) throw new Error("无法停止捕获");
 | 
				
			||||||
 | 
					        alert.info("捕获已停止", 2000);
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        alert.error("停止捕获失败", 3000);
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        release();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 更新触发参数
 | 
				
			||||||
 | 
					    const updateTrigger = async (level: number, risingEdge: boolean) => {
 | 
				
			||||||
 | 
					      const client = AuthManager.createClient(OscilloscopeApiClient);
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const ok = await client.updateTrigger(level, risingEdge);
 | 
				
			||||||
 | 
					        if (ok) {
 | 
				
			||||||
 | 
					          config.triggerLevel = level;
 | 
				
			||||||
 | 
					          config.triggerRisingEdge = risingEdge;
 | 
				
			||||||
 | 
					          alert.success("触发参数已更新", 2000);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          throw new Error();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch {
 | 
				
			||||||
 | 
					        alert.error("更新触发参数失败", 2000);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 更新采样参数
 | 
				
			||||||
 | 
					    const updateSampling = async (
 | 
				
			||||||
 | 
					      horizontalShift: number,
 | 
				
			||||||
 | 
					      decimationRate: number,
 | 
				
			||||||
 | 
					    ) => {
 | 
				
			||||||
 | 
					      const client = AuthManager.createClient(OscilloscopeApiClient);
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const ok = await client.updateSampling(horizontalShift, decimationRate);
 | 
				
			||||||
 | 
					        if (ok) {
 | 
				
			||||||
 | 
					          config.horizontalShift = horizontalShift;
 | 
				
			||||||
 | 
					          config.decimationRate = decimationRate;
 | 
				
			||||||
 | 
					          alert.success("采样参数已更新", 2000);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          throw new Error();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch {
 | 
				
			||||||
 | 
					        alert.error("更新采样参数失败", 2000);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 手动刷新RAM
 | 
				
			||||||
 | 
					    const refreshRAM = async () => {
 | 
				
			||||||
 | 
					      const client = AuthManager.createClient(OscilloscopeApiClient);
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const ok = await client.refreshRAM();
 | 
				
			||||||
 | 
					        if (ok) {
 | 
				
			||||||
 | 
					          // alert.success("RAM已刷新", 2000);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          throw new Error();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch {
 | 
				
			||||||
 | 
					        alert.error("刷新RAM失败", 2000);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 生成测试数据
 | 
				
			||||||
 | 
					    const generateTestData = () => {
 | 
				
			||||||
 | 
					      const freq = 5_000_000;
 | 
				
			||||||
 | 
					      const duration = 0.001; // 1ms
 | 
				
			||||||
 | 
					      const points = Math.floor(freq * duration);
 | 
				
			||||||
      const x = Array.from(
 | 
					      const x = Array.from(
 | 
				
			||||||
        { length: bytes.length },
 | 
					        { length: points },
 | 
				
			||||||
        (_, i) => (i * samplePeriodNs.value) / 1000 // us
 | 
					        (_, i) => (i * 1_000_000_000) / freq / 1000,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      const y = Array.from({ length: points }, (_, i) =>
 | 
				
			||||||
 | 
					        Math.floor(Math.sin(i * 0.01) * 127 + 128),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      const y = Array.from(bytes);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      oscData.value = {
 | 
					      oscData.value = {
 | 
				
			||||||
        x,
 | 
					        x,
 | 
				
			||||||
        y,
 | 
					        y,
 | 
				
			||||||
        xUnit: "us",
 | 
					        xUnit: "us",
 | 
				
			||||||
        yUnit: "V",
 | 
					        yUnit: "V",
 | 
				
			||||||
        adFrequency: resp.adFrequency,
 | 
					        adFrequency: freq,
 | 
				
			||||||
        adVpp: resp.adVpp,
 | 
					        adVpp: 2.0,
 | 
				
			||||||
        adMax: resp.adMax,
 | 
					        adMax: 255,
 | 
				
			||||||
        adMin: resp.adMin,
 | 
					        adMin: 0,
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
    } catch (error) {
 | 
					      alert.success("测试数据生成成功", 2000);
 | 
				
			||||||
      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()) {
 | 
					 | 
				
			||||||
      alert.warn("有其他操作正在进行中,请稍后再试", 3000);
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    isCapturing.value = true;
 | 
					 | 
				
			||||||
    const release = await operationMutex.acquire();
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
 | 
					 | 
				
			||||||
      const started = await client.startCapture();
 | 
					 | 
				
			||||||
      if (!started) throw new Error("无法启动捕获");
 | 
					 | 
				
			||||||
      alert.info("开始捕获...", 2000);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // 启动定时刷新
 | 
					 | 
				
			||||||
      startAutoRefresh();
 | 
					 | 
				
			||||||
    } catch (error) {
 | 
					 | 
				
			||||||
      alert.error("捕获失败", 3000);
 | 
					 | 
				
			||||||
      isCapturing.value = false;
 | 
					 | 
				
			||||||
      stopAutoRefresh();
 | 
					 | 
				
			||||||
    } finally {
 | 
					 | 
				
			||||||
      release();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 停止捕获
 | 
					 | 
				
			||||||
  const stopCapture = async () => {
 | 
					 | 
				
			||||||
    if (!isCapturing.value) {
 | 
					 | 
				
			||||||
      alert.warn("当前没有正在进行的捕获操作", 2000);
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    isCapturing.value = false;
 | 
					 | 
				
			||||||
    stopAutoRefresh();
 | 
					 | 
				
			||||||
    const release = await operationMutex.acquire();
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
 | 
					 | 
				
			||||||
      const stopped = await client.stopCapture();
 | 
					 | 
				
			||||||
      if (!stopped) throw new Error("无法停止捕获");
 | 
					 | 
				
			||||||
      alert.info("捕获已停止", 2000);
 | 
					 | 
				
			||||||
    } catch (error) {
 | 
					 | 
				
			||||||
      alert.error("停止捕获失败", 3000);
 | 
					 | 
				
			||||||
    } finally {
 | 
					 | 
				
			||||||
      release();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 更新触发参数
 | 
					 | 
				
			||||||
  const updateTrigger = async (level: number, risingEdge: boolean) => {
 | 
					 | 
				
			||||||
    const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const ok = await client.updateTrigger(level, risingEdge);
 | 
					 | 
				
			||||||
      if (ok) {
 | 
					 | 
				
			||||||
        config.triggerLevel = level;
 | 
					 | 
				
			||||||
        config.triggerRisingEdge = risingEdge;
 | 
					 | 
				
			||||||
        alert.success("触发参数已更新", 2000);
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        throw new Error();
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch {
 | 
					 | 
				
			||||||
      alert.error("更新触发参数失败", 2000);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 更新采样参数
 | 
					 | 
				
			||||||
  const updateSampling = async (horizontalShift: number, decimationRate: number) => {
 | 
					 | 
				
			||||||
    const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const ok = await client.updateSampling(horizontalShift, decimationRate);
 | 
					 | 
				
			||||||
      if (ok) {
 | 
					 | 
				
			||||||
        config.horizontalShift = horizontalShift;
 | 
					 | 
				
			||||||
        config.decimationRate = decimationRate;
 | 
					 | 
				
			||||||
        alert.success("采样参数已更新", 2000);
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        throw new Error();
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch {
 | 
					 | 
				
			||||||
      alert.error("更新采样参数失败", 2000);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 手动刷新RAM
 | 
					 | 
				
			||||||
  const refreshRAM = async () => {
 | 
					 | 
				
			||||||
    const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const ok = await client.refreshRAM();
 | 
					 | 
				
			||||||
      if (ok) {
 | 
					 | 
				
			||||||
        // alert.success("RAM已刷新", 2000);
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        throw new Error();
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch {
 | 
					 | 
				
			||||||
      alert.error("刷新RAM失败", 2000);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 生成测试数据
 | 
					 | 
				
			||||||
  const generateTestData = () => {
 | 
					 | 
				
			||||||
    const freq = 5_000_000;
 | 
					 | 
				
			||||||
    const duration = 0.001; // 1ms
 | 
					 | 
				
			||||||
    const points = Math.floor(freq * duration);
 | 
					 | 
				
			||||||
    const x = Array.from({ length: points }, (_, i) => (i * 1_000_000_000 / freq) / 1000);
 | 
					 | 
				
			||||||
    const y = Array.from({ length: points }, (_, i) =>
 | 
					 | 
				
			||||||
      Math.floor(Math.sin(i * 0.01) * 127 + 128)
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    oscData.value = {
 | 
					 | 
				
			||||||
      x,
 | 
					 | 
				
			||||||
      y,
 | 
					 | 
				
			||||||
      xUnit: "us",
 | 
					 | 
				
			||||||
      yUnit: "V",
 | 
					 | 
				
			||||||
      adFrequency: freq,
 | 
					 | 
				
			||||||
      adVpp: 2.0,
 | 
					 | 
				
			||||||
      adMax: 255,
 | 
					 | 
				
			||||||
      adMin: 0,
 | 
					 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    alert.success("测试数据生成成功", 2000);
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return {
 | 
					    return {
 | 
				
			||||||
    oscData,
 | 
					      oscData,
 | 
				
			||||||
    config,
 | 
					      config,
 | 
				
			||||||
    isApplying,
 | 
					      isApplying,
 | 
				
			||||||
    isCapturing,
 | 
					      isCapturing,
 | 
				
			||||||
    sampleCount,
 | 
					      sampleCount,
 | 
				
			||||||
    samplePeriodNs,
 | 
					      samplePeriodNs,
 | 
				
			||||||
    refreshIntervalMs,
 | 
					      refreshIntervalMs,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    applyConfiguration,
 | 
					      applyConfiguration,
 | 
				
			||||||
    resetConfiguration,
 | 
					      resetConfiguration,
 | 
				
			||||||
    clearOscilloscopeData,
 | 
					      clearOscilloscopeData,
 | 
				
			||||||
    getOscilloscopeData,
 | 
					      getOscilloscopeData,
 | 
				
			||||||
    startCapture,
 | 
					      startCapture,
 | 
				
			||||||
    stopCapture,
 | 
					      stopCapture,
 | 
				
			||||||
    updateTrigger,
 | 
					      updateTrigger,
 | 
				
			||||||
    updateSampling,
 | 
					      updateSampling,
 | 
				
			||||||
    refreshRAM,
 | 
					      refreshRAM,
 | 
				
			||||||
    generateTestData,
 | 
					      generateTestData,
 | 
				
			||||||
  };
 | 
					    };
 | 
				
			||||||
});
 | 
					  },
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export { useProvideOscilloscope, useOscilloscopeState, DEFAULT_CONFIG };
 | 
					export { useProvideOscilloscope, useOscilloscopeState, DEFAULT_CONFIG };
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -81,7 +81,7 @@
 | 
				
			|||||||
import { ref, onMounted, onUnmounted } from "vue";
 | 
					import { ref, onMounted, onUnmounted } from "vue";
 | 
				
			||||||
import { useRouter } from "vue-router";
 | 
					import { useRouter } from "vue-router";
 | 
				
			||||||
import { AuthManager } from "@/utils/AuthManager";
 | 
					import { AuthManager } from "@/utils/AuthManager";
 | 
				
			||||||
import type { ExamInfo } from "@/APIClient";
 | 
					import { ExamClient, ResourceClient, type ExamInfo } from "@/APIClient";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 接口定义
 | 
					// 接口定义
 | 
				
			||||||
interface Tutorial {
 | 
					interface Tutorial {
 | 
				
			||||||
@@ -121,7 +121,7 @@ onMounted(async () => {
 | 
				
			|||||||
    console.log("正在从数据库加载实验数据...");
 | 
					    console.log("正在从数据库加载实验数据...");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // 创建认证客户端
 | 
					    // 创建认证客户端
 | 
				
			||||||
    const client = AuthManager.createAuthenticatedExamClient();
 | 
					    const client = AuthManager.createClient(ExamClient);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // 获取实验列表
 | 
					    // 获取实验列表
 | 
				
			||||||
    const examList: ExamInfo[] = await client.getExamList();
 | 
					    const examList: ExamInfo[] = await client.getExamList();
 | 
				
			||||||
@@ -142,7 +142,7 @@ onMounted(async () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        // 获取实验的封面资源(模板资源)
 | 
					        // 获取实验的封面资源(模板资源)
 | 
				
			||||||
        const resourceClient = AuthManager.createAuthenticatedResourceClient();
 | 
					        const resourceClient = AuthManager.createClient(ResourceClient);
 | 
				
			||||||
        const resourceList = await resourceClient.getResourceList(
 | 
					        const resourceList = await resourceClient.getResourceList(
 | 
				
			||||||
          exam.id,
 | 
					          exam.id,
 | 
				
			||||||
          "cover",
 | 
					          "cover",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,22 +16,32 @@
 | 
				
			|||||||
            <span class="text-sm">{{ bitstream.name }}</span>
 | 
					            <span class="text-sm">{{ bitstream.name }}</span>
 | 
				
			||||||
            <div class="flex gap-2">
 | 
					            <div class="flex gap-2">
 | 
				
			||||||
              <button
 | 
					              <button
 | 
				
			||||||
                @click="downloadExampleBitstream(bitstream)"
 | 
					                @click="handleExampleBitstream('download', bitstream)"
 | 
				
			||||||
                class="btn btn-sm btn-secondary"
 | 
					                class="btn btn-sm btn-secondary"
 | 
				
			||||||
                :disabled="isDownloading || isProgramming"
 | 
					                :disabled="currentTask !== 'none'"
 | 
				
			||||||
              >
 | 
					              >
 | 
				
			||||||
                <div v-if="isDownloading">
 | 
					                <div
 | 
				
			||||||
 | 
					                  v-if="
 | 
				
			||||||
 | 
					                    currentTask === 'downloading' &&
 | 
				
			||||||
 | 
					                    currentBitstreamId === bitstream.id
 | 
				
			||||||
 | 
					                  "
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
                  <span class="loading loading-spinner loading-xs"></span>
 | 
					                  <span class="loading loading-spinner loading-xs"></span>
 | 
				
			||||||
                  {{ downloadProgress }}%
 | 
					                  下载中...
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <div v-else>下载示例</div>
 | 
					                <div v-else>下载示例</div>
 | 
				
			||||||
              </button>
 | 
					              </button>
 | 
				
			||||||
              <button
 | 
					              <button
 | 
				
			||||||
                @click="programExampleBitstream(bitstream)"
 | 
					                @click="handleExampleBitstream('program', bitstream)"
 | 
				
			||||||
                class="btn btn-sm btn-primary"
 | 
					                class="btn btn-sm btn-primary"
 | 
				
			||||||
                :disabled="isDownloading || isProgramming"
 | 
					                :disabled="currentTask !== 'none'"
 | 
				
			||||||
              >
 | 
					              >
 | 
				
			||||||
                <div v-if="isProgramming">
 | 
					                <div
 | 
				
			||||||
 | 
					                  v-if="
 | 
				
			||||||
 | 
					                    currentTask === 'programming' &&
 | 
				
			||||||
 | 
					                    currentBitstreamId === bitstream.id
 | 
				
			||||||
 | 
					                  "
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
                  <span class="loading loading-spinner loading-xs"></span>
 | 
					                  <span class="loading loading-spinner loading-xs"></span>
 | 
				
			||||||
                  烧录中...
 | 
					                  烧录中...
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
@@ -63,14 +73,18 @@
 | 
				
			|||||||
    <!-- Upload Button -->
 | 
					    <!-- Upload Button -->
 | 
				
			||||||
    <div class="card-actions w-full">
 | 
					    <div class="card-actions w-full">
 | 
				
			||||||
      <button
 | 
					      <button
 | 
				
			||||||
        @click="handleClick"
 | 
					        @click="handleUploadAndDownload"
 | 
				
			||||||
        class="btn btn-primary grow"
 | 
					        class="btn btn-primary grow"
 | 
				
			||||||
        :disabled="isUploading || isProgramming"
 | 
					        :disabled="currentTask !== 'none'"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <div v-if="isUploading">
 | 
					        <div v-if="currentTask === 'uploading'">
 | 
				
			||||||
          <span class="loading loading-spinner"></span>
 | 
					          <span class="loading loading-spinner"></span>
 | 
				
			||||||
          上传中...
 | 
					          上传中...
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div v-else-if="currentTask === 'programming'">
 | 
				
			||||||
 | 
					          <span class="loading loading-spinner"></span>
 | 
				
			||||||
 | 
					          {{ currentProgressPercent }}% ...
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
        <div v-else>上传并下载</div>
 | 
					        <div v-else>上传并下载</div>
 | 
				
			||||||
      </button>
 | 
					      </button>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
@@ -78,27 +92,19 @@
 | 
				
			|||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts" setup>
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { computed, ref, useTemplateRef, onMounted } from "vue";
 | 
					import { ref, useTemplateRef, onMounted } from "vue";
 | 
				
			||||||
import { AuthManager } from "@/utils/AuthManager"; // Adjust the path based on your project structure
 | 
					import { AuthManager } from "@/utils/AuthManager";
 | 
				
			||||||
import { useDialogStore } from "@/stores/dialog";
 | 
					import { useDialogStore } from "@/stores/dialog";
 | 
				
			||||||
import { isNull, isUndefined } from "lodash";
 | 
					 | 
				
			||||||
import { useEquipments } from "@/stores/equipments";
 | 
					import { useEquipments } from "@/stores/equipments";
 | 
				
			||||||
import type { HubConnection } from "@microsoft/signalr";
 | 
					 | 
				
			||||||
import type {
 | 
					 | 
				
			||||||
  IProgressHub,
 | 
					 | 
				
			||||||
  IProgressReceiver,
 | 
					 | 
				
			||||||
} from "@/utils/signalR/TypedSignalR.Client/server.Hubs";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  getHubProxyFactory,
 | 
					 | 
				
			||||||
  getReceiverRegister,
 | 
					 | 
				
			||||||
} from "@/utils/signalR/TypedSignalR.Client";
 | 
					 | 
				
			||||||
import { ProgressStatus } from "@/utils/signalR/server.Hubs";
 | 
					 | 
				
			||||||
import { useRequiredInjection } from "@/utils/Common";
 | 
					import { useRequiredInjection } from "@/utils/Common";
 | 
				
			||||||
import { useAlertStore } from "./Alert";
 | 
					import { useAlertStore } from "./Alert";
 | 
				
			||||||
 | 
					import { ResourceClient, ResourcePurpose } from "@/APIClient";
 | 
				
			||||||
 | 
					import { useProgressStore } from "@/stores/progress";
 | 
				
			||||||
 | 
					import { ProgressStatus, type ProgressInfo } from "@/utils/signalR/server.Hubs";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface Props {
 | 
					interface Props {
 | 
				
			||||||
  maxMemory?: number;
 | 
					  maxMemory?: number;
 | 
				
			||||||
  examId?: string; // 新增examId属性
 | 
					  examId?: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const props = withDefaults(defineProps<Props>(), {
 | 
					const props = withDefaults(defineProps<Props>(), {
 | 
				
			||||||
@@ -111,203 +117,166 @@ const emits = defineEmits<{
 | 
				
			|||||||
}>();
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const alert = useRequiredInjection(useAlertStore);
 | 
					const alert = useRequiredInjection(useAlertStore);
 | 
				
			||||||
 | 
					const progressTracker = useProgressStore();
 | 
				
			||||||
const dialog = useDialogStore();
 | 
					const dialog = useDialogStore();
 | 
				
			||||||
const eqps = useEquipments();
 | 
					const eqps = useEquipments();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const isUploading = ref(false);
 | 
					const availableBitstreams = ref<{ id: string; name: string }[]>([]);
 | 
				
			||||||
const isDownloading = ref(false);
 | 
					 | 
				
			||||||
const isProgramming = ref(false);
 | 
					 | 
				
			||||||
const availableBitstreams = ref<{ id: number; name: string }[]>([]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Progress
 | 
					 | 
				
			||||||
const downloadTaskId = ref("");
 | 
					 | 
				
			||||||
const downloadProgress = ref(0);
 | 
					 | 
				
			||||||
const progressHubConnection = ref<HubConnection>();
 | 
					 | 
				
			||||||
const progressHubProxy = ref<IProgressHub>();
 | 
					 | 
				
			||||||
const progressHubReceiver: IProgressReceiver = {
 | 
					 | 
				
			||||||
  onReceiveProgress: async (msg) => {
 | 
					 | 
				
			||||||
    if (msg.taskId == downloadTaskId.value) {
 | 
					 | 
				
			||||||
      if (msg.status == ProgressStatus.InProgress) {
 | 
					 | 
				
			||||||
        downloadProgress.value = msg.progressPercent;
 | 
					 | 
				
			||||||
      } else if (msg.status == ProgressStatus.Failed) {
 | 
					 | 
				
			||||||
        dialog.error(msg.errorMessage);
 | 
					 | 
				
			||||||
      } else if (msg.status == ProgressStatus.Completed) {
 | 
					 | 
				
			||||||
        alert.info("比特流下载成功");
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
onMounted(async () => {
 | 
					 | 
				
			||||||
  progressHubConnection.value =
 | 
					 | 
				
			||||||
    AuthManager.createAuthenticatedProgressHubConnection();
 | 
					 | 
				
			||||||
  progressHubProxy.value = getHubProxyFactory("IProgressHub").createHubProxy(
 | 
					 | 
				
			||||||
    progressHubConnection.value,
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
  getReceiverRegister("IProgressReceiver").register(
 | 
					 | 
				
			||||||
    progressHubConnection.value,
 | 
					 | 
				
			||||||
    progressHubReceiver,
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const fileInput = useTemplateRef("fileInput");
 | 
					const fileInput = useTemplateRef("fileInput");
 | 
				
			||||||
const bitstream = defineModel("bitstreamFile", {
 | 
					const bitstream = ref<File | undefined>(undefined);
 | 
				
			||||||
  type: File,
 | 
					
 | 
				
			||||||
  default: undefined,
 | 
					// 用一个状态变量替代多个
 | 
				
			||||||
});
 | 
					const currentTask = ref<"none" | "uploading" | "downloading" | "programming">(
 | 
				
			||||||
 | 
					  "none",
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					const currentBitstreamId = ref<string>("");
 | 
				
			||||||
 | 
					const currentProgressId = ref<string>("");
 | 
				
			||||||
 | 
					const currentProgressPercent = ref<number>(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 初始化时加载示例比特流
 | 
					 | 
				
			||||||
onMounted(async () => {
 | 
					onMounted(async () => {
 | 
				
			||||||
  if (!isUndefined(bitstream.value) && !isNull(fileInput.value)) {
 | 
					  if (bitstream.value && fileInput.value) {
 | 
				
			||||||
    let fileList = new DataTransfer();
 | 
					    let fileList = new DataTransfer();
 | 
				
			||||||
    fileList.items.add(bitstream.value);
 | 
					    fileList.items.add(bitstream.value);
 | 
				
			||||||
    fileInput.value.files = fileList.files;
 | 
					    fileInput.value.files = fileList.files;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  await loadAvailableBitstreams();
 | 
					  await loadAvailableBitstreams();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 加载可用的比特流文件列表
 | 
					 | 
				
			||||||
async function loadAvailableBitstreams() {
 | 
					 | 
				
			||||||
  console.log("加载可用比特流文件,examId:", props.examId);
 | 
					 | 
				
			||||||
  if (!props.examId) {
 | 
					 | 
				
			||||||
    availableBitstreams.value = [];
 | 
					 | 
				
			||||||
    return;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const resourceClient = AuthManager.createAuthenticatedResourceClient();
 | 
					 | 
				
			||||||
    // 使用新的ResourceClient API获取比特流模板资源列表
 | 
					 | 
				
			||||||
    const resources = await resourceClient.getResourceList(
 | 
					 | 
				
			||||||
      props.examId,
 | 
					 | 
				
			||||||
      "bitstream",
 | 
					 | 
				
			||||||
      "template",
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    availableBitstreams.value =
 | 
					 | 
				
			||||||
      resources.map((r) => ({ id: r.id, name: r.name })) || [];
 | 
					 | 
				
			||||||
  } catch (error) {
 | 
					 | 
				
			||||||
    console.error("加载比特流列表失败:", error);
 | 
					 | 
				
			||||||
    availableBitstreams.value = [];
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 下载示例比特流
 | 
					 | 
				
			||||||
async function downloadExampleBitstream(bitstream: {
 | 
					 | 
				
			||||||
  id: number;
 | 
					 | 
				
			||||||
  name: string;
 | 
					 | 
				
			||||||
}) {
 | 
					 | 
				
			||||||
  if (isDownloading.value) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  isDownloading.value = true;
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const resourceClient = AuthManager.createAuthenticatedResourceClient();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // 使用新的ResourceClient API获取资源文件
 | 
					 | 
				
			||||||
    const response = await resourceClient.getResourceById(bitstream.id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (response && response.data) {
 | 
					 | 
				
			||||||
      // 创建下载链接
 | 
					 | 
				
			||||||
      const url = URL.createObjectURL(response.data);
 | 
					 | 
				
			||||||
      const link = document.createElement("a");
 | 
					 | 
				
			||||||
      link.href = url;
 | 
					 | 
				
			||||||
      link.download = response.fileName || bitstream.name;
 | 
					 | 
				
			||||||
      document.body.appendChild(link);
 | 
					 | 
				
			||||||
      link.click();
 | 
					 | 
				
			||||||
      document.body.removeChild(link);
 | 
					 | 
				
			||||||
      URL.revokeObjectURL(url);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      dialog.info("示例比特流下载成功");
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      dialog.error("下载失败:响应数据为空");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  } catch (error) {
 | 
					 | 
				
			||||||
    console.error("下载示例比特流失败:", error);
 | 
					 | 
				
			||||||
    dialog.error("下载示例比特流失败");
 | 
					 | 
				
			||||||
  } finally {
 | 
					 | 
				
			||||||
    isDownloading.value = false;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 直接烧录示例比特流
 | 
					 | 
				
			||||||
async function programExampleBitstream(bitstream: {
 | 
					 | 
				
			||||||
  id: number;
 | 
					 | 
				
			||||||
  name: string;
 | 
					 | 
				
			||||||
}) {
 | 
					 | 
				
			||||||
  if (isProgramming.value) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  isProgramming.value = true;
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const downloadTaskId = await eqps.jtagDownloadBitstream(bitstream.id);
 | 
					 | 
				
			||||||
  } catch (error) {
 | 
					 | 
				
			||||||
    console.error("烧录示例比特流失败:", error);
 | 
					 | 
				
			||||||
    dialog.error("烧录示例比特流失败");
 | 
					 | 
				
			||||||
  } finally {
 | 
					 | 
				
			||||||
    isProgramming.value = false;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function handleFileChange(event: Event): void {
 | 
					function handleFileChange(event: Event): void {
 | 
				
			||||||
  const target = event.target as HTMLInputElement;
 | 
					  const target = event.target as HTMLInputElement;
 | 
				
			||||||
  const file = target.files?.[0]; // 获取选中的第一个文件
 | 
					  const file = target.files?.[0];
 | 
				
			||||||
 | 
					  bitstream.value = file || undefined;
 | 
				
			||||||
  if (!file) {
 | 
					 | 
				
			||||||
    return;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  bitstream.value = file;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function checkFile(file: File): boolean {
 | 
					function checkFileInput(): boolean {
 | 
				
			||||||
  const maxBytes = props.maxMemory! * 1024 * 1024; // 将最大容量从 MB 转换为字节
 | 
					  if (!bitstream.value) {
 | 
				
			||||||
  if (file.size > maxBytes) {
 | 
					    dialog.error(`未选择文件`);
 | 
				
			||||||
 | 
					    return false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  const maxBytes = props.maxMemory! * 1024 * 1024;
 | 
				
			||||||
 | 
					  if (bitstream.value.size > maxBytes) {
 | 
				
			||||||
    dialog.error(`文件大小超过最大限制: ${props.maxMemory}MB`);
 | 
					    dialog.error(`文件大小超过最大限制: ${props.maxMemory}MB`);
 | 
				
			||||||
    return false;
 | 
					    return false;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  return true;
 | 
					  return true;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function handleClick(event: Event): Promise<void> {
 | 
					async function downloadBitstream() {
 | 
				
			||||||
  console.log("上传按钮被点击");
 | 
					  currentTask.value = "programming";
 | 
				
			||||||
  if (isNull(bitstream.value) || isUndefined(bitstream.value)) {
 | 
					  try {
 | 
				
			||||||
    dialog.error(`未选择文件`);
 | 
					    currentProgressId.value = await eqps.jtagDownloadBitstream(
 | 
				
			||||||
 | 
					      currentBitstreamId.value,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    progressTracker.register(
 | 
				
			||||||
 | 
					      currentProgressId.value,
 | 
				
			||||||
 | 
					      "programBitstream",
 | 
				
			||||||
 | 
					      handleProgressUpdate,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  } catch {
 | 
				
			||||||
 | 
					    dialog.error("比特流烧录失败");
 | 
				
			||||||
 | 
					    cleanProgressTracker();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function cleanProgressTracker() {
 | 
				
			||||||
 | 
					  currentTask.value = "none";
 | 
				
			||||||
 | 
					  currentProgressId.value = "";
 | 
				
			||||||
 | 
					  currentBitstreamId.value = "";
 | 
				
			||||||
 | 
					  currentProgressPercent.value = 0;
 | 
				
			||||||
 | 
					  progressTracker.unregister(currentProgressId.value, "programBitstream");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function loadAvailableBitstreams() {
 | 
				
			||||||
 | 
					  if (!props.examId) {
 | 
				
			||||||
 | 
					    availableBitstreams.value = [];
 | 
				
			||||||
    return;
 | 
					    return;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!checkFile(bitstream.value)) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  isUploading.value = true;
 | 
					 | 
				
			||||||
  let uploadedBitstreamId: number | null = null;
 | 
					 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    console.log("开始上传比特流文件:", bitstream.value.name);
 | 
					    const resourceClient = AuthManager.createClient(ResourceClient);
 | 
				
			||||||
    const bitstreamId = await eqps.jtagUploadBitstream(
 | 
					    const resources = await resourceClient.getResourceList(
 | 
				
			||||||
      bitstream.value,
 | 
					      props.examId,
 | 
				
			||||||
 | 
					      "bitstream",
 | 
				
			||||||
 | 
					      ResourcePurpose.Template,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    availableBitstreams.value =
 | 
				
			||||||
 | 
					      resources.map((r) => ({ id: r.id, name: r.name })) || [];
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    availableBitstreams.value = [];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 统一处理示例比特流的下载/烧录
 | 
				
			||||||
 | 
					async function handleExampleBitstream(
 | 
				
			||||||
 | 
					  action: "download" | "program",
 | 
				
			||||||
 | 
					  bitstreamObj: { id: string; name: string },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  if (currentTask.value !== "none") return;
 | 
				
			||||||
 | 
					  currentBitstreamId.value = bitstreamObj.id;
 | 
				
			||||||
 | 
					  if (action === "download") {
 | 
				
			||||||
 | 
					    currentTask.value = "downloading";
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const resourceClient = AuthManager.createClient(ResourceClient);
 | 
				
			||||||
 | 
					      const response = await resourceClient.getResourceById(bitstreamObj.id);
 | 
				
			||||||
 | 
					      if (response && response.data) {
 | 
				
			||||||
 | 
					        const url = URL.createObjectURL(response.data);
 | 
				
			||||||
 | 
					        const link = document.createElement("a");
 | 
				
			||||||
 | 
					        link.href = url;
 | 
				
			||||||
 | 
					        link.download = response.fileName || bitstreamObj.name;
 | 
				
			||||||
 | 
					        document.body.appendChild(link);
 | 
				
			||||||
 | 
					        link.click();
 | 
				
			||||||
 | 
					        document.body.removeChild(link);
 | 
				
			||||||
 | 
					        URL.revokeObjectURL(url);
 | 
				
			||||||
 | 
					        alert.info("示例比特流下载成功");
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        alert.error("下载失败:响应数据为空");
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch {
 | 
				
			||||||
 | 
					      alert.error("下载示例比特流失败");
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      currentTask.value = "none";
 | 
				
			||||||
 | 
					      currentBitstreamId.value = "";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } else if (action === "program") {
 | 
				
			||||||
 | 
					    currentBitstreamId.value = bitstreamObj.id;
 | 
				
			||||||
 | 
					    await downloadBitstream();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 上传并下载
 | 
				
			||||||
 | 
					async function handleUploadAndDownload() {
 | 
				
			||||||
 | 
					  if (currentTask.value !== "none") return;
 | 
				
			||||||
 | 
					  if (!checkFileInput()) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  currentTask.value = "uploading";
 | 
				
			||||||
 | 
					  let uploadedBitstreamId: string | null = null;
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    uploadedBitstreamId = await eqps.jtagUploadBitstream(
 | 
				
			||||||
 | 
					      bitstream.value!,
 | 
				
			||||||
      props.examId || "",
 | 
					      props.examId || "",
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    console.log("上传结果,ID:", bitstreamId);
 | 
					    if (!uploadedBitstreamId) throw new Error("上传失败");
 | 
				
			||||||
    if (bitstreamId === null || bitstreamId === undefined) {
 | 
					    emits("finishedUpload", bitstream.value!);
 | 
				
			||||||
      isUploading.value = false;
 | 
					  } catch {
 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    uploadedBitstreamId = bitstreamId;
 | 
					 | 
				
			||||||
  } catch (e) {
 | 
					 | 
				
			||||||
    dialog.error("上传失败");
 | 
					    dialog.error("上传失败");
 | 
				
			||||||
    console.error(e);
 | 
					    currentTask.value = "none";
 | 
				
			||||||
    return;
 | 
					    return;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  isUploading.value = false;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Download
 | 
					  currentBitstreamId.value = uploadedBitstreamId;
 | 
				
			||||||
  try {
 | 
					
 | 
				
			||||||
    console.log("开始下载比特流,ID:", uploadedBitstreamId);
 | 
					  await downloadBitstream();
 | 
				
			||||||
    if (uploadedBitstreamId === null || uploadedBitstreamId === undefined) {
 | 
					}
 | 
				
			||||||
      dialog.error("uploadedBitstreamId is null or undefined");
 | 
					
 | 
				
			||||||
    } else {
 | 
					function handleProgressUpdate(msg: ProgressInfo) {
 | 
				
			||||||
      isDownloading.value = true;
 | 
					  // console.log(msg);
 | 
				
			||||||
      downloadTaskId.value =
 | 
					  if (msg.status === ProgressStatus.Running)
 | 
				
			||||||
        await eqps.jtagDownloadBitstream(uploadedBitstreamId);
 | 
					    currentProgressPercent.value = msg.progressPercent;
 | 
				
			||||||
    }
 | 
					  else if (msg.status === ProgressStatus.Failed) {
 | 
				
			||||||
  } catch (e) {
 | 
					    dialog.error(`比特流烧录失败: ${msg.errorMessage}`);
 | 
				
			||||||
    dialog.error("下载失败");
 | 
					    cleanProgressTracker();
 | 
				
			||||||
    console.error(e);
 | 
					  } else if (msg.status === ProgressStatus.Completed) {
 | 
				
			||||||
 | 
					    dialog.info("比特流烧录成功");
 | 
				
			||||||
 | 
					    cleanProgressTracker();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -212,6 +212,7 @@ import { useEquipments } from "@/stores/equipments";
 | 
				
			|||||||
import { useDialogStore } from "@/stores/dialog";
 | 
					import { useDialogStore } from "@/stores/dialog";
 | 
				
			||||||
import { toInteger } from "lodash";
 | 
					import { toInteger } from "lodash";
 | 
				
			||||||
import { AuthManager } from "@/utils/AuthManager";
 | 
					import { AuthManager } from "@/utils/AuthManager";
 | 
				
			||||||
 | 
					import { DDSClient } from "@/APIClient";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Component Attributes
 | 
					// Component Attributes
 | 
				
			||||||
const props = defineProps<{
 | 
					const props = defineProps<{
 | 
				
			||||||
@@ -221,7 +222,7 @@ const props = defineProps<{
 | 
				
			|||||||
const emit = defineEmits(["update:modelValue"]);
 | 
					const emit = defineEmits(["update:modelValue"]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Global varibles
 | 
					// Global varibles
 | 
				
			||||||
const dds = AuthManager.createAuthenticatedDDSClient();
 | 
					const dds = AuthManager.createClient(DDSClient);
 | 
				
			||||||
const eqps = useEquipments();
 | 
					const eqps = useEquipments();
 | 
				
			||||||
const dialog = useDialogStore();
 | 
					const dialog = useDialogStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,65 +1,114 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div class="seven-segment-display" :style="{
 | 
					  <div
 | 
				
			||||||
    width: width + 'px',
 | 
					    class="seven-segment-display"
 | 
				
			||||||
    height: height + 'px',
 | 
					    :style="{
 | 
				
			||||||
    position: 'relative',
 | 
					      width: width + 'px',
 | 
				
			||||||
  }">
 | 
					      height: height + 'px',
 | 
				
			||||||
    <svg xmlns="http://www.w3.org/2000/svg" :width="width" :height="height" viewBox="0 0 120 220" class="display">
 | 
					      position: 'relative',
 | 
				
			||||||
 | 
					    }"
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <svg
 | 
				
			||||||
 | 
					      xmlns="http://www.w3.org/2000/svg"
 | 
				
			||||||
 | 
					      :width="width"
 | 
				
			||||||
 | 
					      :height="height"
 | 
				
			||||||
 | 
					      viewBox="0 0 120 220"
 | 
				
			||||||
 | 
					      class="display"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
      <!-- 数码管基座 -->
 | 
					      <!-- 数码管基座 -->
 | 
				
			||||||
      <rect width="120" height="180" x="0" y="0" fill="#222" rx="10" ry="10" />
 | 
					      <rect width="120" height="180" x="0" y="0" fill="#222" rx="10" ry="10" />
 | 
				
			||||||
      <rect width="110" height="170" x="5" y="5" fill="#333" rx="5" ry="5" />
 | 
					      <rect width="110" height="170" x="5" y="5" fill="#333" rx="5" ry="5" />
 | 
				
			||||||
      <!-- 7段 + 小数点,每个段由多边形表示,重新设计点位置使其更接近实际数码管 -->
 | 
					      <!-- 7段 + 小数点,每个段由多边形表示,重新设计点位置使其更接近实际数码管 -->
 | 
				
			||||||
      <!-- a段 (顶部横线) -->
 | 
					      <!-- a段 (顶部横线) -->
 | 
				
			||||||
      <polygon :points="'30,20 90,20 98,28 82,36 38,36 22,28'"
 | 
					      <polygon
 | 
				
			||||||
 | 
					        :points="'30,20 90,20 98,28 82,36 38,36 22,28'"
 | 
				
			||||||
        :fill="isSegmentActive('a') ? segmentColor : inactiveColor"
 | 
					        :fill="isSegmentActive('a') ? segmentColor : inactiveColor"
 | 
				
			||||||
        :style="{ opacity: isSegmentActive('a') ? 1 : 0.15 }" class="segment" />
 | 
					        :style="{ opacity: isSegmentActive('a') ? 1 : 0.15 }"
 | 
				
			||||||
 | 
					        class="segment"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <!-- b段 (右上竖线) -->
 | 
					      <!-- b段 (右上竖线) -->
 | 
				
			||||||
      <polygon :points="'100,30 108,38 108,82 100,90 92,82 92,38'"
 | 
					      <polygon
 | 
				
			||||||
 | 
					        :points="'100,30 108,38 108,82 100,90 92,82 92,38'"
 | 
				
			||||||
        :fill="isSegmentActive('b') ? segmentColor : inactiveColor"
 | 
					        :fill="isSegmentActive('b') ? segmentColor : inactiveColor"
 | 
				
			||||||
        :style="{ opacity: isSegmentActive('b') ? 1 : 0.15 }" class="segment" />
 | 
					        :style="{ opacity: isSegmentActive('b') ? 1 : 0.15 }"
 | 
				
			||||||
 | 
					        class="segment"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <!-- c段 (右下竖线) -->
 | 
					      <!-- c段 (右下竖线) -->
 | 
				
			||||||
      <polygon :points="'100,90 108,98 108,142 100,150 92,142 92,98'"
 | 
					      <polygon
 | 
				
			||||||
 | 
					        :points="'100,90 108,98 108,142 100,150 92,142 92,98'"
 | 
				
			||||||
        :fill="isSegmentActive('c') ? segmentColor : inactiveColor"
 | 
					        :fill="isSegmentActive('c') ? segmentColor : inactiveColor"
 | 
				
			||||||
        :style="{ opacity: isSegmentActive('c') ? 1 : 0.15 }" class="segment" />
 | 
					        :style="{ opacity: isSegmentActive('c') ? 1 : 0.15 }"
 | 
				
			||||||
 | 
					        class="segment"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <!-- d段 (底部横线) -->
 | 
					      <!-- d段 (底部横线) -->
 | 
				
			||||||
      <polygon :points="'30,160 90,160 98,152 82,144 38,144 22,152'"
 | 
					      <polygon
 | 
				
			||||||
 | 
					        :points="'30,160 90,160 98,152 82,144 38,144 22,152'"
 | 
				
			||||||
        :fill="isSegmentActive('d') ? segmentColor : inactiveColor"
 | 
					        :fill="isSegmentActive('d') ? segmentColor : inactiveColor"
 | 
				
			||||||
        :style="{ opacity: isSegmentActive('d') ? 1 : 0.15 }" class="segment" />
 | 
					        :style="{ opacity: isSegmentActive('d') ? 1 : 0.15 }"
 | 
				
			||||||
 | 
					        class="segment"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <!-- e段 (左下竖线) -->
 | 
					      <!-- e段 (左下竖线) -->
 | 
				
			||||||
      <polygon :points="'20,90 28,98 28,142 20,150 12,142 12,98'"
 | 
					      <polygon
 | 
				
			||||||
 | 
					        :points="'20,90 28,98 28,142 20,150 12,142 12,98'"
 | 
				
			||||||
        :fill="isSegmentActive('e') ? segmentColor : inactiveColor"
 | 
					        :fill="isSegmentActive('e') ? segmentColor : inactiveColor"
 | 
				
			||||||
        :style="{ opacity: isSegmentActive('e') ? 1 : 0.15 }" class="segment" />
 | 
					        :style="{ opacity: isSegmentActive('e') ? 1 : 0.15 }"
 | 
				
			||||||
 | 
					        class="segment"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <!-- f段 (左上竖线) -->
 | 
					      <!-- f段 (左上竖线) -->
 | 
				
			||||||
      <polygon :points="'20,30 28,38 28,82 20,90 12,82 12,38'"
 | 
					      <polygon
 | 
				
			||||||
 | 
					        :points="'20,30 28,38 28,82 20,90 12,82 12,38'"
 | 
				
			||||||
        :fill="isSegmentActive('f') ? segmentColor : inactiveColor"
 | 
					        :fill="isSegmentActive('f') ? segmentColor : inactiveColor"
 | 
				
			||||||
        :style="{ opacity: isSegmentActive('f') ? 1 : 0.15 }" class="segment" />
 | 
					        :style="{ opacity: isSegmentActive('f') ? 1 : 0.15 }"
 | 
				
			||||||
 | 
					        class="segment"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <!-- g段 (中间横线) -->
 | 
					      <!-- g段 (中间横线) -->
 | 
				
			||||||
      <polygon :points="'30,90 38,82 82,82 90,90 82,98 38,98'"
 | 
					      <polygon
 | 
				
			||||||
 | 
					        :points="'30,90 38,82 82,82 90,90 82,98 38,98'"
 | 
				
			||||||
        :fill="isSegmentActive('g') ? segmentColor : inactiveColor"
 | 
					        :fill="isSegmentActive('g') ? segmentColor : inactiveColor"
 | 
				
			||||||
        :style="{ opacity: isSegmentActive('g') ? 1 : 0.15 }" class="segment" />
 | 
					        :style="{ opacity: isSegmentActive('g') ? 1 : 0.15 }"
 | 
				
			||||||
 | 
					        class="segment"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
      <!-- dp段 (小数点) -->
 | 
					      <!-- dp段 (小数点) -->
 | 
				
			||||||
      <circle cx="108" cy="154" r="6" :fill="isSegmentActive('dp') ? segmentColor : inactiveColor"
 | 
					      <circle
 | 
				
			||||||
        :style="{ opacity: isSegmentActive('dp') ? 1 : 0.15 }" class="segment" />
 | 
					        cx="108"
 | 
				
			||||||
 | 
					        cy="154"
 | 
				
			||||||
 | 
					        r="6"
 | 
				
			||||||
 | 
					        :fill="isSegmentActive('dp') ? segmentColor : inactiveColor"
 | 
				
			||||||
 | 
					        :style="{ opacity: isSegmentActive('dp') ? 1 : 0.15 }"
 | 
				
			||||||
 | 
					        class="segment"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
    </svg>
 | 
					    </svg>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <!-- 引脚 -->
 | 
					    <!-- 引脚 -->
 | 
				
			||||||
    <div v-for="pin in pins" :key="pin.pinId" :style="{
 | 
					    <div
 | 
				
			||||||
      position: 'absolute',
 | 
					      v-for="pin in pins"
 | 
				
			||||||
      left: `${pin.x * props.size}px`,
 | 
					      :key="pin.pinId"
 | 
				
			||||||
      top: `${pin.y * props.size}px`,
 | 
					      :style="{
 | 
				
			||||||
      transform: 'translate(-50%, -50%)',
 | 
					        position: 'absolute',
 | 
				
			||||||
    }" :data-pin-wrapper="`${pin.pinId}`" :data-pin-x="`${pin.x * props.size}`"
 | 
					        left: `${pin.x * props.size}px`,
 | 
				
			||||||
      :data-pin-y="`${pin.y * props.size}`">
 | 
					        top: `${pin.y * props.size}px`,
 | 
				
			||||||
      <Pin :ref="(el) => {
 | 
					        transform: 'translate(-50%, -50%)',
 | 
				
			||||||
          if (el) pinRefs[pin.pinId] = el;
 | 
					      }"
 | 
				
			||||||
        }
 | 
					      :data-pin-wrapper="`${pin.pinId}`"
 | 
				
			||||||
        " :label="pin.pinId" :constraint="pin.constraint" :pinId="pin.pinId" @pin-click="$emit('pin-click', $event)" />
 | 
					      :data-pin-x="`${pin.x * props.size}`"
 | 
				
			||||||
 | 
					      :data-pin-y="`${pin.y * props.size}`"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <Pin
 | 
				
			||||||
 | 
					        :ref="
 | 
				
			||||||
 | 
					          (el) => {
 | 
				
			||||||
 | 
					            if (el) pinRefs[pin.pinId] = el;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        "
 | 
				
			||||||
 | 
					        :label="pin.pinId"
 | 
				
			||||||
 | 
					        :constraint="pin.constraint"
 | 
				
			||||||
 | 
					        :pinId="pin.pinId"
 | 
				
			||||||
 | 
					        @pin-click="$emit('pin-click', $event)"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
@@ -217,12 +266,12 @@ function isSegmentActive(
 | 
				
			|||||||
  if (isInAfterglowMode.value) {
 | 
					  if (isInAfterglowMode.value) {
 | 
				
			||||||
    return afterglowStates.value[segment];
 | 
					    return afterglowStates.value[segment];
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  // 如果COM口未激活,所有段都不显示
 | 
					  // 如果COM口未激活,所有段都不显示
 | 
				
			||||||
  if (!currentComActive.value) {
 | 
					  if (!currentComActive.value) {
 | 
				
			||||||
    return false;
 | 
					    return false;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  // 否则使用稳定状态
 | 
					  // 否则使用稳定状态
 | 
				
			||||||
  return stableSegmentStates.value[segment];
 | 
					  return stableSegmentStates.value[segment];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -232,7 +281,7 @@ function updateSegmentStates() {
 | 
				
			|||||||
  // 先获取COM口状态
 | 
					  // 先获取COM口状态
 | 
				
			||||||
  const comPin = props.pins.find((p) => p.pinId === "COM");
 | 
					  const comPin = props.pins.find((p) => p.pinId === "COM");
 | 
				
			||||||
  let comActive = false; // 默认未激活
 | 
					  let comActive = false; // 默认未激活
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  if (comPin && comPin.constraint) {
 | 
					  if (comPin && comPin.constraint) {
 | 
				
			||||||
    const comState = getConstraintState(comPin.constraint);
 | 
					    const comState = getConstraintState(comPin.constraint);
 | 
				
			||||||
    if (props.cathodeType === "anode") {
 | 
					    if (props.cathodeType === "anode") {
 | 
				
			||||||
@@ -274,7 +323,8 @@ function updateSegmentStates() {
 | 
				
			|||||||
  for (const pin of props.pins) {
 | 
					  for (const pin of props.pins) {
 | 
				
			||||||
    if (["a", "b", "c", "d", "e", "f", "g", "dp"].includes(pin.pinId)) {
 | 
					    if (["a", "b", "c", "d", "e", "f", "g", "dp"].includes(pin.pinId)) {
 | 
				
			||||||
      if (!pin.constraint) {
 | 
					      if (!pin.constraint) {
 | 
				
			||||||
        segmentStates.value[pin.pinId as keyof typeof segmentStates.value] = false;
 | 
					        segmentStates.value[pin.pinId as keyof typeof segmentStates.value] =
 | 
				
			||||||
 | 
					          false;
 | 
				
			||||||
        continue;
 | 
					        continue;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      const pinState = getConstraintState(pin.constraint);
 | 
					      const pinState = getConstraintState(pin.constraint);
 | 
				
			||||||
@@ -285,7 +335,8 @@ function updateSegmentStates() {
 | 
				
			|||||||
        newState = pinState === "low";
 | 
					        newState = pinState === "low";
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      // 段状态只有在COM激活时才有效
 | 
					      // 段状态只有在COM激活时才有效
 | 
				
			||||||
      segmentStates.value[pin.pinId as keyof typeof segmentStates.value] = newState;
 | 
					      segmentStates.value[pin.pinId as keyof typeof segmentStates.value] =
 | 
				
			||||||
 | 
					        newState;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -328,22 +379,25 @@ function updateAfterglowBuffers() {
 | 
				
			|||||||
// 进入余晖模式
 | 
					// 进入余晖模式
 | 
				
			||||||
function enterAfterglowMode() {
 | 
					function enterAfterglowMode() {
 | 
				
			||||||
  isInAfterglowMode.value = true;
 | 
					  isInAfterglowMode.value = true;
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  // 保存当前稳定状态作为余晖状态
 | 
					  // 保存当前稳定状态作为余晖状态
 | 
				
			||||||
  for (const segmentId of ["a", "b", "c", "d", "e", "f", "g", "dp"]) {
 | 
					  for (const segmentId of ["a", "b", "c", "d", "e", "f", "g", "dp"]) {
 | 
				
			||||||
    const typedSegmentId = segmentId as keyof typeof stableSegmentStates.value;
 | 
					    const typedSegmentId = segmentId as keyof typeof stableSegmentStates.value;
 | 
				
			||||||
    afterglowStates.value[typedSegmentId] = stableSegmentStates.value[typedSegmentId];
 | 
					    afterglowStates.value[typedSegmentId] =
 | 
				
			||||||
    
 | 
					      stableSegmentStates.value[typedSegmentId];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // 设置定时器,在余晖持续时间后退出余晖模式
 | 
					    // 设置定时器,在余晖持续时间后退出余晖模式
 | 
				
			||||||
    if (afterglowTimers.value[segmentId]) {
 | 
					    if (afterglowTimers.value[segmentId]) {
 | 
				
			||||||
      clearTimeout(afterglowTimers.value[segmentId]!);
 | 
					      clearTimeout(afterglowTimers.value[segmentId]!);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    afterglowTimers.value[segmentId] = setTimeout(() => {
 | 
					    afterglowTimers.value[segmentId] = setTimeout(() => {
 | 
				
			||||||
      afterglowStates.value[typedSegmentId] = false;
 | 
					      afterglowStates.value[typedSegmentId] = false;
 | 
				
			||||||
      
 | 
					
 | 
				
			||||||
      // 检查是否所有段都已经关闭
 | 
					      // 检查是否所有段都已经关闭
 | 
				
			||||||
      const allSegmentsOff = Object.values(afterglowStates.value).every(state => !state);
 | 
					      const allSegmentsOff = Object.values(afterglowStates.value).every(
 | 
				
			||||||
 | 
					        (state) => !state,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
      if (allSegmentsOff) {
 | 
					      if (allSegmentsOff) {
 | 
				
			||||||
        exitAfterglowMode();
 | 
					        exitAfterglowMode();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@@ -354,14 +408,14 @@ function enterAfterglowMode() {
 | 
				
			|||||||
// 退出余晖模式
 | 
					// 退出余晖模式
 | 
				
			||||||
function exitAfterglowMode() {
 | 
					function exitAfterglowMode() {
 | 
				
			||||||
  isInAfterglowMode.value = false;
 | 
					  isInAfterglowMode.value = false;
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  // 清除所有定时器
 | 
					  // 清除所有定时器
 | 
				
			||||||
  for (const segmentId of ["a", "b", "c", "d", "e", "f", "g", "dp"]) {
 | 
					  for (const segmentId of ["a", "b", "c", "d", "e", "f", "g", "dp"]) {
 | 
				
			||||||
    if (afterglowTimers.value[segmentId]) {
 | 
					    if (afterglowTimers.value[segmentId]) {
 | 
				
			||||||
      clearTimeout(afterglowTimers.value[segmentId]!);
 | 
					      clearTimeout(afterglowTimers.value[segmentId]!);
 | 
				
			||||||
      afterglowTimers.value[segmentId] = null;
 | 
					      afterglowTimers.value[segmentId] = null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    // 重置余晖状态
 | 
					    // 重置余晖状态
 | 
				
			||||||
    const typedSegmentId = segmentId as keyof typeof afterglowStates.value;
 | 
					    const typedSegmentId = segmentId as keyof typeof afterglowStates.value;
 | 
				
			||||||
    afterglowStates.value[typedSegmentId] = false;
 | 
					    afterglowStates.value[typedSegmentId] = false;
 | 
				
			||||||
@@ -397,11 +451,6 @@ onUnmounted(() => {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					 | 
				
			||||||
// 暴露属性和方法
 | 
					 | 
				
			||||||
defineExpose({
 | 
					 | 
				
			||||||
  updateSegmentStates,
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style scoped>
 | 
					<style scoped>
 | 
				
			||||||
@@ -418,7 +467,8 @@ defineExpose({
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
/* 数码管发光效果 */
 | 
					/* 数码管发光效果 */
 | 
				
			||||||
.segment[style*="opacity: 1"] {
 | 
					.segment[style*="opacity: 1"] {
 | 
				
			||||||
  filter: drop-shadow(0 0 4px v-bind(segmentColor)) drop-shadow(0 0 2px v-bind(segmentColor));
 | 
					  filter: drop-shadow(0 0 4px v-bind(segmentColor))
 | 
				
			||||||
 | 
					    drop-shadow(0 0 2px v-bind(segmentColor));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										413
									
								
								src/components/equipments/SevenSegmentDisplayUltimate.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										413
									
								
								src/components/equipments/SevenSegmentDisplayUltimate.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,413 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div
 | 
				
			||||||
 | 
					    class="seven-segment-display"
 | 
				
			||||||
 | 
					    :style="{
 | 
				
			||||||
 | 
					      width: width + 'px',
 | 
				
			||||||
 | 
					      height: height + 'px',
 | 
				
			||||||
 | 
					      position: 'relative',
 | 
				
			||||||
 | 
					    }"
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <svg
 | 
				
			||||||
 | 
					      xmlns="http://www.w3.org/2000/svg"
 | 
				
			||||||
 | 
					      :width="width"
 | 
				
			||||||
 | 
					      :height="height"
 | 
				
			||||||
 | 
					      viewBox="0 0 120 220"
 | 
				
			||||||
 | 
					      class="display"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <!-- 数码管基座 -->
 | 
				
			||||||
 | 
					      <rect width="120" height="180" x="0" y="0" fill="#222" rx="10" ry="10" />
 | 
				
			||||||
 | 
					      <rect width="110" height="170" x="5" y="5" fill="#333" rx="5" ry="5" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <!-- 7段显示 -->
 | 
				
			||||||
 | 
					      <polygon
 | 
				
			||||||
 | 
					        v-for="(segment, id) in segmentPaths"
 | 
				
			||||||
 | 
					        :key="id"
 | 
				
			||||||
 | 
					        :points="segment.points"
 | 
				
			||||||
 | 
					        :fill="isSegmentActive(id) ? segmentColor : inactiveColor"
 | 
				
			||||||
 | 
					        :style="{ opacity: isSegmentActive(id) ? 1 : 0.15 }"
 | 
				
			||||||
 | 
					        :class="{ segment: true, active: isSegmentActive(id) }"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <!-- 小数点 -->
 | 
				
			||||||
 | 
					      <circle
 | 
				
			||||||
 | 
					        cx="108"
 | 
				
			||||||
 | 
					        cy="154"
 | 
				
			||||||
 | 
					        r="6"
 | 
				
			||||||
 | 
					        :fill="isSegmentActive('dp') ? segmentColor : inactiveColor"
 | 
				
			||||||
 | 
					        :style="{ opacity: isSegmentActive('dp') ? 1 : 0.15 }"
 | 
				
			||||||
 | 
					        :class="{ segment: true, active: isSegmentActive('dp') }"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </svg>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- 引脚(仅在非数字孪生模式下显示) -->
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      v-if="!props.enableDigitalTwin"
 | 
				
			||||||
 | 
					      v-for="pin in props.pins"
 | 
				
			||||||
 | 
					      :key="pin.pinId"
 | 
				
			||||||
 | 
					      :style="{
 | 
				
			||||||
 | 
					        position: 'absolute',
 | 
				
			||||||
 | 
					        left: `${pin.x * props.size}px`,
 | 
				
			||||||
 | 
					        top: `${pin.y * props.size}px`,
 | 
				
			||||||
 | 
					        transform: 'translate(-50%, -50%)',
 | 
				
			||||||
 | 
					      }"
 | 
				
			||||||
 | 
					      :data-pin-wrapper="`${pin.pinId}`"
 | 
				
			||||||
 | 
					      :data-pin-x="`${pin.x * props.size}`"
 | 
				
			||||||
 | 
					      :data-pin-y="`${pin.y * props.size}`"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <Pin
 | 
				
			||||||
 | 
					        :ref="
 | 
				
			||||||
 | 
					          (el) => {
 | 
				
			||||||
 | 
					            if (el) pinRefs[pin.pinId] = el;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        "
 | 
				
			||||||
 | 
					        :label="pin.pinId"
 | 
				
			||||||
 | 
					        :constraint="pin.constraint"
 | 
				
			||||||
 | 
					        :pinId="pin.pinId"
 | 
				
			||||||
 | 
					        @pin-click="$emit('pin-click', $event)"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { ref, computed, watch, onMounted, onUnmounted } from "vue";
 | 
				
			||||||
 | 
					import { useConstraintsStore } from "../../stores/constraints";
 | 
				
			||||||
 | 
					import Pin from "./Pin.vue";
 | 
				
			||||||
 | 
					import { useEquipments } from "@/stores/equipments";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ============================================================================
 | 
				
			||||||
 | 
					// Linus式极简数据结构:一个byte解决一切
 | 
				
			||||||
 | 
					// ============================================================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Props {
 | 
				
			||||||
 | 
					  size?: number;
 | 
				
			||||||
 | 
					  color?: string;
 | 
				
			||||||
 | 
					  enableDigitalTwin?: boolean;
 | 
				
			||||||
 | 
					  digitalTwinNum?: number;
 | 
				
			||||||
 | 
					  afterglowDuration?: number;
 | 
				
			||||||
 | 
					  cathodeType?: "common" | "anode";
 | 
				
			||||||
 | 
					  pins?: Array<{
 | 
				
			||||||
 | 
					    pinId: string;
 | 
				
			||||||
 | 
					    constraint: string;
 | 
				
			||||||
 | 
					    x: number;
 | 
				
			||||||
 | 
					    y: number;
 | 
				
			||||||
 | 
					  }>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = withDefaults(defineProps<Props>(), {
 | 
				
			||||||
 | 
					  size: 1,
 | 
				
			||||||
 | 
					  color: "red",
 | 
				
			||||||
 | 
					  enableDigitalTwin: false,
 | 
				
			||||||
 | 
					  digitalTwinNum: 0,
 | 
				
			||||||
 | 
					  afterglowDuration: 500,
 | 
				
			||||||
 | 
					  cathodeType: "common",
 | 
				
			||||||
 | 
					  pins: () => [
 | 
				
			||||||
 | 
					    { pinId: "a", constraint: "", x: 10, y: 170 },
 | 
				
			||||||
 | 
					    { pinId: "b", constraint: "", x: 24, y: 170 },
 | 
				
			||||||
 | 
					    { pinId: "c", constraint: "", x: 38, y: 170 },
 | 
				
			||||||
 | 
					    { pinId: "d", constraint: "", x: 52, y: 170 },
 | 
				
			||||||
 | 
					    { pinId: "e", constraint: "", x: 66, y: 170 },
 | 
				
			||||||
 | 
					    { pinId: "f", constraint: "", x: 80, y: 170 },
 | 
				
			||||||
 | 
					    { pinId: "g", constraint: "", x: 94, y: 170 },
 | 
				
			||||||
 | 
					    { pinId: "dp", constraint: "", x: 108, y: 170 },
 | 
				
			||||||
 | 
					    { pinId: "COM", constraint: "", x: 60, y: 10 },
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ============================================================================
 | 
				
			||||||
 | 
					// 核心状态:简单到极致
 | 
				
			||||||
 | 
					// ============================================================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 当前显示状态 - 8bit对应8个段
 | 
				
			||||||
 | 
					const displayByte = ref<number>(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 余晖状态
 | 
				
			||||||
 | 
					const afterglowByte = ref<number>(0);
 | 
				
			||||||
 | 
					const afterglowTimer = ref<number | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 约束系统状态(兼容模式)
 | 
				
			||||||
 | 
					const constraintStates = ref<Record<string, boolean>>({
 | 
				
			||||||
 | 
					  a: false,
 | 
				
			||||||
 | 
					  b: false,
 | 
				
			||||||
 | 
					  c: false,
 | 
				
			||||||
 | 
					  d: false,
 | 
				
			||||||
 | 
					  e: false,
 | 
				
			||||||
 | 
					  f: false,
 | 
				
			||||||
 | 
					  g: false,
 | 
				
			||||||
 | 
					  dp: false,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ============================================================================
 | 
				
			||||||
 | 
					// Bit操作:硬件工程师的好品味
 | 
				
			||||||
 | 
					// ============================================================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 段到bit位的映射 (标准7段数码管编码)
 | 
				
			||||||
 | 
					const SEGMENT_BITS = {
 | 
				
			||||||
 | 
					  a: 0, // bit 0
 | 
				
			||||||
 | 
					  b: 1, // bit 1
 | 
				
			||||||
 | 
					  c: 2, // bit 2
 | 
				
			||||||
 | 
					  d: 3, // bit 3
 | 
				
			||||||
 | 
					  e: 4, // bit 4
 | 
				
			||||||
 | 
					  f: 5, // bit 5
 | 
				
			||||||
 | 
					  g: 6, // bit 6
 | 
				
			||||||
 | 
					  dp: 7, // bit 7
 | 
				
			||||||
 | 
					} as const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function isBitSet(byte: number, bit: number): boolean {
 | 
				
			||||||
 | 
					  return (byte & (1 << bit)) !== 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function isSegmentActive(segmentId: keyof typeof SEGMENT_BITS): boolean {
 | 
				
			||||||
 | 
					  if (props.enableDigitalTwin) {
 | 
				
			||||||
 | 
					    // 数字孪生模式:余晖优先,然后是当前byte
 | 
				
			||||||
 | 
					    const bit = SEGMENT_BITS[segmentId];
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      isBitSet(afterglowByte.value, bit) || isBitSet(displayByte.value, bit)
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    // 约束模式:使用传统逻辑
 | 
				
			||||||
 | 
					    return constraintStates.value[segmentId] || false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ============================================================================
 | 
				
			||||||
 | 
					// SignalR数字孪生集成
 | 
				
			||||||
 | 
					// ============================================================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const eqps = useEquipments();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function initDigitalTwin() {
 | 
				
			||||||
 | 
					  if (
 | 
				
			||||||
 | 
					    !props.enableDigitalTwin ||
 | 
				
			||||||
 | 
					    props.digitalTwinNum < 0 ||
 | 
				
			||||||
 | 
					    props.digitalTwinNum > 31
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    eqps.sevenSegmentDisplaySetOnOff(props.enableDigitalTwin);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log(
 | 
				
			||||||
 | 
					      `Digital twin initialized for address: ${props.digitalTwinNum}`,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.warn("Failed to initialize digital twin:", error);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					watch(
 | 
				
			||||||
 | 
					  () => [eqps.sevenSegmentDisplayData],
 | 
				
			||||||
 | 
					  () => {
 | 
				
			||||||
 | 
					    if (
 | 
				
			||||||
 | 
					      !eqps.sevenSegmentDisplayData ||
 | 
				
			||||||
 | 
					      props.digitalTwinNum < 0 ||
 | 
				
			||||||
 | 
					      props.digitalTwinNum > 31
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    handleDigitalTwinData(eqps.sevenSegmentDisplayData[props.digitalTwinNum]);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function handleDigitalTwinData(data: any) {
 | 
				
			||||||
 | 
					  let newByte = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (typeof data === "number") {
 | 
				
			||||||
 | 
					    // 直接是byte数据
 | 
				
			||||||
 | 
					    newByte = data & 0xff; // 确保只取低8位
 | 
				
			||||||
 | 
					  } else if (data && typeof data.value === "number") {
 | 
				
			||||||
 | 
					    // 包装在对象中的byte数据
 | 
				
			||||||
 | 
					    newByte = data.value & 0xff;
 | 
				
			||||||
 | 
					  } else if (data && data.segments) {
 | 
				
			||||||
 | 
					    // 段状态对象格式
 | 
				
			||||||
 | 
					    Object.keys(SEGMENT_BITS).forEach((segment) => {
 | 
				
			||||||
 | 
					      if (data.segments[segment]) {
 | 
				
			||||||
 | 
					        newByte |= 1 << SEGMENT_BITS[segment as keyof typeof SEGMENT_BITS];
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  updateDisplayByte(newByte);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function updateDisplayByte(newByte: number) {
 | 
				
			||||||
 | 
					  const oldByte = displayByte.value;
 | 
				
			||||||
 | 
					  displayByte.value = newByte;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 启动余晖效果
 | 
				
			||||||
 | 
					  if (oldByte !== 0 && newByte !== oldByte) {
 | 
				
			||||||
 | 
					    startAfterglow(oldByte);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function startAfterglow(byte: number) {
 | 
				
			||||||
 | 
					  afterglowByte.value = byte;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (afterglowTimer.value) {
 | 
				
			||||||
 | 
					    clearTimeout(afterglowTimer.value);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  afterglowTimer.value = setTimeout(() => {
 | 
				
			||||||
 | 
					    afterglowByte.value = 0;
 | 
				
			||||||
 | 
					    afterglowTimer.value = null;
 | 
				
			||||||
 | 
					  }, props.afterglowDuration);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function cleanupDigitalTwin() {
 | 
				
			||||||
 | 
					  eqps.sevenSegmentDisplaySetOnOff(false);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ============================================================================
 | 
				
			||||||
 | 
					// 约束系统兼容(传统模式)
 | 
				
			||||||
 | 
					// ============================================================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { getConstraintState, onConstraintStateChange } = useConstraintsStore();
 | 
				
			||||||
 | 
					let constraintUnsubscribe: (() => void) | null = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function updateConstraintStates() {
 | 
				
			||||||
 | 
					  if (props.enableDigitalTwin) return; // 数字孪生模式下忽略约束
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 获取COM状态
 | 
				
			||||||
 | 
					  const comPin = props.pins.find((p) => p.pinId === "COM");
 | 
				
			||||||
 | 
					  const comActive = isComActive(comPin);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!comActive) {
 | 
				
			||||||
 | 
					    // COM不活跃,所有段关闭
 | 
				
			||||||
 | 
					    Object.keys(constraintStates.value).forEach((key) => {
 | 
				
			||||||
 | 
					      constraintStates.value[key] = false;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 更新各段状态
 | 
				
			||||||
 | 
					  props.pins.forEach((pin) => {
 | 
				
			||||||
 | 
					    if (Object.hasOwnProperty.call(SEGMENT_BITS, pin.pinId)) {
 | 
				
			||||||
 | 
					      constraintStates.value[pin.pinId] = isPinActive(pin);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function isComActive(comPin: any): boolean {
 | 
				
			||||||
 | 
					  if (!comPin?.constraint) return true;
 | 
				
			||||||
 | 
					  const state = getConstraintState(comPin.constraint);
 | 
				
			||||||
 | 
					  return props.cathodeType === "common" ? state === "low" : state === "low";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function isPinActive(pin: any): boolean {
 | 
				
			||||||
 | 
					  if (!pin.constraint) return false;
 | 
				
			||||||
 | 
					  const state = getConstraintState(pin.constraint);
 | 
				
			||||||
 | 
					  return props.cathodeType === "common" ? state === "high" : state === "low";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ============================================================================
 | 
				
			||||||
 | 
					// 渲染数据
 | 
				
			||||||
 | 
					// ============================================================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const segmentPaths = {
 | 
				
			||||||
 | 
					  a: { points: "30,20 90,20 98,28 82,36 38,36 22,28" },
 | 
				
			||||||
 | 
					  b: { points: "100,30 108,38 108,82 100,90 92,82 92,38" },
 | 
				
			||||||
 | 
					  c: { points: "100,90 108,98 108,142 100,150 92,142 92,98" },
 | 
				
			||||||
 | 
					  d: { points: "30,160 90,160 98,152 82,144 38,144 22,152" },
 | 
				
			||||||
 | 
					  e: { points: "20,90 28,98 28,142 20,150 12,142 12,98" },
 | 
				
			||||||
 | 
					  f: { points: "20,30 28,38 28,82 20,90 12,82 12,38" },
 | 
				
			||||||
 | 
					  g: { points: "30,90 38,82 82,82 90,90 82,98 38,98" },
 | 
				
			||||||
 | 
					} as const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ============================================================================
 | 
				
			||||||
 | 
					// 计算属性
 | 
				
			||||||
 | 
					// ============================================================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const width = computed(() => 120 * props.size);
 | 
				
			||||||
 | 
					const height = computed(() => 220 * props.size);
 | 
				
			||||||
 | 
					const segmentColor = computed(() => props.color);
 | 
				
			||||||
 | 
					const inactiveColor = computed(() => "#FFFFFF");
 | 
				
			||||||
 | 
					const pinRefs = ref<Record<string, any>>({});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ============================================================================
 | 
				
			||||||
 | 
					// 生命周期
 | 
				
			||||||
 | 
					// ============================================================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onMounted(async () => {
 | 
				
			||||||
 | 
					  if (props.enableDigitalTwin) {
 | 
				
			||||||
 | 
					    await initDigitalTwin();
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    constraintUnsubscribe = onConstraintStateChange(updateConstraintStates);
 | 
				
			||||||
 | 
					    updateConstraintStates();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onUnmounted(() => {
 | 
				
			||||||
 | 
					  cleanupDigitalTwin();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (constraintUnsubscribe) {
 | 
				
			||||||
 | 
					    constraintUnsubscribe();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (afterglowTimer.value) {
 | 
				
			||||||
 | 
					    clearTimeout(afterglowTimer.value);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 监听模式切换
 | 
				
			||||||
 | 
					watch(
 | 
				
			||||||
 | 
					  () => [props.enableDigitalTwin],
 | 
				
			||||||
 | 
					  async () => {
 | 
				
			||||||
 | 
					    // 清理旧模式
 | 
				
			||||||
 | 
					    cleanupDigitalTwin();
 | 
				
			||||||
 | 
					    if (constraintUnsubscribe) {
 | 
				
			||||||
 | 
					      constraintUnsubscribe();
 | 
				
			||||||
 | 
					      constraintUnsubscribe = null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 初始化新模式
 | 
				
			||||||
 | 
					    if (props.enableDigitalTwin) {
 | 
				
			||||||
 | 
					      await initDigitalTwin();
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      constraintUnsubscribe = onConstraintStateChange(updateConstraintStates);
 | 
				
			||||||
 | 
					      updateConstraintStates();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.seven-segment-display {
 | 
				
			||||||
 | 
					  display: inline-block;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.segment {
 | 
				
			||||||
 | 
					  transition:
 | 
				
			||||||
 | 
					    opacity 0.2s,
 | 
				
			||||||
 | 
					    fill 0.2s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.segment.active {
 | 
				
			||||||
 | 
					  filter: drop-shadow(0 0 4px v-bind(segmentColor))
 | 
				
			||||||
 | 
					    drop-shadow(0 0 2px v-bind(segmentColor));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					export function getDefaultProps() {
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    size: 1,
 | 
				
			||||||
 | 
					    color: "red",
 | 
				
			||||||
 | 
					    enableDigitalTwin: false,
 | 
				
			||||||
 | 
					    digitalTwinNum: 0,
 | 
				
			||||||
 | 
					    afterglowDuration: 500,
 | 
				
			||||||
 | 
					    cathodeType: "common",
 | 
				
			||||||
 | 
					    pins: [
 | 
				
			||||||
 | 
					      { pinId: "a", constraint: "", x: 10, y: 170 },
 | 
				
			||||||
 | 
					      { pinId: "b", constraint: "", x: 24, y: 170 },
 | 
				
			||||||
 | 
					      { pinId: "c", constraint: "", x: 38, y: 170 },
 | 
				
			||||||
 | 
					      { pinId: "d", constraint: "", x: 52, y: 170 },
 | 
				
			||||||
 | 
					      { pinId: "e", constraint: "", x: 66, y: 170 },
 | 
				
			||||||
 | 
					      { pinId: "f", constraint: "", x: 80, y: 170 },
 | 
				
			||||||
 | 
					      { pinId: "g", constraint: "", x: 94, y: 170 },
 | 
				
			||||||
 | 
					      { pinId: "dp", constraint: "", x: 108, y: 170 },
 | 
				
			||||||
 | 
					      { pinId: "COM", constraint: "", x: 60, y: 10 },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
@@ -1,17 +1,30 @@
 | 
				
			|||||||
// filepath: c:\_Project\FPGA_WebLab\FPGA_WebLab\src\components\equipments\Switch.vue
 | 
					 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <svg 
 | 
					  <svg
 | 
				
			||||||
    xmlns="http://www.w3.org/2000/svg" 
 | 
					    xmlns="http://www.w3.org/2000/svg"
 | 
				
			||||||
    :width="width" 
 | 
					    :width="width"
 | 
				
			||||||
    :height="height" 
 | 
					    :height="height"
 | 
				
			||||||
    :viewBox="`4 6 ${props.switchCount + 2} 4`"
 | 
					    :viewBox="`4 6 ${switchCount + 2} 4`"
 | 
				
			||||||
    class="dip-switch"
 | 
					    class="dip-switch"
 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    <defs>
 | 
					    <defs>
 | 
				
			||||||
      <filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
 | 
					      <filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
 | 
				
			||||||
        <feFlood result="flood" flood-color="#f08a5d" flood-opacity="1"></feFlood>
 | 
					        <feFlood
 | 
				
			||||||
        <feComposite in="flood" result="mask" in2="SourceGraphic" operator="in"></feComposite>
 | 
					          result="flood"
 | 
				
			||||||
        <feMorphology in="mask" result="dilated" operator="dilate" radius="0.02"></feMorphology>
 | 
					          flood-color="#f08a5d"
 | 
				
			||||||
 | 
					          flood-opacity="1"
 | 
				
			||||||
 | 
					        ></feFlood>
 | 
				
			||||||
 | 
					        <feComposite
 | 
				
			||||||
 | 
					          in="flood"
 | 
				
			||||||
 | 
					          result="mask"
 | 
				
			||||||
 | 
					          in2="SourceGraphic"
 | 
				
			||||||
 | 
					          operator="in"
 | 
				
			||||||
 | 
					        ></feComposite>
 | 
				
			||||||
 | 
					        <feMorphology
 | 
				
			||||||
 | 
					          in="mask"
 | 
				
			||||||
 | 
					          result="dilated"
 | 
				
			||||||
 | 
					          operator="dilate"
 | 
				
			||||||
 | 
					          radius="0.02"
 | 
				
			||||||
 | 
					        ></feMorphology>
 | 
				
			||||||
        <feGaussianBlur in="dilated" stdDeviation="0.05" result="blur1" />
 | 
					        <feGaussianBlur in="dilated" stdDeviation="0.05" result="blur1" />
 | 
				
			||||||
        <feGaussianBlur in="dilated" stdDeviation="0.1" result="blur2" />
 | 
					        <feGaussianBlur in="dilated" stdDeviation="0.1" result="blur2" />
 | 
				
			||||||
        <feGaussianBlur in="dilated" stdDeviation="0.2" result="blur3" />
 | 
					        <feGaussianBlur in="dilated" stdDeviation="0.2" result="blur3" />
 | 
				
			||||||
@@ -23,29 +36,41 @@
 | 
				
			|||||||
        </feMerge>
 | 
					        </feMerge>
 | 
				
			||||||
      </filter>
 | 
					      </filter>
 | 
				
			||||||
    </defs>
 | 
					    </defs>
 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    <g>
 | 
					    <g>
 | 
				
			||||||
      <!-- 红色背景随开关数量变化宽度 -->
 | 
					      <rect
 | 
				
			||||||
      <rect :width="props.switchCount + 2" height="4" x="4" y="6" fill="#c01401" rx="0.1" />
 | 
					        :width="switchCount + 2"
 | 
				
			||||||
      <text v-if="props.showLabels" fill="white" font-size="0.7" x="4.25" y="6.75">ON</text>
 | 
					        height="4"
 | 
				
			||||||
      
 | 
					        x="4"
 | 
				
			||||||
 | 
					        y="6"
 | 
				
			||||||
 | 
					        fill="#c01401"
 | 
				
			||||||
 | 
					        rx="0.1"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					      <text
 | 
				
			||||||
 | 
					        v-if="props.showLabels"
 | 
				
			||||||
 | 
					        fill="white"
 | 
				
			||||||
 | 
					        font-size="0.7"
 | 
				
			||||||
 | 
					        x="4.25"
 | 
				
			||||||
 | 
					        y="6.75"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        ON
 | 
				
			||||||
 | 
					      </text>
 | 
				
			||||||
      <g>
 | 
					      <g>
 | 
				
			||||||
        <template v-for="(_, index) in Array(props.switchCount)" :key="index">
 | 
					        <template v-for="(_, index) in Array(switchCount)" :key="index">
 | 
				
			||||||
          <rect 
 | 
					          <rect
 | 
				
			||||||
            class="glow interactive" 
 | 
					            class="glow interactive"
 | 
				
			||||||
            @click="toggleBtnStatus(index)" 
 | 
					            @click="toggleBtnStatus(index)"
 | 
				
			||||||
            width="0.7" 
 | 
					            width="0.7"
 | 
				
			||||||
            height="2" 
 | 
					            height="2"
 | 
				
			||||||
            fill="#68716f" 
 | 
					            fill="#68716f"
 | 
				
			||||||
            :x="5.15 + index" 
 | 
					            :x="5.15 + index"
 | 
				
			||||||
            y="7" 
 | 
					            y="7"
 | 
				
			||||||
            rx="0.1" 
 | 
					            rx="0.1"
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
          <text 
 | 
					          <text
 | 
				
			||||||
            v-if="props.showLabels"
 | 
					            v-if="props.showLabels"
 | 
				
			||||||
            :x="5.5 + index" 
 | 
					            :x="5.5 + index"
 | 
				
			||||||
            y="9.5" 
 | 
					            y="9.5"
 | 
				
			||||||
            font-size="0.4" 
 | 
					            font-size="0.4"
 | 
				
			||||||
            text-anchor="middle"
 | 
					            text-anchor="middle"
 | 
				
			||||||
            fill="#444"
 | 
					            fill="#444"
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
@@ -53,19 +78,21 @@
 | 
				
			|||||||
          </text>
 | 
					          </text>
 | 
				
			||||||
        </template>
 | 
					        </template>
 | 
				
			||||||
      </g>
 | 
					      </g>
 | 
				
			||||||
      
 | 
					 | 
				
			||||||
      <g>
 | 
					      <g>
 | 
				
			||||||
        <template v-for="(location, index) in btnLocation" :key="`btn-${index}`">
 | 
					        <template
 | 
				
			||||||
          <rect 
 | 
					          v-for="(location, index) in btnLocation"
 | 
				
			||||||
 | 
					          :key="`btn-${index}`"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <rect
 | 
				
			||||||
            class="interactive"
 | 
					            class="interactive"
 | 
				
			||||||
            @click="toggleBtnStatus(index)" 
 | 
					            @click="toggleBtnStatus(index)"
 | 
				
			||||||
            width="0.65" 
 | 
					            width="0.65"
 | 
				
			||||||
            height="0.65" 
 | 
					            height="0.65"
 | 
				
			||||||
            fill="white" 
 | 
					            fill="white"
 | 
				
			||||||
            :x="5.175 + index" 
 | 
					            :x="5.175 + index"
 | 
				
			||||||
            :y="location" 
 | 
					            :y="location"
 | 
				
			||||||
            rx="0.1"
 | 
					            rx="0.1"
 | 
				
			||||||
            opacity="1" 
 | 
					            opacity="1"
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
        </template>
 | 
					        </template>
 | 
				
			||||||
      </g>
 | 
					      </g>
 | 
				
			||||||
@@ -74,119 +101,112 @@
 | 
				
			|||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts" setup>
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { computed, ref, watch } from "vue";
 | 
					import { SwitchClient } from "@/APIClient";
 | 
				
			||||||
 | 
					import { AuthManager } from "@/utils/AuthManager";
 | 
				
			||||||
 | 
					import { isUndefined } from "lodash";
 | 
				
			||||||
 | 
					import { ref, computed, watch, onMounted } from "vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface Props {
 | 
					interface Props {
 | 
				
			||||||
  size?: number;
 | 
					  size?: number;
 | 
				
			||||||
 | 
					  enableDigitalTwin?: boolean;
 | 
				
			||||||
  switchCount?: number;
 | 
					  switchCount?: number;
 | 
				
			||||||
  // 新增属性
 | 
					  initialValues?: string;
 | 
				
			||||||
  initialValues?: boolean[] | string; // 开关的初始状态,可以是布尔数组或逗号分隔的字符串
 | 
					  showLabels?: boolean;
 | 
				
			||||||
  showLabels?: boolean;      // 是否显示标签
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const props = withDefaults(defineProps<Props>(), {
 | 
					const props = withDefaults(defineProps<Props>(), {
 | 
				
			||||||
  size: 1,
 | 
					  size: 1,
 | 
				
			||||||
 | 
					  enableDigitalTwin: false,
 | 
				
			||||||
  switchCount: 6,
 | 
					  switchCount: 6,
 | 
				
			||||||
  initialValues: () => [],
 | 
					  initialValues: "",
 | 
				
			||||||
  showLabels: true
 | 
					  showLabels: true,
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 计算实际宽高
 | 
					const switchCount = computed(() => {
 | 
				
			||||||
const width = computed(() => {
 | 
					  if (props.enableDigitalTwin) return 5;
 | 
				
			||||||
  // 每个开关占用25px宽度,再加上两侧边距(20px)
 | 
					  else return props.switchCount;
 | 
				
			||||||
  return (props.switchCount * 25 + 20) * props.size;
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
const height = computed(() => 85 * props.size); // 高度保持固定比例
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 定义发出的事件
 | 
					function getClient() {
 | 
				
			||||||
const emit = defineEmits(['change', 'switch-toggle']);
 | 
					  return AuthManager.createClient(SwitchClient);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 解析初始值,支持字符串和数组两种格式
 | 
					// 解析初始值
 | 
				
			||||||
const parseInitialValues = () => {
 | 
					function parseInitialValues(): boolean[] {
 | 
				
			||||||
  if (Array.isArray(props.initialValues)) {
 | 
					  if (Array.isArray(props.initialValues)) {
 | 
				
			||||||
    return [...props.initialValues].slice(0, props.switchCount);
 | 
					    return [...props.initialValues].slice(0, switchCount.value);
 | 
				
			||||||
  } else if (typeof props.initialValues === 'string' && props.initialValues.trim() !== '') {
 | 
					 | 
				
			||||||
    // 将逗号分隔的字符串转换为布尔数组
 | 
					 | 
				
			||||||
    const values = props.initialValues.split(',')
 | 
					 | 
				
			||||||
      .map(val => val.trim() === '1' || val.trim().toLowerCase() === 'true')
 | 
					 | 
				
			||||||
      .slice(0, props.switchCount);
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    // 如果数组长度小于开关数量,用 false 填充
 | 
					 | 
				
			||||||
    while (values.length < props.switchCount) {
 | 
					 | 
				
			||||||
      values.push(false);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    return values;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  // 默认返回全部为 false 的数组
 | 
					  if (
 | 
				
			||||||
  return Array(props.switchCount).fill(false);
 | 
					    typeof props.initialValues === "string" &&
 | 
				
			||||||
};
 | 
					    props.initialValues.trim() !== ""
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
// 初始化按钮状态
 | 
					    const arr = props.initialValues
 | 
				
			||||||
const btnStatus = ref(parseInitialValues());
 | 
					      .split(",")
 | 
				
			||||||
 | 
					      .map((val) => val.trim() === "1" || val.trim().toLowerCase() === "true");
 | 
				
			||||||
// 监听 switchCount 变化,调整开关状态数组
 | 
					    while (arr.length < props.switchCount) arr.push(false);
 | 
				
			||||||
watch(() => props.switchCount, (newCount) => {
 | 
					    return arr.slice(0, props.switchCount);
 | 
				
			||||||
  if (newCount !== btnStatus.value.length) {
 | 
					 | 
				
			||||||
    // 如果新数量大于当前数量,则扩展数组
 | 
					 | 
				
			||||||
    if (newCount > btnStatus.value.length) {
 | 
					 | 
				
			||||||
      btnStatus.value = [
 | 
					 | 
				
			||||||
        ...btnStatus.value,
 | 
					 | 
				
			||||||
        ...Array(newCount - btnStatus.value.length).fill(false)
 | 
					 | 
				
			||||||
      ];
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      // 如果新数量小于当前数量,则截断数组
 | 
					 | 
				
			||||||
      btnStatus.value = btnStatus.value.slice(0, newCount);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}, { immediate: true });
 | 
					  return Array(switchCount.value).fill(false);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 监听 initialValues 变化,更新开关状态
 | 
					// 状态唯一真相
 | 
				
			||||||
watch(() => props.initialValues, () => {
 | 
					const btnStatus = ref<boolean[]>(parseInitialValues());
 | 
				
			||||||
  btnStatus.value = parseInitialValues();
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const btnLocation = computed(() => {
 | 
					// 计算宽高
 | 
				
			||||||
  return btnStatus.value.map((status) => {
 | 
					const width = computed(() => (switchCount.value * 25 + 20) * props.size);
 | 
				
			||||||
    return status ? 7.025 : 8.325;
 | 
					const height = computed(() => 85 * props.size);
 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
function setBtnStatus(btnNum: number, isOn: boolean): void {
 | 
					// 按钮位置
 | 
				
			||||||
  if (btnNum >= 0 && btnNum < btnStatus.value.length) {
 | 
					const btnLocation = computed(() =>
 | 
				
			||||||
    btnStatus.value[btnNum] = isOn;
 | 
					  btnStatus.value.map((status) => (status ? 7.025 : 8.325)),
 | 
				
			||||||
    emit('change', { index: btnNum, value: isOn, states: [...btnStatus.value] });
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 状态变更统一处理
 | 
				
			||||||
 | 
					function updateStatus(newStates: boolean[], index?: number) {
 | 
				
			||||||
 | 
					  btnStatus.value = newStates.slice(0, switchCount.value);
 | 
				
			||||||
 | 
					  if (props.enableDigitalTwin) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const client = getClient();
 | 
				
			||||||
 | 
					      if (!isUndefined(index))
 | 
				
			||||||
 | 
					        client.setSwitchOnOff(index + 1, newStates[index]);
 | 
				
			||||||
 | 
					      else client.setMultiSwitchsOnOff(btnStatus.value);
 | 
				
			||||||
 | 
					    } catch (error: any) {}
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function toggleBtnStatus(btnNum: number): void {
 | 
					// 切换单个
 | 
				
			||||||
  if (btnNum >= 0 && btnNum < btnStatus.value.length) {
 | 
					function toggleBtnStatus(idx: number) {
 | 
				
			||||||
    btnStatus.value[btnNum] = !btnStatus.value[btnNum];
 | 
					  if (idx < 0 || idx >= btnStatus.value.length) return;
 | 
				
			||||||
    emit('switch-toggle', { 
 | 
					  const newStates = [...btnStatus.value];
 | 
				
			||||||
      index: btnNum, 
 | 
					  newStates[idx] = !newStates[idx];
 | 
				
			||||||
      value: btnStatus.value[btnNum], 
 | 
					  updateStatus(newStates, idx);
 | 
				
			||||||
      states: [...btnStatus.value] 
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 一次性设置所有开关状态
 | 
					// 单个设置
 | 
				
			||||||
function setAllStates(states: boolean[]): void {
 | 
					function setBtnStatus(idx: number, isOn: boolean) {
 | 
				
			||||||
  const newStates = states.slice(0, props.switchCount);
 | 
					  if (idx < 0 || idx >= btnStatus.value.length) return;
 | 
				
			||||||
  while (newStates.length < props.switchCount) {
 | 
					  const newStates = [...btnStatus.value];
 | 
				
			||||||
    newStates.push(false);
 | 
					  newStates[idx] = isOn;
 | 
				
			||||||
  }
 | 
					  updateStatus(newStates, idx);
 | 
				
			||||||
  btnStatus.value = newStates;
 | 
					 | 
				
			||||||
  emit('change', { states: [...btnStatus.value] });
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 暴露组件方法和状态
 | 
					// 监听 props 变化只同步一次
 | 
				
			||||||
defineExpose({
 | 
					watch(
 | 
				
			||||||
  setBtnStatus,
 | 
					  () => props.enableDigitalTwin,
 | 
				
			||||||
  toggleBtnStatus,
 | 
					  (newVal) => {
 | 
				
			||||||
  setAllStates,
 | 
					    const client = getClient();
 | 
				
			||||||
  getBtnStatus: () => [...btnStatus.value]
 | 
					    client.setEnable(newVal);
 | 
				
			||||||
});
 | 
					  },
 | 
				
			||||||
 | 
					  { immediate: true },
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					watch(
 | 
				
			||||||
 | 
					  () => [switchCount.value, props.initialValues],
 | 
				
			||||||
 | 
					  () => {
 | 
				
			||||||
 | 
					    btnStatus.value = parseInitialValues();
 | 
				
			||||||
 | 
					    updateStatus(btnStatus.value);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style scoped lang="postcss">
 | 
					<style scoped lang="postcss">
 | 
				
			||||||
@@ -194,17 +214,27 @@ defineExpose({
 | 
				
			|||||||
  display: block;
 | 
					  display: block;
 | 
				
			||||||
  padding: 0;
 | 
					  padding: 0;
 | 
				
			||||||
  margin: 0;
 | 
					  margin: 0;
 | 
				
			||||||
  line-height: 0; /* 移除行高导致的额外间距 */
 | 
					  line-height: 0;
 | 
				
			||||||
  font-size: 0; /* 防止文本节点造成的间距 */
 | 
					  font-size: 0;
 | 
				
			||||||
  box-sizing: content-box;
 | 
					  box-sizing: content-box;
 | 
				
			||||||
  overflow: visible;
 | 
					  overflow: visible;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
rect {
 | 
					rect {
 | 
				
			||||||
  transition: all 100ms ease-in-out;
 | 
					  transition: all 100ms ease-in-out;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
.interactive {
 | 
					.interactive {
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					export function getDefaultProps() {
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    size: 1,
 | 
				
			||||||
 | 
					    enableDigitalTwin: false,
 | 
				
			||||||
 | 
					    switchCount: 6,
 | 
				
			||||||
 | 
					    initialValues: "",
 | 
				
			||||||
 | 
					    showLabels: true,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,15 +7,28 @@ import { isNumber } from "mathjs";
 | 
				
			|||||||
import { Mutex, withTimeout } from "async-mutex";
 | 
					import { Mutex, withTimeout } from "async-mutex";
 | 
				
			||||||
import { useConstraintsStore } from "@/stores/constraints";
 | 
					import { useConstraintsStore } from "@/stores/constraints";
 | 
				
			||||||
import { useDialogStore } from "./dialog";
 | 
					import { useDialogStore } from "./dialog";
 | 
				
			||||||
import { toFileParameterOrUndefined } from "@/utils/Common";
 | 
					import {
 | 
				
			||||||
 | 
					  base64ToArrayBuffer,
 | 
				
			||||||
 | 
					  toFileParameterOrUndefined,
 | 
				
			||||||
 | 
					} from "@/utils/Common";
 | 
				
			||||||
import { AuthManager } from "@/utils/AuthManager";
 | 
					import { AuthManager } from "@/utils/AuthManager";
 | 
				
			||||||
import { HubConnection, HubConnectionBuilder } from "@microsoft/signalr";
 | 
					import { HubConnection, HubConnectionState } from "@microsoft/signalr";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  getHubProxyFactory,
 | 
					  getHubProxyFactory,
 | 
				
			||||||
  getReceiverRegister,
 | 
					  getReceiverRegister,
 | 
				
			||||||
} from "@/utils/signalR/TypedSignalR.Client";
 | 
					} from "@/utils/signalR/TypedSignalR.Client";
 | 
				
			||||||
import { ResourcePurpose, type ResourceInfo } from "@/APIClient";
 | 
					import {
 | 
				
			||||||
import type { IJtagHub } from "@/utils/signalR/TypedSignalR.Client/server.Hubs";
 | 
					  JtagClient,
 | 
				
			||||||
 | 
					  MatrixKeyClient,
 | 
				
			||||||
 | 
					  PowerClient,
 | 
				
			||||||
 | 
					  ResourceClient,
 | 
				
			||||||
 | 
					  ResourcePurpose,
 | 
				
			||||||
 | 
					  type ResourceInfo,
 | 
				
			||||||
 | 
					} from "@/APIClient";
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
					  IDigitalTubesHub,
 | 
				
			||||||
 | 
					  IJtagHub,
 | 
				
			||||||
 | 
					} from "@/utils/signalR/TypedSignalR.Client/server.Hubs";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useEquipments = defineStore("equipments", () => {
 | 
					export const useEquipments = defineStore("equipments", () => {
 | 
				
			||||||
  // Global Stores
 | 
					  // Global Stores
 | 
				
			||||||
@@ -26,6 +39,7 @@ export const useEquipments = defineStore("equipments", () => {
 | 
				
			|||||||
  const boardPort = useLocalStorage("fpga-board-port", 1234);
 | 
					  const boardPort = useLocalStorage("fpga-board-port", 1234);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Jtag
 | 
					  // Jtag
 | 
				
			||||||
 | 
					  const enableJtagBoundaryScan = ref(false);
 | 
				
			||||||
  const jtagBitstream = ref<File>();
 | 
					  const jtagBitstream = ref<File>();
 | 
				
			||||||
  const jtagBoundaryScanFreq = ref(100);
 | 
					  const jtagBoundaryScanFreq = ref(100);
 | 
				
			||||||
  const jtagUserBitstreams = ref<ResourceInfo[]>([]);
 | 
					  const jtagUserBitstreams = ref<ResourceInfo[]>([]);
 | 
				
			||||||
@@ -39,8 +53,7 @@ export const useEquipments = defineStore("equipments", () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  onMounted(async () => {
 | 
					  onMounted(async () => {
 | 
				
			||||||
    // 每次挂载都重新创建连接
 | 
					    // 每次挂载都重新创建连接
 | 
				
			||||||
    jtagHubConnection.value =
 | 
					    jtagHubConnection.value = AuthManager.createHubConnection("JtagHub");
 | 
				
			||||||
      AuthManager.createAuthenticatedJtagHubConnection();
 | 
					 | 
				
			||||||
    jtagHubProxy.value = getHubProxyFactory("IJtagHub").createHubProxy(
 | 
					    jtagHubProxy.value = getHubProxyFactory("IJtagHub").createHubProxy(
 | 
				
			||||||
      jtagHubConnection.value,
 | 
					      jtagHubConnection.value,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
@@ -62,46 +75,6 @@ export const useEquipments = defineStore("equipments", () => {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Matrix Key
 | 
					 | 
				
			||||||
  const matrixKeyStates = reactive(new Array<boolean>(16).fill(false));
 | 
					 | 
				
			||||||
  const matrixKeypadClientMutex = withTimeout(
 | 
					 | 
				
			||||||
    new Mutex(),
 | 
					 | 
				
			||||||
    1000,
 | 
					 | 
				
			||||||
    new Error("Matrixkeyclient Mutex Timeout!"),
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Power
 | 
					 | 
				
			||||||
  const powerClientMutex = withTimeout(
 | 
					 | 
				
			||||||
    new Mutex(),
 | 
					 | 
				
			||||||
    1000,
 | 
					 | 
				
			||||||
    new Error("Matrixkeyclient Mutex Timeout!"),
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Enable Setting
 | 
					 | 
				
			||||||
  const enableJtagBoundaryScan = ref(false);
 | 
					 | 
				
			||||||
  const enableMatrixKey = ref(false);
 | 
					 | 
				
			||||||
  const enablePower = ref(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  function setMatrixKey(
 | 
					 | 
				
			||||||
    keyNum: number | string | undefined,
 | 
					 | 
				
			||||||
    keyValue: boolean,
 | 
					 | 
				
			||||||
  ): boolean {
 | 
					 | 
				
			||||||
    let _keyNum: number;
 | 
					 | 
				
			||||||
    if (isString(keyNum)) {
 | 
					 | 
				
			||||||
      _keyNum = toNumber(keyNum);
 | 
					 | 
				
			||||||
    } else if (isNumber(keyNum)) {
 | 
					 | 
				
			||||||
      _keyNum = keyNum;
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      return false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (z.number().nonnegative().max(16).safeParse(_keyNum).success) {
 | 
					 | 
				
			||||||
      matrixKeyStates[_keyNum] = keyValue;
 | 
					 | 
				
			||||||
      return true;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return false;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async function jtagBoundaryScanSetOnOff(enable: boolean) {
 | 
					  async function jtagBoundaryScanSetOnOff(enable: boolean) {
 | 
				
			||||||
    if (isUndefined(jtagHubProxy.value)) {
 | 
					    if (isUndefined(jtagHubProxy.value)) {
 | 
				
			||||||
      console.error("JtagHub Not Initialize...");
 | 
					      console.error("JtagHub Not Initialize...");
 | 
				
			||||||
@@ -134,7 +107,7 @@ export const useEquipments = defineStore("equipments", () => {
 | 
				
			|||||||
      // 自动开启电源
 | 
					      // 自动开启电源
 | 
				
			||||||
      await powerSetOnOff(true);
 | 
					      await powerSetOnOff(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const resourceClient = AuthManager.createAuthenticatedResourceClient();
 | 
					      const resourceClient = AuthManager.createClient(ResourceClient);
 | 
				
			||||||
      const resp = await resourceClient.addResource(
 | 
					      const resp = await resourceClient.addResource(
 | 
				
			||||||
        "bitstream",
 | 
					        "bitstream",
 | 
				
			||||||
        ResourcePurpose.User,
 | 
					        ResourcePurpose.User,
 | 
				
			||||||
@@ -166,7 +139,7 @@ export const useEquipments = defineStore("equipments", () => {
 | 
				
			|||||||
      // 自动开启电源
 | 
					      // 自动开启电源
 | 
				
			||||||
      await powerSetOnOff(true);
 | 
					      await powerSetOnOff(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const jtagClient = AuthManager.createAuthenticatedJtagClient();
 | 
					      const jtagClient = AuthManager.createClient(JtagClient);
 | 
				
			||||||
      const resp = await jtagClient.downloadBitstream(
 | 
					      const resp = await jtagClient.downloadBitstream(
 | 
				
			||||||
        boardAddr.value,
 | 
					        boardAddr.value,
 | 
				
			||||||
        boardPort.value,
 | 
					        boardPort.value,
 | 
				
			||||||
@@ -188,7 +161,7 @@ export const useEquipments = defineStore("equipments", () => {
 | 
				
			|||||||
      // 自动开启电源
 | 
					      // 自动开启电源
 | 
				
			||||||
      await powerSetOnOff(true);
 | 
					      await powerSetOnOff(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const jtagClient = AuthManager.createAuthenticatedJtagClient();
 | 
					      const jtagClient = AuthManager.createClient(JtagClient);
 | 
				
			||||||
      const resp = await jtagClient.getDeviceIDCode(
 | 
					      const resp = await jtagClient.getDeviceIDCode(
 | 
				
			||||||
        boardAddr.value,
 | 
					        boardAddr.value,
 | 
				
			||||||
        boardPort.value,
 | 
					        boardPort.value,
 | 
				
			||||||
@@ -208,7 +181,7 @@ export const useEquipments = defineStore("equipments", () => {
 | 
				
			|||||||
      // 自动开启电源
 | 
					      // 自动开启电源
 | 
				
			||||||
      await powerSetOnOff(true);
 | 
					      await powerSetOnOff(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const jtagClient = AuthManager.createAuthenticatedJtagClient();
 | 
					      const jtagClient = AuthManager.createClient(JtagClient);
 | 
				
			||||||
      const resp = await jtagClient.setSpeed(
 | 
					      const resp = await jtagClient.setSpeed(
 | 
				
			||||||
        boardAddr.value,
 | 
					        boardAddr.value,
 | 
				
			||||||
        boardPort.value,
 | 
					        boardPort.value,
 | 
				
			||||||
@@ -223,12 +196,38 @@ export const useEquipments = defineStore("equipments", () => {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Matrix Key
 | 
				
			||||||
 | 
					  const enableMatrixKey = ref(false);
 | 
				
			||||||
 | 
					  const matrixKeyStates = reactive(new Array<boolean>(16).fill(false));
 | 
				
			||||||
 | 
					  const matrixKeypadClientMutex = withTimeout(
 | 
				
			||||||
 | 
					    new Mutex(),
 | 
				
			||||||
 | 
					    1000,
 | 
				
			||||||
 | 
					    new Error("Matrixkeyclient Mutex Timeout!"),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  function setMatrixKey(
 | 
				
			||||||
 | 
					    keyNum: number | string | undefined,
 | 
				
			||||||
 | 
					    keyValue: boolean,
 | 
				
			||||||
 | 
					  ): boolean {
 | 
				
			||||||
 | 
					    let _keyNum: number;
 | 
				
			||||||
 | 
					    if (isString(keyNum)) {
 | 
				
			||||||
 | 
					      _keyNum = toNumber(keyNum);
 | 
				
			||||||
 | 
					    } else if (isNumber(keyNum)) {
 | 
				
			||||||
 | 
					      _keyNum = keyNum;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (z.number().nonnegative().max(16).safeParse(_keyNum).success) {
 | 
				
			||||||
 | 
					      matrixKeyStates[_keyNum] = keyValue;
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async function matrixKeypadSetKeyStates(keyStates: boolean[]) {
 | 
					  async function matrixKeypadSetKeyStates(keyStates: boolean[]) {
 | 
				
			||||||
    const release = await matrixKeypadClientMutex.acquire();
 | 
					    const release = await matrixKeypadClientMutex.acquire();
 | 
				
			||||||
    console.log("set Key !!!!!!!!!!!!");
 | 
					 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const matrixKeypadClient =
 | 
					      const matrixKeypadClient = AuthManager.createClient(MatrixKeyClient);
 | 
				
			||||||
        AuthManager.createAuthenticatedMatrixKeyClient();
 | 
					 | 
				
			||||||
      const resp = await matrixKeypadClient.setMatrixKeyStatus(
 | 
					      const resp = await matrixKeypadClient.setMatrixKeyStatus(
 | 
				
			||||||
        boardAddr.value,
 | 
					        boardAddr.value,
 | 
				
			||||||
        boardPort.value,
 | 
					        boardPort.value,
 | 
				
			||||||
@@ -246,9 +245,8 @@ export const useEquipments = defineStore("equipments", () => {
 | 
				
			|||||||
  async function matrixKeypadEnable(enable: boolean) {
 | 
					  async function matrixKeypadEnable(enable: boolean) {
 | 
				
			||||||
    const release = await matrixKeypadClientMutex.acquire();
 | 
					    const release = await matrixKeypadClientMutex.acquire();
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
 | 
					      const matrixKeypadClient = AuthManager.createClient(MatrixKeyClient);
 | 
				
			||||||
      if (enable) {
 | 
					      if (enable) {
 | 
				
			||||||
        const matrixKeypadClient =
 | 
					 | 
				
			||||||
          AuthManager.createAuthenticatedMatrixKeyClient();
 | 
					 | 
				
			||||||
        const resp = await matrixKeypadClient.enabelMatrixKey(
 | 
					        const resp = await matrixKeypadClient.enabelMatrixKey(
 | 
				
			||||||
          boardAddr.value,
 | 
					          boardAddr.value,
 | 
				
			||||||
          boardPort.value,
 | 
					          boardPort.value,
 | 
				
			||||||
@@ -256,8 +254,6 @@ export const useEquipments = defineStore("equipments", () => {
 | 
				
			|||||||
        enableMatrixKey.value = resp;
 | 
					        enableMatrixKey.value = resp;
 | 
				
			||||||
        return resp;
 | 
					        return resp;
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        const matrixKeypadClient =
 | 
					 | 
				
			||||||
          AuthManager.createAuthenticatedMatrixKeyClient();
 | 
					 | 
				
			||||||
        const resp = await matrixKeypadClient.disableMatrixKey(
 | 
					        const resp = await matrixKeypadClient.disableMatrixKey(
 | 
				
			||||||
          boardAddr.value,
 | 
					          boardAddr.value,
 | 
				
			||||||
          boardPort.value,
 | 
					          boardPort.value,
 | 
				
			||||||
@@ -274,10 +270,17 @@ export const useEquipments = defineStore("equipments", () => {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Power
 | 
				
			||||||
 | 
					  const powerClientMutex = withTimeout(
 | 
				
			||||||
 | 
					    new Mutex(),
 | 
				
			||||||
 | 
					    1000,
 | 
				
			||||||
 | 
					    new Error("Matrixkeyclient Mutex Timeout!"),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const enablePower = ref(false);
 | 
				
			||||||
  async function powerSetOnOff(enable: boolean) {
 | 
					  async function powerSetOnOff(enable: boolean) {
 | 
				
			||||||
    const release = await powerClientMutex.acquire();
 | 
					    const release = await powerClientMutex.acquire();
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const powerClient = AuthManager.createAuthenticatedPowerClient();
 | 
					      const powerClient = AuthManager.createClient(PowerClient);
 | 
				
			||||||
      const resp = await powerClient.setPowerOnOff(
 | 
					      const resp = await powerClient.setPowerOnOff(
 | 
				
			||||||
        boardAddr.value,
 | 
					        boardAddr.value,
 | 
				
			||||||
        boardPort.value,
 | 
					        boardPort.value,
 | 
				
			||||||
@@ -293,6 +296,74 @@ export const useEquipments = defineStore("equipments", () => {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Seven Segment Display
 | 
				
			||||||
 | 
					  const enableSevenSegmentDisplay = ref(false);
 | 
				
			||||||
 | 
					  const sevenSegmentDisplayFrequency = ref(100);
 | 
				
			||||||
 | 
					  const sevenSegmentDisplayData = ref<Uint8Array>();
 | 
				
			||||||
 | 
					  const sevenSegmentDisplayHub = ref<HubConnection>();
 | 
				
			||||||
 | 
					  const sevenSegmentDisplayHubProxy = ref<IDigitalTubesHub>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async function sevenSegmentDisplaySetOnOff(enable: boolean) {
 | 
				
			||||||
 | 
					    if (!sevenSegmentDisplayHub.value || !sevenSegmentDisplayHubProxy.value)
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    if (sevenSegmentDisplayHub.value.state === HubConnectionState.Disconnected)
 | 
				
			||||||
 | 
					      await sevenSegmentDisplayHub.value.start();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (enable) {
 | 
				
			||||||
 | 
					      await sevenSegmentDisplayHubProxy.value.startScan();
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      await sevenSegmentDisplayHubProxy.value.stopScan();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async function sevenSegmentDisplaySetFrequency(frequency: number) {
 | 
				
			||||||
 | 
					    if (!sevenSegmentDisplayHub.value || !sevenSegmentDisplayHubProxy.value)
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    if (sevenSegmentDisplayHub.value.state === HubConnectionState.Disconnected)
 | 
				
			||||||
 | 
					      await sevenSegmentDisplayHub.value.start();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await sevenSegmentDisplayHubProxy.value.setFrequency(frequency);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async function sevenSegmentDisplayGetStatus() {
 | 
				
			||||||
 | 
					    if (!sevenSegmentDisplayHub.value || !sevenSegmentDisplayHubProxy.value)
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    if (sevenSegmentDisplayHub.value.state === HubConnectionState.Disconnected)
 | 
				
			||||||
 | 
					      await sevenSegmentDisplayHub.value.start();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return await sevenSegmentDisplayHubProxy.value.getStatus();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async function handleSevenSegmentDisplayOnReceive(msg: string) {
 | 
				
			||||||
 | 
					    const bytes = base64ToArrayBuffer(msg);
 | 
				
			||||||
 | 
					    sevenSegmentDisplayData.value = new Uint8Array(bytes);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onMounted(async () => {
 | 
				
			||||||
 | 
					    // 每次挂载都重新创建连接
 | 
				
			||||||
 | 
					    sevenSegmentDisplayHub.value =
 | 
				
			||||||
 | 
					      AuthManager.createHubConnection("DigitalTubesHub");
 | 
				
			||||||
 | 
					    sevenSegmentDisplayHubProxy.value = getHubProxyFactory(
 | 
				
			||||||
 | 
					      "IDigitalTubesHub",
 | 
				
			||||||
 | 
					    ).createHubProxy(sevenSegmentDisplayHub.value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    getReceiverRegister("IDigitalTubesReceiver").register(
 | 
				
			||||||
 | 
					      sevenSegmentDisplayHub.value,
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        onReceive: handleSevenSegmentDisplayOnReceive,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onUnmounted(() => {
 | 
				
			||||||
 | 
					    // 断开连接,清理资源
 | 
				
			||||||
 | 
					    if (sevenSegmentDisplayHub.value) {
 | 
				
			||||||
 | 
					      sevenSegmentDisplayHub.value.stop();
 | 
				
			||||||
 | 
					      sevenSegmentDisplayHub.value = undefined;
 | 
				
			||||||
 | 
					      sevenSegmentDisplayHubProxy.value = undefined;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    boardAddr,
 | 
					    boardAddr,
 | 
				
			||||||
    boardPort,
 | 
					    boardPort,
 | 
				
			||||||
@@ -320,5 +391,13 @@ export const useEquipments = defineStore("equipments", () => {
 | 
				
			|||||||
    enablePower,
 | 
					    enablePower,
 | 
				
			||||||
    powerClientMutex,
 | 
					    powerClientMutex,
 | 
				
			||||||
    powerSetOnOff,
 | 
					    powerSetOnOff,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Seven Segment Display
 | 
				
			||||||
 | 
					    enableSevenSegmentDisplay,
 | 
				
			||||||
 | 
					    sevenSegmentDisplayData,
 | 
				
			||||||
 | 
					    sevenSegmentDisplayFrequency,
 | 
				
			||||||
 | 
					    sevenSegmentDisplaySetOnOff,
 | 
				
			||||||
 | 
					    sevenSegmentDisplaySetFrequency,
 | 
				
			||||||
 | 
					    sevenSegmentDisplayGetStatus,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										83
									
								
								src/stores/progress.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/stores/progress.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,83 @@
 | 
				
			|||||||
 | 
					import type { HubConnection } from "@microsoft/signalr";
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
					  IProgressHub,
 | 
				
			||||||
 | 
					  IProgressReceiver,
 | 
				
			||||||
 | 
					} from "@/utils/signalR/TypedSignalR.Client/server.Hubs";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  getHubProxyFactory,
 | 
				
			||||||
 | 
					  getReceiverRegister,
 | 
				
			||||||
 | 
					} from "@/utils/signalR/TypedSignalR.Client";
 | 
				
			||||||
 | 
					import { ProgressStatus, type ProgressInfo } from "@/utils/signalR/server.Hubs";
 | 
				
			||||||
 | 
					import { onMounted, onUnmounted, ref, shallowRef } from "vue";
 | 
				
			||||||
 | 
					import { defineStore } from "pinia";
 | 
				
			||||||
 | 
					import { AuthManager } from "@/utils/AuthManager";
 | 
				
			||||||
 | 
					import { forEach, isUndefined } from "lodash";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type ProgressCallback = (msg: ProgressInfo) => void;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useProgressStore = defineStore("progress", () => {
 | 
				
			||||||
 | 
					  // taskId -> name -> callback
 | 
				
			||||||
 | 
					  const progressCallbackFuncs = shallowRef<
 | 
				
			||||||
 | 
					    Map<string, Map<string, ProgressCallback>>
 | 
				
			||||||
 | 
					  >(new Map());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const progressHubConnection = shallowRef<HubConnection>();
 | 
				
			||||||
 | 
					  const progressHubProxy = shallowRef<IProgressHub>();
 | 
				
			||||||
 | 
					  const progressHubReceiver: IProgressReceiver = {
 | 
				
			||||||
 | 
					    onReceiveProgress: async (msg) => {
 | 
				
			||||||
 | 
					      const taskMap = progressCallbackFuncs.value.get(msg.taskId);
 | 
				
			||||||
 | 
					      if (taskMap) {
 | 
				
			||||||
 | 
					        for (const func of taskMap.values()) {
 | 
				
			||||||
 | 
					          func(msg);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onMounted(async () => {
 | 
				
			||||||
 | 
					    progressHubConnection.value =
 | 
				
			||||||
 | 
					      AuthManager.createHubConnection("ProgressHub");
 | 
				
			||||||
 | 
					    progressHubProxy.value = getHubProxyFactory("IProgressHub").createHubProxy(
 | 
				
			||||||
 | 
					      progressHubConnection.value,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    getReceiverRegister("IProgressReceiver").register(
 | 
				
			||||||
 | 
					      progressHubConnection.value,
 | 
				
			||||||
 | 
					      progressHubReceiver,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    progressHubConnection.value.start();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onUnmounted(() => {
 | 
				
			||||||
 | 
					    if (progressHubConnection.value) {
 | 
				
			||||||
 | 
					      progressHubConnection.value.stop();
 | 
				
			||||||
 | 
					      progressHubConnection.value = undefined;
 | 
				
			||||||
 | 
					      progressHubProxy.value = undefined;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function register(progressId: string, name: string, func: ProgressCallback) {
 | 
				
			||||||
 | 
					    progressHubProxy.value?.join(progressId);
 | 
				
			||||||
 | 
					    let taskMap = progressCallbackFuncs.value.get(progressId);
 | 
				
			||||||
 | 
					    if (!taskMap) {
 | 
				
			||||||
 | 
					      taskMap = new Map();
 | 
				
			||||||
 | 
					      progressCallbackFuncs.value?.set(progressId, taskMap);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    taskMap.set(name, func);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function unregister(taskId: string, name: string) {
 | 
				
			||||||
 | 
					    progressHubProxy.value?.leave(taskId);
 | 
				
			||||||
 | 
					    const taskMap = progressCallbackFuncs.value.get(taskId);
 | 
				
			||||||
 | 
					    if (taskMap) {
 | 
				
			||||||
 | 
					      taskMap.delete(name);
 | 
				
			||||||
 | 
					      if (taskMap.size === 0) {
 | 
				
			||||||
 | 
					        progressCallbackFuncs.value?.delete(taskId);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    register,
 | 
				
			||||||
 | 
					    unregister,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@@ -1,313 +1,105 @@
 | 
				
			|||||||
import {
 | 
					import { DataClient } from "@/APIClient";
 | 
				
			||||||
  DataClient,
 | 
					 | 
				
			||||||
  VideoStreamClient,
 | 
					 | 
				
			||||||
  BsdlParserClient,
 | 
					 | 
				
			||||||
  DDSClient,
 | 
					 | 
				
			||||||
  JtagClient,
 | 
					 | 
				
			||||||
  MatrixKeyClient,
 | 
					 | 
				
			||||||
  PowerClient,
 | 
					 | 
				
			||||||
  RemoteUpdateClient,
 | 
					 | 
				
			||||||
  TutorialClient,
 | 
					 | 
				
			||||||
  UDPClient,
 | 
					 | 
				
			||||||
  LogicAnalyzerClient,
 | 
					 | 
				
			||||||
  NetConfigClient,
 | 
					 | 
				
			||||||
  OscilloscopeApiClient,
 | 
					 | 
				
			||||||
  DebuggerClient,
 | 
					 | 
				
			||||||
  ExamClient,
 | 
					 | 
				
			||||||
  ResourceClient,
 | 
					 | 
				
			||||||
  HdmiVideoStreamClient,
 | 
					 | 
				
			||||||
} from "@/APIClient";
 | 
					 | 
				
			||||||
import router from "@/router";
 | 
					 | 
				
			||||||
import { HubConnectionBuilder } from "@microsoft/signalr";
 | 
					import { HubConnectionBuilder } from "@microsoft/signalr";
 | 
				
			||||||
import axios, { type AxiosInstance } from "axios";
 | 
					import axios, { type AxiosInstance } from "axios";
 | 
				
			||||||
import { isNull } from "lodash";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 支持的客户端类型联合类型
 | 
					 | 
				
			||||||
type SupportedClient =
 | 
					 | 
				
			||||||
  | DataClient
 | 
					 | 
				
			||||||
  | VideoStreamClient
 | 
					 | 
				
			||||||
  | BsdlParserClient
 | 
					 | 
				
			||||||
  | DDSClient
 | 
					 | 
				
			||||||
  | JtagClient
 | 
					 | 
				
			||||||
  | MatrixKeyClient
 | 
					 | 
				
			||||||
  | PowerClient
 | 
					 | 
				
			||||||
  | RemoteUpdateClient
 | 
					 | 
				
			||||||
  | TutorialClient
 | 
					 | 
				
			||||||
  | LogicAnalyzerClient
 | 
					 | 
				
			||||||
  | UDPClient
 | 
					 | 
				
			||||||
  | NetConfigClient
 | 
					 | 
				
			||||||
  | OscilloscopeApiClient
 | 
					 | 
				
			||||||
  | DebuggerClient
 | 
					 | 
				
			||||||
  | ExamClient
 | 
					 | 
				
			||||||
  | ResourceClient
 | 
					 | 
				
			||||||
  | HdmiVideoStreamClient;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 简单到让人想哭的认证管理器
 | 
				
			||||||
export class AuthManager {
 | 
					export class AuthManager {
 | 
				
			||||||
  // 存储token到localStorage
 | 
					  private static readonly TOKEN_KEY = "authToken";
 | 
				
			||||||
  public static setToken(token: string): void {
 | 
					
 | 
				
			||||||
    localStorage.setItem("authToken", token);
 | 
					  // 核心数据:就是个字符串
 | 
				
			||||||
 | 
					  static getToken(): string | null {
 | 
				
			||||||
 | 
					    return localStorage.getItem(this.TOKEN_KEY);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 从localStorage获取token
 | 
					  static setToken(token: string): void {
 | 
				
			||||||
  public static getToken(): string | null {
 | 
					    localStorage.setItem(this.TOKEN_KEY, token);
 | 
				
			||||||
    return localStorage.getItem("authToken");
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 清除token
 | 
					  static clearToken(): void {
 | 
				
			||||||
  public static clearToken(): void {
 | 
					    localStorage.removeItem(this.TOKEN_KEY);
 | 
				
			||||||
    localStorage.removeItem("authToken");
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 检查是否已认证
 | 
					  // 核心功能:创建带认证的HTTP配置
 | 
				
			||||||
  public static async isAuthenticated(): Promise<boolean> {
 | 
					  static getAuthHeaders(): Record<string, string> {
 | 
				
			||||||
    return await AuthManager.verifyToken();
 | 
					    const token = this.getToken();
 | 
				
			||||||
 | 
					    return token ? { Authorization: `Bearer ${token}` } : {};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 通用的为HTTP请求添加Authorization header的方法
 | 
					  // 一个方法搞定所有客户端,不要17个垃圾方法
 | 
				
			||||||
  public static addAuthHeader(client: SupportedClient): void {
 | 
					  static createClient<T>(
 | 
				
			||||||
    const token = AuthManager.getToken();
 | 
					    ClientClass: new (baseUrl?: string, config?: any) => T,
 | 
				
			||||||
    if (token) {
 | 
					    baseUrl?: string,
 | 
				
			||||||
      // 创建一个自定义的 http 对象,包装原有的 fetch 方法
 | 
					 | 
				
			||||||
      const customHttp = {
 | 
					 | 
				
			||||||
        fetch: (url: RequestInfo, init?: RequestInit) => {
 | 
					 | 
				
			||||||
          if (!init) init = {};
 | 
					 | 
				
			||||||
          if (!init.headers) init.headers = {};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          // 添加Authorization header
 | 
					 | 
				
			||||||
          if (typeof init.headers === "object" && init.headers !== null) {
 | 
					 | 
				
			||||||
            (init.headers as any)["Authorization"] = `Bearer ${token}`;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          // 使用全局 fetch 或 window.fetch
 | 
					 | 
				
			||||||
          return (window as any).fetch(url, init);
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // 重新构造客户端,传入自定义的 http 对象
 | 
					 | 
				
			||||||
      const ClientClass = client.constructor as new (
 | 
					 | 
				
			||||||
        baseUrl?: string,
 | 
					 | 
				
			||||||
        http?: any,
 | 
					 | 
				
			||||||
      ) => SupportedClient;
 | 
					 | 
				
			||||||
      const newClient = new ClientClass(undefined, customHttp);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // 将新客户端的属性复制到原客户端(这是一个 workaround)
 | 
					 | 
				
			||||||
      // 更好的做法是返回新的客户端实例
 | 
					 | 
				
			||||||
      Object.setPrototypeOf(client, Object.getPrototypeOf(newClient));
 | 
					 | 
				
			||||||
      Object.assign(client, newClient);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 私有方法:创建带认证的HTTP客户端
 | 
					 | 
				
			||||||
  private static createAuthenticatedHttp() {
 | 
					 | 
				
			||||||
    const token = AuthManager.getToken();
 | 
					 | 
				
			||||||
    if (!token) {
 | 
					 | 
				
			||||||
      return null;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      fetch: (url: RequestInfo, init?: RequestInit) => {
 | 
					 | 
				
			||||||
        if (!init) init = {};
 | 
					 | 
				
			||||||
        if (!init.headers) init.headers = {};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (typeof init.headers === "object" && init.headers !== null) {
 | 
					 | 
				
			||||||
          (init.headers as any)["Authorization"] = `Bearer ${token}`;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return (window as any).fetch(url, init);
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 私有方法:创建带认证的Axios实例
 | 
					 | 
				
			||||||
  private static createAuthenticatedAxiosInstance(): AxiosInstance | null {
 | 
					 | 
				
			||||||
    const token = AuthManager.getToken();
 | 
					 | 
				
			||||||
    if (!token) return null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const instance = axios.create();
 | 
					 | 
				
			||||||
    instance.interceptors.request.use((config) => {
 | 
					 | 
				
			||||||
      config.headers = config.headers || {};
 | 
					 | 
				
			||||||
      (config.headers as any)["Authorization"] = `Bearer ${token}`;
 | 
					 | 
				
			||||||
      return config;
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    return instance;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 通用的创建已认证客户端的方法(使用泛型)
 | 
					 | 
				
			||||||
  public static createAuthenticatedClient<T extends SupportedClient>(
 | 
					 | 
				
			||||||
    ClientClass: new (baseUrl?: string, instance?: AxiosInstance) => T,
 | 
					 | 
				
			||||||
  ): T {
 | 
					  ): T {
 | 
				
			||||||
    const axiosInstance = AuthManager.createAuthenticatedAxiosInstance();
 | 
					    const token = this.getToken();
 | 
				
			||||||
    return axiosInstance
 | 
					    if (!token) {
 | 
				
			||||||
      ? new ClientClass(undefined, axiosInstance)
 | 
					      return new ClientClass(baseUrl);
 | 
				
			||||||
      : new ClientClass();
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 对于axios客户端
 | 
				
			||||||
 | 
					    const axiosInstance = axios.create({
 | 
				
			||||||
 | 
					      headers: this.getAuthHeaders(),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return new ClientClass(baseUrl, axiosInstance);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 便捷方法:创建已配置认证的各种客户端
 | 
					  // SignalR连接 - 简单明了
 | 
				
			||||||
  public static createAuthenticatedDataClient(): DataClient {
 | 
					  static createHubConnection(
 | 
				
			||||||
    return AuthManager.createAuthenticatedClient(DataClient);
 | 
					    hubPath: "ProgressHub" | "JtagHub" | "DigitalTubesHub",
 | 
				
			||||||
  }
 | 
					  ) {
 | 
				
			||||||
 | 
					 | 
				
			||||||
  public static createAuthenticatedVideoStreamClient(): VideoStreamClient {
 | 
					 | 
				
			||||||
    return AuthManager.createAuthenticatedClient(VideoStreamClient);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  public static createAuthenticatedBsdlParserClient(): BsdlParserClient {
 | 
					 | 
				
			||||||
    return AuthManager.createAuthenticatedClient(BsdlParserClient);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  public static createAuthenticatedDDSClient(): DDSClient {
 | 
					 | 
				
			||||||
    return AuthManager.createAuthenticatedClient(DDSClient);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  public static createAuthenticatedJtagClient(): JtagClient {
 | 
					 | 
				
			||||||
    return AuthManager.createAuthenticatedClient(JtagClient);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  public static createAuthenticatedMatrixKeyClient(): MatrixKeyClient {
 | 
					 | 
				
			||||||
    return AuthManager.createAuthenticatedClient(MatrixKeyClient);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  public static createAuthenticatedPowerClient(): PowerClient {
 | 
					 | 
				
			||||||
    return AuthManager.createAuthenticatedClient(PowerClient);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  public static createAuthenticatedRemoteUpdateClient(): RemoteUpdateClient {
 | 
					 | 
				
			||||||
    return AuthManager.createAuthenticatedClient(RemoteUpdateClient);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  public static createAuthenticatedTutorialClient(): TutorialClient {
 | 
					 | 
				
			||||||
    return AuthManager.createAuthenticatedClient(TutorialClient);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  public static createAuthenticatedUDPClient(): UDPClient {
 | 
					 | 
				
			||||||
    return AuthManager.createAuthenticatedClient(UDPClient);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  public static createAuthenticatedLogicAnalyzerClient(): LogicAnalyzerClient {
 | 
					 | 
				
			||||||
    return AuthManager.createAuthenticatedClient(LogicAnalyzerClient);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  public static createAuthenticatedNetConfigClient(): NetConfigClient {
 | 
					 | 
				
			||||||
    return AuthManager.createAuthenticatedClient(NetConfigClient);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  public static createAuthenticatedOscilloscopeApiClient(): OscilloscopeApiClient {
 | 
					 | 
				
			||||||
    return AuthManager.createAuthenticatedClient(OscilloscopeApiClient);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  public static createAuthenticatedDebuggerClient(): DebuggerClient {
 | 
					 | 
				
			||||||
    return AuthManager.createAuthenticatedClient(DebuggerClient);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  public static createAuthenticatedExamClient(): ExamClient {
 | 
					 | 
				
			||||||
    return AuthManager.createAuthenticatedClient(ExamClient);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  public static createAuthenticatedResourceClient(): ResourceClient {
 | 
					 | 
				
			||||||
    return AuthManager.createAuthenticatedClient(ResourceClient);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  public static createAuthenticatedHdmiVideoStreamClient(): HdmiVideoStreamClient {
 | 
					 | 
				
			||||||
    return AuthManager.createAuthenticatedClient(HdmiVideoStreamClient);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  public static createAuthenticatedJtagHubConnection() {
 | 
					 | 
				
			||||||
    return new HubConnectionBuilder()
 | 
					    return new HubConnectionBuilder()
 | 
				
			||||||
      .withUrl("http://127.0.0.1:5000/hubs/JtagHub", {
 | 
					      .withUrl(`http://127.0.0.1:5000/hubs/${hubPath}`, {
 | 
				
			||||||
        accessTokenFactory: () => this.getToken() ?? "",
 | 
					        accessTokenFactory: () => this.getToken() ?? "",
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
      .withAutomaticReconnect()
 | 
					      .withAutomaticReconnect()
 | 
				
			||||||
      .build();
 | 
					      .build();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public static createAuthenticatedProgressHubConnection() {
 | 
					  // 认证逻辑 - 去除所有废话
 | 
				
			||||||
    return new HubConnectionBuilder()
 | 
					  static async login(username: string, password: string): Promise<boolean> {
 | 
				
			||||||
      .withUrl("http://127.0.0.1:5000/hubs/ProgressHub", {
 | 
					 | 
				
			||||||
        accessTokenFactory: () => this.getToken() ?? "",
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .withAutomaticReconnect()
 | 
					 | 
				
			||||||
      .build();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 登录函数
 | 
					 | 
				
			||||||
  public static async login(
 | 
					 | 
				
			||||||
    username: string,
 | 
					 | 
				
			||||||
    password: string,
 | 
					 | 
				
			||||||
  ): Promise<boolean> {
 | 
					 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const client = new DataClient();
 | 
					      const client = new DataClient();
 | 
				
			||||||
      const token = await client.login(username, password);
 | 
					      const token = await client.login(username, password);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (token) {
 | 
					      if (!token) return false;
 | 
				
			||||||
        AuthManager.setToken(token);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // 验证token
 | 
					      this.setToken(token);
 | 
				
			||||||
        const authClient = AuthManager.createAuthenticatedDataClient();
 | 
					 | 
				
			||||||
        await authClient.testAuth();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return true;
 | 
					      // 验证token - 如果失败直接抛异常
 | 
				
			||||||
      }
 | 
					      await this.createClient(DataClient).testAuth();
 | 
				
			||||||
      return false;
 | 
					 | 
				
			||||||
    } catch (error) {
 | 
					 | 
				
			||||||
      AuthManager.clearToken();
 | 
					 | 
				
			||||||
      throw error;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 登出函数
 | 
					 | 
				
			||||||
  public static logout(): void {
 | 
					 | 
				
			||||||
    AuthManager.clearToken();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 验证当前token是否有效
 | 
					 | 
				
			||||||
  public static async verifyToken(): Promise<boolean> {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const token = AuthManager.getToken();
 | 
					 | 
				
			||||||
      if (!token) {
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const client = AuthManager.createAuthenticatedDataClient();
 | 
					 | 
				
			||||||
      await client.testAuth();
 | 
					 | 
				
			||||||
      return true;
 | 
					      return true;
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch {
 | 
				
			||||||
      AuthManager.clearToken();
 | 
					      this.clearToken();
 | 
				
			||||||
      return false;
 | 
					      throw new Error("Login failed");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 验证管理员权限
 | 
					  static logout(): void {
 | 
				
			||||||
  public static async verifyAdminAuth(): Promise<boolean> {
 | 
					    this.clearToken();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 简单的验证 - 不要搞复杂
 | 
				
			||||||
 | 
					  static async isAuthenticated(): Promise<boolean> {
 | 
				
			||||||
 | 
					    if (!this.getToken()) return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const token = AuthManager.getToken();
 | 
					      await this.createClient(DataClient).testAuth();
 | 
				
			||||||
      if (!token) {
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const client = AuthManager.createAuthenticatedDataClient();
 | 
					 | 
				
			||||||
      await client.testAdminAuth();
 | 
					 | 
				
			||||||
      return true;
 | 
					      return true;
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch {
 | 
				
			||||||
      // 只有在token完全无效的情况下才清除token
 | 
					      this.clearToken();
 | 
				
			||||||
      // 401错误表示token有效但权限不足,不应清除token
 | 
					 | 
				
			||||||
      if (error && typeof error === "object" && "status" in error) {
 | 
					 | 
				
			||||||
        // 如果是403 (Forbidden) 或401 (Unauthorized),说明token有效但权限不足
 | 
					 | 
				
			||||||
        if (error.status === 401 || error.status === 403) {
 | 
					 | 
				
			||||||
          return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        // 其他状态码可能表示token无效,清除token
 | 
					 | 
				
			||||||
        AuthManager.clearToken();
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        // 网络错误等,不清除token
 | 
					 | 
				
			||||||
        console.error("管理员权限验证失败:", error);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return false;
 | 
					      return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 检查客户端是否已配置认证
 | 
					  static async isAdminAuthenticated(): Promise<boolean> {
 | 
				
			||||||
  public static isClientAuthenticated(client: SupportedClient): boolean {
 | 
					    if (!this.getToken()) return false;
 | 
				
			||||||
    const token = AuthManager.getToken();
 | 
					
 | 
				
			||||||
    return !!token;
 | 
					    try {
 | 
				
			||||||
 | 
					      await this.createClient(DataClient).testAdminAuth();
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    } catch {
 | 
				
			||||||
 | 
					      this.clearToken();
 | 
				
			||||||
 | 
					      return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,7 +17,7 @@ export interface BoardData extends Board {
 | 
				
			|||||||
const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
 | 
					const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
 | 
				
			||||||
  // 远程升级相关参数
 | 
					  // 远程升级相关参数
 | 
				
			||||||
  const devPort = 1234;
 | 
					  const devPort = 1234;
 | 
				
			||||||
  const remoteUpdater = AuthManager.createAuthenticatedRemoteUpdateClient();
 | 
					  const remoteUpdater = AuthManager.createClient(RemoteUpdateClient);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 统一的板卡数据
 | 
					  // 统一的板卡数据
 | 
				
			||||||
  const boards = ref<BoardData[]>([]);
 | 
					  const boards = ref<BoardData[]>([]);
 | 
				
			||||||
@@ -35,13 +35,13 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
 | 
				
			|||||||
  async function getAllBoards(): Promise<{ success: boolean; error?: string }> {
 | 
					  async function getAllBoards(): Promise<{ success: boolean; error?: string }> {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      // 验证管理员权限
 | 
					      // 验证管理员权限
 | 
				
			||||||
      const hasAdminAuth = await AuthManager.verifyAdminAuth();
 | 
					      const hasAdminAuth = await AuthManager.isAdminAuthenticated();
 | 
				
			||||||
      if (!hasAdminAuth) {
 | 
					      if (!hasAdminAuth) {
 | 
				
			||||||
        console.error("权限验证失败");
 | 
					        console.error("权限验证失败");
 | 
				
			||||||
        return { success: false, error: "权限不足" };
 | 
					        return { success: false, error: "权限不足" };
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const client = AuthManager.createAuthenticatedDataClient();
 | 
					      const client = AuthManager.createClient(DataClient);
 | 
				
			||||||
      const result = await client.getAllBoards();
 | 
					      const result = await client.getAllBoards();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (result) {
 | 
					      if (result) {
 | 
				
			||||||
@@ -77,7 +77,7 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
 | 
				
			|||||||
  ): Promise<{ success: boolean; error?: string; boardId?: string }> {
 | 
					  ): Promise<{ success: boolean; error?: string; boardId?: string }> {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      // 验证管理员权限
 | 
					      // 验证管理员权限
 | 
				
			||||||
      const hasAdminAuth = await AuthManager.verifyAdminAuth();
 | 
					      const hasAdminAuth = await AuthManager.isAdminAuthenticated();
 | 
				
			||||||
      if (!hasAdminAuth) {
 | 
					      if (!hasAdminAuth) {
 | 
				
			||||||
        console.error("权限验证失败");
 | 
					        console.error("权限验证失败");
 | 
				
			||||||
        return { success: false, error: "权限不足" };
 | 
					        return { success: false, error: "权限不足" };
 | 
				
			||||||
@@ -89,11 +89,11 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
 | 
				
			|||||||
        return { success: false, error: "参数不完整" };
 | 
					        return { success: false, error: "参数不完整" };
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const client = AuthManager.createAuthenticatedDataClient();
 | 
					      const client = AuthManager.createClient(DataClient);
 | 
				
			||||||
      const boardId = await client.addBoard(name);
 | 
					      const boardId = await client.addBoard(name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (boardId) {
 | 
					      if (boardId) {
 | 
				
			||||||
        console.log("新增板卡成功", { boardId, name});
 | 
					        console.log("新增板卡成功", { boardId, name });
 | 
				
			||||||
        // 刷新板卡列表
 | 
					        // 刷新板卡列表
 | 
				
			||||||
        await getAllBoards();
 | 
					        await getAllBoards();
 | 
				
			||||||
        return { success: true };
 | 
					        return { success: true };
 | 
				
			||||||
@@ -119,7 +119,7 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
 | 
				
			|||||||
  ): Promise<{ success: boolean; error?: string }> {
 | 
					  ): Promise<{ success: boolean; error?: string }> {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      // 验证管理员权限
 | 
					      // 验证管理员权限
 | 
				
			||||||
      const hasAdminAuth = await AuthManager.verifyAdminAuth();
 | 
					      const hasAdminAuth = await AuthManager.isAdminAuthenticated();
 | 
				
			||||||
      if (!hasAdminAuth) {
 | 
					      if (!hasAdminAuth) {
 | 
				
			||||||
        console.error("权限验证失败");
 | 
					        console.error("权限验证失败");
 | 
				
			||||||
        return { success: false, error: "权限不足" };
 | 
					        return { success: false, error: "权限不足" };
 | 
				
			||||||
@@ -130,7 +130,7 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
 | 
				
			|||||||
        return { success: false, error: "板卡ID不能为空" };
 | 
					        return { success: false, error: "板卡ID不能为空" };
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const client = AuthManager.createAuthenticatedDataClient();
 | 
					      const client = AuthManager.createClient(DataClient);
 | 
				
			||||||
      const result = await client.deleteBoard(boardId);
 | 
					      const result = await client.deleteBoard(boardId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (result > 0) {
 | 
					      if (result > 0) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -59,3 +59,12 @@ export function formatDate(date: Date | string) {
 | 
				
			|||||||
    minute: "2-digit",
 | 
					    minute: "2-digit",
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function base64ToArrayBuffer(base64: string) {
 | 
				
			||||||
 | 
					  var binaryString = atob(base64);
 | 
				
			||||||
 | 
					  var bytes = new Uint8Array(binaryString.length);
 | 
				
			||||||
 | 
					  for (var i = 0; i < binaryString.length; i++) {
 | 
				
			||||||
 | 
					    bytes[i] = binaryString.charCodeAt(i);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return bytes.buffer;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,7 +4,7 @@
 | 
				
			|||||||
// @ts-nocheck
 | 
					// @ts-nocheck
 | 
				
			||||||
import type { HubConnection, IStreamResult, Subject } from '@microsoft/signalr';
 | 
					import type { HubConnection, IStreamResult, Subject } from '@microsoft/signalr';
 | 
				
			||||||
import type { IDigitalTubesHub, IJtagHub, IProgressHub, IDigitalTubesReceiver, IJtagReceiver, IProgressReceiver } from './server.Hubs';
 | 
					import type { IDigitalTubesHub, IJtagHub, IProgressHub, IDigitalTubesReceiver, IJtagReceiver, IProgressReceiver } from './server.Hubs';
 | 
				
			||||||
import type { ProgressInfo } from '../server.Hubs';
 | 
					import type { DigitalTubeTaskStatus, ProgressInfo } from '../server.Hubs';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// components
 | 
					// components
 | 
				
			||||||
@@ -107,6 +107,10 @@ class IDigitalTubesHub_HubProxy implements IDigitalTubesHub {
 | 
				
			|||||||
    public readonly setFrequency = async (frequency: number): Promise<boolean> => {
 | 
					    public readonly setFrequency = async (frequency: number): Promise<boolean> => {
 | 
				
			||||||
        return await this.connection.invoke("SetFrequency", frequency);
 | 
					        return await this.connection.invoke("SetFrequency", frequency);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public readonly getStatus = async (): Promise<DigitalTubeTaskStatus> => {
 | 
				
			||||||
 | 
					        return await this.connection.invoke("GetStatus");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class IJtagHub_HubProxyFactory implements HubProxyFactory<IJtagHub> {
 | 
					class IJtagHub_HubProxyFactory implements HubProxyFactory<IJtagHub> {
 | 
				
			||||||
@@ -157,6 +161,14 @@ class IProgressHub_HubProxy implements IProgressHub {
 | 
				
			|||||||
    public readonly join = async (taskId: string): Promise<boolean> => {
 | 
					    public readonly join = async (taskId: string): Promise<boolean> => {
 | 
				
			||||||
        return await this.connection.invoke("Join", taskId);
 | 
					        return await this.connection.invoke("Join", taskId);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public readonly leave = async (taskId: string): Promise<boolean> => {
 | 
				
			||||||
 | 
					        return await this.connection.invoke("Leave", taskId);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public readonly getProgress = async (taskId: string): Promise<ProgressInfo> => {
 | 
				
			||||||
 | 
					        return await this.connection.invoke("GetProgress", taskId);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,7 +3,7 @@
 | 
				
			|||||||
/* tslint:disable */
 | 
					/* tslint:disable */
 | 
				
			||||||
// @ts-nocheck
 | 
					// @ts-nocheck
 | 
				
			||||||
import type { IStreamResult, Subject } from '@microsoft/signalr';
 | 
					import type { IStreamResult, Subject } from '@microsoft/signalr';
 | 
				
			||||||
import type { ProgressInfo } from '../server.Hubs';
 | 
					import type { DigitalTubeTaskStatus, ProgressInfo } from '../server.Hubs';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type IDigitalTubesHub = {
 | 
					export type IDigitalTubesHub = {
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@@ -19,6 +19,10 @@ export type IDigitalTubesHub = {
 | 
				
			|||||||
    * @returns Transpiled from System.Threading.Tasks.Task<bool>
 | 
					    * @returns Transpiled from System.Threading.Tasks.Task<bool>
 | 
				
			||||||
    */
 | 
					    */
 | 
				
			||||||
    setFrequency(frequency: number): Promise<boolean>;
 | 
					    setFrequency(frequency: number): Promise<boolean>;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					    * @returns Transpiled from System.Threading.Tasks.Task<server.Hubs.DigitalTubeTaskStatus?>
 | 
				
			||||||
 | 
					    */
 | 
				
			||||||
 | 
					    getStatus(): Promise<DigitalTubeTaskStatus>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type IJtagHub = {
 | 
					export type IJtagHub = {
 | 
				
			||||||
@@ -44,6 +48,16 @@ export type IProgressHub = {
 | 
				
			|||||||
    * @returns Transpiled from System.Threading.Tasks.Task<bool>
 | 
					    * @returns Transpiled from System.Threading.Tasks.Task<bool>
 | 
				
			||||||
    */
 | 
					    */
 | 
				
			||||||
    join(taskId: string): Promise<boolean>;
 | 
					    join(taskId: string): Promise<boolean>;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					    * @param taskId Transpiled from string
 | 
				
			||||||
 | 
					    * @returns Transpiled from System.Threading.Tasks.Task<bool>
 | 
				
			||||||
 | 
					    */
 | 
				
			||||||
 | 
					    leave(taskId: string): Promise<boolean>;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					    * @param taskId Transpiled from string
 | 
				
			||||||
 | 
					    * @returns Transpiled from System.Threading.Tasks.Task<server.Hubs.ProgressInfo?>
 | 
				
			||||||
 | 
					    */
 | 
				
			||||||
 | 
					    getProgress(taskId: string): Promise<ProgressInfo>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type IDigitalTubesReceiver = {
 | 
					export type IDigitalTubesReceiver = {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,13 +2,20 @@
 | 
				
			|||||||
/* eslint-disable */
 | 
					/* eslint-disable */
 | 
				
			||||||
/* tslint:disable */
 | 
					/* tslint:disable */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** Transpiled from server.Hubs.DigitalTubeTaskStatus */
 | 
				
			||||||
 | 
					export type DigitalTubeTaskStatus = {
 | 
				
			||||||
 | 
					    /** Transpiled from int */
 | 
				
			||||||
 | 
					    frequency: number;
 | 
				
			||||||
 | 
					    /** Transpiled from bool */
 | 
				
			||||||
 | 
					    isRunning: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/** Transpiled from server.Hubs.ProgressStatus */
 | 
					/** Transpiled from server.Hubs.ProgressStatus */
 | 
				
			||||||
export enum ProgressStatus {
 | 
					export enum ProgressStatus {
 | 
				
			||||||
    Pending = 0,
 | 
					    Running = 0,
 | 
				
			||||||
    InProgress = 1,
 | 
					    Completed = 1,
 | 
				
			||||||
    Completed = 2,
 | 
					    Canceled = 2,
 | 
				
			||||||
    Canceled = 3,
 | 
					    Failed = 3,
 | 
				
			||||||
    Failed = 4,
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/** Transpiled from server.Hubs.ProgressInfo */
 | 
					/** Transpiled from server.Hubs.ProgressInfo */
 | 
				
			||||||
@@ -17,7 +24,7 @@ export type ProgressInfo = {
 | 
				
			|||||||
    taskId: string;
 | 
					    taskId: string;
 | 
				
			||||||
    /** Transpiled from server.Hubs.ProgressStatus */
 | 
					    /** Transpiled from server.Hubs.ProgressStatus */
 | 
				
			||||||
    status: ProgressStatus;
 | 
					    status: ProgressStatus;
 | 
				
			||||||
    /** Transpiled from int */
 | 
					    /** Transpiled from double */
 | 
				
			||||||
    progressPercent: number;
 | 
					    progressPercent: number;
 | 
				
			||||||
    /** Transpiled from string */
 | 
					    /** Transpiled from string */
 | 
				
			||||||
    errorMessage: string;
 | 
					    errorMessage: string;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -274,7 +274,7 @@ const handleSignUp = async () => {
 | 
				
			|||||||
// 页面初始化时检查是否已有有效token
 | 
					// 页面初始化时检查是否已有有效token
 | 
				
			||||||
const checkExistingToken = async () => {
 | 
					const checkExistingToken = async () => {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const isValid = await AuthManager.verifyToken();
 | 
					    const isValid = await AuthManager.isAuthenticated();
 | 
				
			||||||
    if (isValid) {
 | 
					    if (isValid) {
 | 
				
			||||||
      // 如果token仍然有效,直接跳转到project页面
 | 
					      // 如果token仍然有效,直接跳转到project页面
 | 
				
			||||||
      router.go(-1);
 | 
					      router.go(-1);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -418,7 +418,12 @@ import {
 | 
				
			|||||||
  FileArchiveIcon,
 | 
					  FileArchiveIcon,
 | 
				
			||||||
  FileJsonIcon,
 | 
					  FileJsonIcon,
 | 
				
			||||||
} from "lucide-vue-next";
 | 
					} from "lucide-vue-next";
 | 
				
			||||||
import { ExamDto, type FileParameter } from "@/APIClient";
 | 
					import {
 | 
				
			||||||
 | 
					  ExamClient,
 | 
				
			||||||
 | 
					  ExamDto,
 | 
				
			||||||
 | 
					  ResourceClient,
 | 
				
			||||||
 | 
					  type FileParameter,
 | 
				
			||||||
 | 
					} from "@/APIClient";
 | 
				
			||||||
import { useAlertStore } from "@/components/Alert";
 | 
					import { useAlertStore } from "@/components/Alert";
 | 
				
			||||||
import { AuthManager } from "@/utils/AuthManager";
 | 
					import { AuthManager } from "@/utils/AuthManager";
 | 
				
			||||||
import { useRequiredInjection } from "@/utils/Common";
 | 
					import { useRequiredInjection } from "@/utils/Common";
 | 
				
			||||||
@@ -618,7 +623,7 @@ const submitCreateExam = async () => {
 | 
				
			|||||||
  isUpdating.value = true;
 | 
					  isUpdating.value = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const client = AuthManager.createAuthenticatedExamClient();
 | 
					    const client = AuthManager.createClient(ExamClient);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let exam: ExamInfo;
 | 
					    let exam: ExamInfo;
 | 
				
			||||||
    if (mode.value === "create") {
 | 
					    if (mode.value === "create") {
 | 
				
			||||||
@@ -671,7 +676,7 @@ const submitCreateExam = async () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// 上传实验资源
 | 
					// 上传实验资源
 | 
				
			||||||
async function uploadExamResources(examId: string) {
 | 
					async function uploadExamResources(examId: string) {
 | 
				
			||||||
  const client = AuthManager.createAuthenticatedResourceClient();
 | 
					  const client = AuthManager.createClient(ResourceClient);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    // 上传MD文档
 | 
					    // 上传MD文档
 | 
				
			||||||
@@ -750,7 +755,7 @@ function close() {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function editExam(examId: string) {
 | 
					async function editExam(examId: string) {
 | 
				
			||||||
  const client = AuthManager.createAuthenticatedExamClient();
 | 
					  const client = AuthManager.createClient(ExamClient);
 | 
				
			||||||
  const examInfo = await client.getExam(examId);
 | 
					  const examInfo = await client.getExam(examId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  editExamInfo.value = {
 | 
					  editExamInfo.value = {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -250,7 +250,13 @@
 | 
				
			|||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import { ResourcePurpose, type ExamInfo, type ResourceInfo } from "@/APIClient";
 | 
					import {
 | 
				
			||||||
 | 
					  ExamClient,
 | 
				
			||||||
 | 
					  ResourceClient,
 | 
				
			||||||
 | 
					  ResourcePurpose,
 | 
				
			||||||
 | 
					  type ExamInfo,
 | 
				
			||||||
 | 
					  type ResourceInfo,
 | 
				
			||||||
 | 
					} from "@/APIClient";
 | 
				
			||||||
import { useAlertStore } from "@/components/Alert";
 | 
					import { useAlertStore } from "@/components/Alert";
 | 
				
			||||||
import { AuthManager } from "@/utils/AuthManager";
 | 
					import { AuthManager } from "@/utils/AuthManager";
 | 
				
			||||||
import { useRequiredInjection } from "@/utils/Common";
 | 
					import { useRequiredInjection } from "@/utils/Common";
 | 
				
			||||||
@@ -274,7 +280,7 @@ const props = defineProps<{
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const commitsList = ref<ResourceInfo[]>();
 | 
					const commitsList = ref<ResourceInfo[]>();
 | 
				
			||||||
async function updateCommits() {
 | 
					async function updateCommits() {
 | 
				
			||||||
  const client = AuthManager.createAuthenticatedExamClient();
 | 
					  const client = AuthManager.createClient(ExamClient);
 | 
				
			||||||
  const list = await client.getCommitsByExamId(props.selectedExam.id);
 | 
					  const list = await client.getCommitsByExamId(props.selectedExam.id);
 | 
				
			||||||
  commitsList.value = list;
 | 
					  commitsList.value = list;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -288,7 +294,7 @@ const downloadResources = async () => {
 | 
				
			|||||||
  downloadingResources.value = true;
 | 
					  downloadingResources.value = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const resourceClient = AuthManager.createAuthenticatedResourceClient();
 | 
					    const resourceClient = AuthManager.createClient(ResourceClient);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // 获取资源包列表(模板资源)
 | 
					    // 获取资源包列表(模板资源)
 | 
				
			||||||
    const resourceList = await resourceClient.getResourceList(
 | 
					    const resourceList = await resourceClient.getResourceList(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -181,7 +181,7 @@
 | 
				
			|||||||
import { ref, onMounted, computed } from "vue";
 | 
					import { ref, onMounted, computed } from "vue";
 | 
				
			||||||
import { useRoute } from "vue-router";
 | 
					import { useRoute } from "vue-router";
 | 
				
			||||||
import { AuthManager } from "@/utils/AuthManager";
 | 
					import { AuthManager } from "@/utils/AuthManager";
 | 
				
			||||||
import { type ExamInfo } from "@/APIClient";
 | 
					import { ExamClient, type ExamInfo } from "@/APIClient";
 | 
				
			||||||
import { formatDate } from "@/utils/Common";
 | 
					import { formatDate } from "@/utils/Common";
 | 
				
			||||||
import ExamInfoModal from "./ExamInfoModal.vue";
 | 
					import ExamInfoModal from "./ExamInfoModal.vue";
 | 
				
			||||||
import ExamEditModal from "./ExamEditModal.vue";
 | 
					import ExamEditModal from "./ExamEditModal.vue";
 | 
				
			||||||
@@ -206,7 +206,7 @@ async function refreshExams() {
 | 
				
			|||||||
  error.value = "";
 | 
					  error.value = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const client = AuthManager.createAuthenticatedExamClient();
 | 
					    const client = AuthManager.createClient(ExamClient);
 | 
				
			||||||
    exams.value = await client.getExamList();
 | 
					    exams.value = await client.getExamList();
 | 
				
			||||||
  } catch (err: any) {
 | 
					  } catch (err: any) {
 | 
				
			||||||
    error.value = err.message || "获取实验列表失败";
 | 
					    error.value = err.message || "获取实验列表失败";
 | 
				
			||||||
@@ -218,7 +218,7 @@ async function refreshExams() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
async function viewExam(examId: string) {
 | 
					async function viewExam(examId: string) {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const client = AuthManager.createAuthenticatedExamClient();
 | 
					    const client = AuthManager.createClient(ExamClient);
 | 
				
			||||||
    selectedExam.value = await client.getExam(examId);
 | 
					    selectedExam.value = await client.getExam(examId);
 | 
				
			||||||
    showInfoModal.value = true;
 | 
					    showInfoModal.value = true;
 | 
				
			||||||
  } catch (err: any) {
 | 
					  } catch (err: any) {
 | 
				
			||||||
@@ -248,7 +248,7 @@ onMounted(async () => {
 | 
				
			|||||||
    router.push("/login");
 | 
					    router.push("/login");
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  isAdmin.value = await AuthManager.verifyAdminAuth();
 | 
					  isAdmin.value = await AuthManager.isAdminAuthenticated();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await refreshExams();
 | 
					  await refreshExams();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -266,7 +266,12 @@
 | 
				
			|||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import { CaptureMode, ChannelConfig, DebuggerConfig } from "@/APIClient";
 | 
					import {
 | 
				
			||||||
 | 
					  CaptureMode,
 | 
				
			||||||
 | 
					  ChannelConfig,
 | 
				
			||||||
 | 
					  DebuggerClient,
 | 
				
			||||||
 | 
					  DebuggerConfig,
 | 
				
			||||||
 | 
					} from "@/APIClient";
 | 
				
			||||||
import { useAlertStore } from "@/components/Alert";
 | 
					import { useAlertStore } from "@/components/Alert";
 | 
				
			||||||
import BaseInputField from "@/components/InputField/BaseInputField.vue";
 | 
					import BaseInputField from "@/components/InputField/BaseInputField.vue";
 | 
				
			||||||
import type { LogicDataType } from "@/components/WaveformDisplay";
 | 
					import type { LogicDataType } from "@/components/WaveformDisplay";
 | 
				
			||||||
@@ -421,7 +426,7 @@ async function startCapture() {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  isCapturing.value = true;
 | 
					  isCapturing.value = true;
 | 
				
			||||||
  const client = AuthManager.createAuthenticatedDebuggerClient();
 | 
					  const client = AuthManager.createClient(DebuggerClient);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 构造API配置
 | 
					  // 构造API配置
 | 
				
			||||||
  const channelConfigs = channels.value
 | 
					  const channelConfigs = channels.value
 | 
				
			||||||
 
 | 
				
			|||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -8,7 +8,7 @@
 | 
				
			|||||||
        @layout="handleVerticalSplitterResize"
 | 
					        @layout="handleVerticalSplitterResize"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <!-- 使用 v-show 替代 v-if -->
 | 
					        <!-- 使用 v-show 替代 v-if -->
 | 
				
			||||||
        <SplitterPanel 
 | 
					        <SplitterPanel
 | 
				
			||||||
          v-show="!isBottomBarFullscreen"
 | 
					          v-show="!isBottomBarFullscreen"
 | 
				
			||||||
          id="splitter-group-v-panel-project"
 | 
					          id="splitter-group-v-panel-project"
 | 
				
			||||||
          :default-size="verticalSplitterSize"
 | 
					          :default-size="verticalSplitterSize"
 | 
				
			||||||
@@ -60,8 +60,8 @@
 | 
				
			|||||||
                  v-show="showDocPanel"
 | 
					                  v-show="showDocPanel"
 | 
				
			||||||
                  class="doc-panel overflow-y-auto h-full"
 | 
					                  class="doc-panel overflow-y-auto h-full"
 | 
				
			||||||
                >
 | 
					                >
 | 
				
			||||||
                  <MarkdownRenderer 
 | 
					                  <MarkdownRenderer
 | 
				
			||||||
                    :content="documentContent" 
 | 
					                    :content="documentContent"
 | 
				
			||||||
                    :examId="(route.query.examId as string) || ''"
 | 
					                    :examId="(route.query.examId as string) || ''"
 | 
				
			||||||
                  />
 | 
					                  />
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
@@ -80,11 +80,13 @@
 | 
				
			|||||||
        <!-- 功能底栏 -->
 | 
					        <!-- 功能底栏 -->
 | 
				
			||||||
        <SplitterPanel
 | 
					        <SplitterPanel
 | 
				
			||||||
          id="splitter-group-v-panel-bar"
 | 
					          id="splitter-group-v-panel-bar"
 | 
				
			||||||
          :default-size="isBottomBarFullscreen ? 100 : (100 - verticalSplitterSize)"
 | 
					          :default-size="
 | 
				
			||||||
 | 
					            isBottomBarFullscreen ? 100 : 100 - verticalSplitterSize
 | 
				
			||||||
 | 
					          "
 | 
				
			||||||
          :min-size="isBottomBarFullscreen ? 100 : 15"
 | 
					          :min-size="isBottomBarFullscreen ? 100 : 15"
 | 
				
			||||||
          class="w-full overflow-hidden pt-3"
 | 
					          class="w-full overflow-hidden pt-3"
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <BottomBar 
 | 
					          <BottomBar
 | 
				
			||||||
            :isFullscreen="isBottomBarFullscreen"
 | 
					            :isFullscreen="isBottomBarFullscreen"
 | 
				
			||||||
            @toggle-fullscreen="handleToggleBottomBarFullscreen"
 | 
					            @toggle-fullscreen="handleToggleBottomBarFullscreen"
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
@@ -106,22 +108,48 @@
 | 
				
			|||||||
    />
 | 
					    />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <!-- Navbar切换浮动按钮 -->
 | 
					    <!-- Navbar切换浮动按钮 -->
 | 
				
			||||||
    <div 
 | 
					    <div
 | 
				
			||||||
      class="navbar-toggle-btn"
 | 
					      class="navbar-toggle-btn"
 | 
				
			||||||
      :class="{ 'with-navbar': navbarControl.showNavbar.value }"
 | 
					      :class="{ 'with-navbar': navbarControl.showNavbar.value }"
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <button 
 | 
					      <button
 | 
				
			||||||
        @click="navbarControl.toggleNavbar"
 | 
					        @click="navbarControl.toggleNavbar"
 | 
				
			||||||
        class="btn btn-circle btn-primary shadow-lg hover:shadow-xl transition-all duration-300"
 | 
					        class="btn btn-circle btn-primary shadow-lg hover:shadow-xl transition-all duration-300"
 | 
				
			||||||
        :class="{ 'btn-outline': navbarControl.showNavbar.value }"
 | 
					        :class="{ 'btn-outline': navbarControl.showNavbar.value }"
 | 
				
			||||||
        :title="navbarControl.showNavbar.value ? '隐藏顶部导航栏' : '显示顶部导航栏'"
 | 
					        :title="
 | 
				
			||||||
 | 
					          navbarControl.showNavbar.value ? '隐藏顶部导航栏' : '显示顶部导航栏'
 | 
				
			||||||
 | 
					        "
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <!-- 使用SVG图标表示菜单/关闭状态 -->
 | 
					        <!-- 使用SVG图标表示菜单/关闭状态 -->
 | 
				
			||||||
        <svg v-if="navbarControl.showNavbar.value" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
 | 
					        <svg
 | 
				
			||||||
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
 | 
					          v-if="navbarControl.showNavbar.value"
 | 
				
			||||||
 | 
					          xmlns="http://www.w3.org/2000/svg"
 | 
				
			||||||
 | 
					          class="h-6 w-6"
 | 
				
			||||||
 | 
					          fill="none"
 | 
				
			||||||
 | 
					          viewBox="0 0 24 24"
 | 
				
			||||||
 | 
					          stroke="currentColor"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <path
 | 
				
			||||||
 | 
					            stroke-linecap="round"
 | 
				
			||||||
 | 
					            stroke-linejoin="round"
 | 
				
			||||||
 | 
					            stroke-width="2"
 | 
				
			||||||
 | 
					            d="M6 18L18 6M6 6l12 12"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
        </svg>
 | 
					        </svg>
 | 
				
			||||||
        <svg v-else xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
 | 
					        <svg
 | 
				
			||||||
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
 | 
					          v-else
 | 
				
			||||||
 | 
					          xmlns="http://www.w3.org/2000/svg"
 | 
				
			||||||
 | 
					          class="h-6 w-6"
 | 
				
			||||||
 | 
					          fill="none"
 | 
				
			||||||
 | 
					          viewBox="0 0 24 24"
 | 
				
			||||||
 | 
					          stroke="currentColor"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <path
 | 
				
			||||||
 | 
					            stroke-linecap="round"
 | 
				
			||||||
 | 
					            stroke-linejoin="round"
 | 
				
			||||||
 | 
					            stroke-width="2"
 | 
				
			||||||
 | 
					            d="M4 6h16M4 12h16M4 18h16"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
        </svg>
 | 
					        </svg>
 | 
				
			||||||
      </button>
 | 
					      </button>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
@@ -131,7 +159,7 @@
 | 
				
			|||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import { ref, onMounted, watch, inject, type Ref } from "vue";
 | 
					import { ref, onMounted, watch, inject, type Ref } from "vue";
 | 
				
			||||||
import { useRouter } from "vue-router";
 | 
					import { useRouter } from "vue-router";
 | 
				
			||||||
import { useLocalStorage } from '@vueuse/core'; // 添加VueUse导入
 | 
					import { useLocalStorage } from "@vueuse/core"; // 添加VueUse导入
 | 
				
			||||||
import { SplitterGroup, SplitterPanel, SplitterResizeHandle } from "reka-ui";
 | 
					import { SplitterGroup, SplitterPanel, SplitterResizeHandle } from "reka-ui";
 | 
				
			||||||
import DiagramCanvas from "@/components/LabCanvas/DiagramCanvas.vue";
 | 
					import DiagramCanvas from "@/components/LabCanvas/DiagramCanvas.vue";
 | 
				
			||||||
import ComponentSelector from "@/components/LabCanvas/ComponentSelector.vue";
 | 
					import ComponentSelector from "@/components/LabCanvas/ComponentSelector.vue";
 | 
				
			||||||
@@ -143,7 +171,7 @@ import { useProvideComponentManager } from "@/components/LabCanvas";
 | 
				
			|||||||
import { useAlertStore } from "@/components/Alert";
 | 
					import { useAlertStore } from "@/components/Alert";
 | 
				
			||||||
import { AuthManager } from "@/utils/AuthManager";
 | 
					import { AuthManager } from "@/utils/AuthManager";
 | 
				
			||||||
import { useEquipments } from "@/stores/equipments";
 | 
					import { useEquipments } from "@/stores/equipments";
 | 
				
			||||||
import type { Board } from "@/APIClient";
 | 
					import { DataClient, ResourceClient, type Board } from "@/APIClient";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { useRoute } from "vue-router";
 | 
					import { useRoute } from "vue-router";
 | 
				
			||||||
const route = useRoute();
 | 
					const route = useRoute();
 | 
				
			||||||
@@ -158,20 +186,29 @@ const equipments = useEquipments();
 | 
				
			|||||||
const alert = useAlertStore();
 | 
					const alert = useAlertStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --- Navbar控制 ---
 | 
					// --- Navbar控制 ---
 | 
				
			||||||
const navbarControl = inject('navbar') as {
 | 
					const navbarControl = inject("navbar") as {
 | 
				
			||||||
  showNavbar: Ref<boolean>;
 | 
					  showNavbar: Ref<boolean>;
 | 
				
			||||||
  toggleNavbar: () => void;
 | 
					  toggleNavbar: () => void;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --- 使用VueUse保存分栏状态 ---
 | 
					// --- 使用VueUse保存分栏状态 ---
 | 
				
			||||||
// 左右分栏比例(默认60%)
 | 
					// 左右分栏比例(默认60%)
 | 
				
			||||||
const horizontalSplitterSize = useLocalStorage('project-horizontal-splitter-size', 60);
 | 
					const horizontalSplitterSize = useLocalStorage(
 | 
				
			||||||
 | 
					  "project-horizontal-splitter-size",
 | 
				
			||||||
 | 
					  60,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
// 上下分栏比例(默认80%)
 | 
					// 上下分栏比例(默认80%)
 | 
				
			||||||
const verticalSplitterSize = useLocalStorage('project-vertical-splitter-size', 80);
 | 
					const verticalSplitterSize = useLocalStorage(
 | 
				
			||||||
 | 
					  "project-vertical-splitter-size",
 | 
				
			||||||
 | 
					  80,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
// 底栏全屏状态
 | 
					// 底栏全屏状态
 | 
				
			||||||
const isBottomBarFullscreen = useLocalStorage('project-bottom-bar-fullscreen', false);
 | 
					const isBottomBarFullscreen = useLocalStorage(
 | 
				
			||||||
 | 
					  "project-bottom-bar-fullscreen",
 | 
				
			||||||
 | 
					  false,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
// 文档面板显示状态
 | 
					// 文档面板显示状态
 | 
				
			||||||
const showDocPanel = useLocalStorage('project-show-doc-panel', false);
 | 
					const showDocPanel = useLocalStorage("project-show-doc-panel", false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function handleToggleBottomBarFullscreen() {
 | 
					function handleToggleBottomBarFullscreen() {
 | 
				
			||||||
  isBottomBarFullscreen.value = !isBottomBarFullscreen.value;
 | 
					  isBottomBarFullscreen.value = !isBottomBarFullscreen.value;
 | 
				
			||||||
@@ -216,25 +253,25 @@ async function loadDocumentContent() {
 | 
				
			|||||||
    const examId = route.query.examId as string;
 | 
					    const examId = route.query.examId as string;
 | 
				
			||||||
    if (examId) {
 | 
					    if (examId) {
 | 
				
			||||||
      // 如果有实验ID,从API加载实验文档
 | 
					      // 如果有实验ID,从API加载实验文档
 | 
				
			||||||
      console.log('加载实验文档:', examId);
 | 
					      console.log("加载实验文档:", examId);
 | 
				
			||||||
      const client = AuthManager.createAuthenticatedResourceClient();
 | 
					      const client = AuthManager.createClient(ResourceClient);
 | 
				
			||||||
      
 | 
					
 | 
				
			||||||
      // 获取markdown类型的模板资源列表
 | 
					      // 获取markdown类型的模板资源列表
 | 
				
			||||||
      const resources = await client.getResourceList(examId, 'doc', 'template');
 | 
					      const resources = await client.getResourceList(examId, "doc", "template");
 | 
				
			||||||
      
 | 
					
 | 
				
			||||||
      if (resources && resources.length > 0) {
 | 
					      if (resources && resources.length > 0) {
 | 
				
			||||||
        // 获取第一个markdown资源
 | 
					        // 获取第一个markdown资源
 | 
				
			||||||
        const markdownResource = resources[0];
 | 
					        const markdownResource = resources[0];
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        // 使用新的ResourceClient API获取资源文件内容
 | 
					        // 使用新的ResourceClient API获取资源文件内容
 | 
				
			||||||
        const response = await client.getResourceById(markdownResource.id);
 | 
					        const response = await client.getResourceById(markdownResource.id);
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        if (!response || !response.data) {
 | 
					        if (!response || !response.data) {
 | 
				
			||||||
          throw new Error('获取markdown文件失败');
 | 
					          throw new Error("获取markdown文件失败");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        const content = await response.data.text();
 | 
					        const content = await response.data.text();
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        // 更新文档内容,暂时不处理图片路径,由MarkdownRenderer处理
 | 
					        // 更新文档内容,暂时不处理图片路径,由MarkdownRenderer处理
 | 
				
			||||||
        documentContent.value = content;
 | 
					        documentContent.value = content;
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
@@ -279,17 +316,17 @@ function updateComponentDirectProp(
 | 
				
			|||||||
// 检查并初始化用户实验板
 | 
					// 检查并初始化用户实验板
 | 
				
			||||||
async function checkAndInitializeBoard() {
 | 
					async function checkAndInitializeBoard() {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const client = AuthManager.createAuthenticatedDataClient();
 | 
					    const client = AuthManager.createClient(DataClient);
 | 
				
			||||||
    const userInfo = await client.getUserInfo();
 | 
					    const userInfo = await client.getUserInfo();
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    if (userInfo.boardID && userInfo.boardID.trim() !== '') {
 | 
					    if (userInfo.boardID && userInfo.boardID.trim() !== "") {
 | 
				
			||||||
      // 用户已绑定实验板,获取实验板信息并更新到equipment
 | 
					      // 用户已绑定实验板,获取实验板信息并更新到equipment
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        const board = await client.getBoardByID(userInfo.boardID);
 | 
					        const board = await client.getBoardByID(userInfo.boardID);
 | 
				
			||||||
        updateEquipmentFromBoard(board);
 | 
					        updateEquipmentFromBoard(board);
 | 
				
			||||||
        alert?.show(`实验板 ${board.boardName} 已连接`, "success");
 | 
					        alert?.show(`实验板 ${board.boardName} 已连接`, "success");
 | 
				
			||||||
      } catch (boardError) {
 | 
					      } catch (boardError) {
 | 
				
			||||||
        console.error('获取实验板信息失败:', boardError);
 | 
					        console.error("获取实验板信息失败:", boardError);
 | 
				
			||||||
        alert?.show("获取实验板信息失败", "error");
 | 
					        alert?.show("获取实验板信息失败", "error");
 | 
				
			||||||
        showRequestBoardDialog.value = true;
 | 
					        showRequestBoardDialog.value = true;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@@ -298,7 +335,7 @@ async function checkAndInitializeBoard() {
 | 
				
			|||||||
      showRequestBoardDialog.value = true;
 | 
					      showRequestBoardDialog.value = true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  } catch (error) {
 | 
					  } catch (error) {
 | 
				
			||||||
    console.error('检查用户实验板失败:', error);
 | 
					    console.error("检查用户实验板失败:", error);
 | 
				
			||||||
    alert?.show("检查用户信息失败", "error");
 | 
					    alert?.show("检查用户信息失败", "error");
 | 
				
			||||||
    showRequestBoardDialog.value = true;
 | 
					    showRequestBoardDialog.value = true;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -308,12 +345,12 @@ async function checkAndInitializeBoard() {
 | 
				
			|||||||
function updateEquipmentFromBoard(board: Board) {
 | 
					function updateEquipmentFromBoard(board: Board) {
 | 
				
			||||||
  equipments.boardAddr = board.ipAddr;
 | 
					  equipments.boardAddr = board.ipAddr;
 | 
				
			||||||
  equipments.boardPort = board.port;
 | 
					  equipments.boardPort = board.port;
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  console.log(`实验板信息已更新到equipment store:`, {
 | 
					  console.log(`实验板信息已更新到equipment store:`, {
 | 
				
			||||||
    address: board.ipAddr,
 | 
					    address: board.ipAddr,
 | 
				
			||||||
    port: board.port,
 | 
					    port: board.port,
 | 
				
			||||||
    boardName: board.boardName,
 | 
					    boardName: board.boardName,
 | 
				
			||||||
    boardId: board.id
 | 
					    boardId: board.id,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -321,7 +358,7 @@ function updateEquipmentFromBoard(board: Board) {
 | 
				
			|||||||
function handleRequestBoardClose() {
 | 
					function handleRequestBoardClose() {
 | 
				
			||||||
  showRequestBoardDialog.value = false;
 | 
					  showRequestBoardDialog.value = false;
 | 
				
			||||||
  // 如果用户取消申请,可以选择返回上一页或显示警告
 | 
					  // 如果用户取消申请,可以选择返回上一页或显示警告
 | 
				
			||||||
  router.push('/');
 | 
					  router.push("/");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 处理申请实验板成功
 | 
					// 处理申请实验板成功
 | 
				
			||||||
@@ -338,12 +375,12 @@ onMounted(async () => {
 | 
				
			|||||||
    const isAuthenticated = await AuthManager.isAuthenticated();
 | 
					    const isAuthenticated = await AuthManager.isAuthenticated();
 | 
				
			||||||
    if (!isAuthenticated) {
 | 
					    if (!isAuthenticated) {
 | 
				
			||||||
      // 验证失败,跳转到登录页面
 | 
					      // 验证失败,跳转到登录页面
 | 
				
			||||||
      router.push('/login');
 | 
					      router.push("/login");
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  } catch (error) {
 | 
					  } catch (error) {
 | 
				
			||||||
    console.error('身份验证失败:', error);
 | 
					    console.error("身份验证失败:", error);
 | 
				
			||||||
    router.push('/login');
 | 
					    router.push("/login");
 | 
				
			||||||
    return;
 | 
					    return;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -75,7 +75,7 @@ import { ref, watch } from "vue";
 | 
				
			|||||||
import { CheckCircle } from "lucide-vue-next";
 | 
					import { CheckCircle } from "lucide-vue-next";
 | 
				
			||||||
import { AuthManager } from "@/utils/AuthManager";
 | 
					import { AuthManager } from "@/utils/AuthManager";
 | 
				
			||||||
import { useAlertStore } from "@/components/Alert";
 | 
					import { useAlertStore } from "@/components/Alert";
 | 
				
			||||||
import type { Board } from "@/APIClient";
 | 
					import { DataClient, type Board } from "@/APIClient";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface Props {
 | 
					interface Props {
 | 
				
			||||||
  open: boolean;
 | 
					  open: boolean;
 | 
				
			||||||
@@ -113,7 +113,7 @@ async function checkUserBoard() {
 | 
				
			|||||||
  boardInfo.value = null;
 | 
					  boardInfo.value = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const client = AuthManager.createAuthenticatedDataClient();
 | 
					    const client = AuthManager.createClient(DataClient);
 | 
				
			||||||
    const userInfo = await client.getUserInfo();
 | 
					    const userInfo = await client.getUserInfo();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (userInfo.boardID && userInfo.boardID.trim() !== "") {
 | 
					    if (userInfo.boardID && userInfo.boardID.trim() !== "") {
 | 
				
			||||||
@@ -140,7 +140,7 @@ async function requestBoard() {
 | 
				
			|||||||
  requesting.value = true;
 | 
					  requesting.value = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const client = AuthManager.createAuthenticatedDataClient();
 | 
					    const client = AuthManager.createClient(DataClient);
 | 
				
			||||||
    const board = await client.getAvailableBoard(undefined);
 | 
					    const board = await client.getAvailableBoard(undefined);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (board) {
 | 
					    if (board) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -433,7 +433,7 @@ const currentVideoSource = ref("");
 | 
				
			|||||||
const logs = ref<Array<{ time: Date; level: string; message: string }>>([]);
 | 
					const logs = ref<Array<{ time: Date; level: string; message: string }>>([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// API 客户端
 | 
					// API 客户端
 | 
				
			||||||
const videoClient = AuthManager.createAuthenticatedVideoStreamClient();
 | 
					const videoClient = AuthManager.createClient(VideoStreamClient);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 添加日志
 | 
					// 添加日志
 | 
				
			||||||
const addLog = (level: string, message: string) => {
 | 
					const addLog = (level: string, message: string) => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -174,7 +174,12 @@
 | 
				
			|||||||
import { ref, reactive, watch } from "vue";
 | 
					import { ref, reactive, watch } from "vue";
 | 
				
			||||||
import { AuthManager } from "../../utils/AuthManager";
 | 
					import { AuthManager } from "../../utils/AuthManager";
 | 
				
			||||||
import { useAlertStore } from "../../components/Alert";
 | 
					import { useAlertStore } from "../../components/Alert";
 | 
				
			||||||
import { BoardStatus, type NetworkConfigDto } from "../../APIClient";
 | 
					import {
 | 
				
			||||||
 | 
					  BoardStatus,
 | 
				
			||||||
 | 
					  DataClient,
 | 
				
			||||||
 | 
					  NetConfigClient,
 | 
				
			||||||
 | 
					  type NetworkConfigDto,
 | 
				
			||||||
 | 
					} from "../../APIClient";
 | 
				
			||||||
import { useRequiredInjection } from "@/utils/Common";
 | 
					import { useRequiredInjection } from "@/utils/Common";
 | 
				
			||||||
import { useBoardManager } from "@/utils/BoardManager";
 | 
					import { useBoardManager } from "@/utils/BoardManager";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -267,8 +272,7 @@ async function handleSubmit() {
 | 
				
			|||||||
  isSubmitting.value = true;
 | 
					  isSubmitting.value = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    // 通过 AuthManager 获取认证的 DataClient
 | 
					    const dataClient = AuthManager.createClient(DataClient);
 | 
				
			||||||
    const dataClient = AuthManager.createAuthenticatedDataClient();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // 添加板卡到数据库
 | 
					    // 添加板卡到数据库
 | 
				
			||||||
    const boardId = await dataClient.addBoard(form.name.trim());
 | 
					    const boardId = await dataClient.addBoard(form.name.trim());
 | 
				
			||||||
@@ -293,8 +297,7 @@ async function handleCancelPairing() {
 | 
				
			|||||||
  if (!addedBoardId.value) return;
 | 
					  if (!addedBoardId.value) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    // 通过 AuthManager 获取认证的 DataClient
 | 
					    const dataClient = AuthManager.createClient(DataClient);
 | 
				
			||||||
    const dataClient = AuthManager.createAuthenticatedDataClient();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // 删除添加的板卡
 | 
					    // 删除添加的板卡
 | 
				
			||||||
    await dataClient.deleteBoard(addedBoardId.value);
 | 
					    await dataClient.deleteBoard(addedBoardId.value);
 | 
				
			||||||
@@ -317,8 +320,8 @@ async function handlePairingConfirm() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    // 通过 AuthManager 获取认证的客户端
 | 
					    // 通过 AuthManager 获取认证的客户端
 | 
				
			||||||
    const dataClient = AuthManager.createAuthenticatedDataClient();
 | 
					    const dataClient = AuthManager.createClient(DataClient);
 | 
				
			||||||
    const netConfigClient = AuthManager.createAuthenticatedNetConfigClient();
 | 
					    const netConfigClient = AuthManager.createClient(NetConfigClient);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // 获取数据库中对应分配的板卡信息
 | 
					    // 获取数据库中对应分配的板卡信息
 | 
				
			||||||
    const boardInfo = await dataClient.getBoardByID(addedBoardId.value);
 | 
					    const boardInfo = await dataClient.getBoardByID(addedBoardId.value);
 | 
				
			||||||
@@ -365,7 +368,7 @@ async function handlePairingConfirm() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // 配置失败,删除数据库中的板卡信息
 | 
					    // 配置失败,删除数据库中的板卡信息
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const dataClient = AuthManager.createAuthenticatedDataClient();
 | 
					      const dataClient = AuthManager.createClient(DataClient);
 | 
				
			||||||
      await dataClient.deleteBoard(addedBoardId.value);
 | 
					      await dataClient.deleteBoard(addedBoardId.value);
 | 
				
			||||||
    } catch (deleteError) {
 | 
					    } catch (deleteError) {
 | 
				
			||||||
      console.error("删除板卡失败:", deleteError);
 | 
					      console.error("删除板卡失败:", deleteError);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -42,12 +42,12 @@ const isAdmin = ref(false);
 | 
				
			|||||||
function setActivePage(event: Event) {
 | 
					function setActivePage(event: Event) {
 | 
				
			||||||
  const target = event.currentTarget as HTMLLinkElement;
 | 
					  const target = event.currentTarget as HTMLLinkElement;
 | 
				
			||||||
  const newPage = toNumber(target.id);
 | 
					  const newPage = toNumber(target.id);
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  // 如果用户不是管理员但试图访问管理员页面,则忽略
 | 
					  // 如果用户不是管理员但试图访问管理员页面,则忽略
 | 
				
			||||||
  if (newPage === 100 && !isAdmin.value) {
 | 
					  if (newPage === 100 && !isAdmin.value) {
 | 
				
			||||||
    return;
 | 
					    return;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  activePage.value = newPage;
 | 
					  activePage.value = newPage;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -60,16 +60,16 @@ onMounted(async () => {
 | 
				
			|||||||
      // 这里可以使用路由跳转
 | 
					      // 这里可以使用路由跳转
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    // 验证管理员权限
 | 
					    // 验证管理员权限
 | 
				
			||||||
    isAdmin.value = await AuthManager.verifyAdminAuth();
 | 
					    isAdmin.value = await AuthManager.isAdminAuthenticated();
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    // 如果当前页面是管理员页面但用户不是管理员,切换到用户信息页面
 | 
					    // 如果当前页面是管理员页面但用户不是管理员,切换到用户信息页面
 | 
				
			||||||
    if (activePage.value === 100 && !isAdmin.value) {
 | 
					    if (activePage.value === 100 && !isAdmin.value) {
 | 
				
			||||||
      activePage.value = 1;
 | 
					      activePage.value = 1;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  } catch (error) {
 | 
					  } catch (error) {
 | 
				
			||||||
    console.error('用户认证检查失败:', error);
 | 
					    console.error("用户认证检查失败:", error);
 | 
				
			||||||
    // 可以在这里处理错误,比如显示错误信息或重定向到登录页面
 | 
					    // 可以在这里处理错误,比如显示错误信息或重定向到登录页面
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -273,7 +273,13 @@
 | 
				
			|||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import { ref, onMounted } from "vue";
 | 
					import { ref, onMounted } from "vue";
 | 
				
			||||||
import { AuthManager } from "@/utils/AuthManager";
 | 
					import { AuthManager } from "@/utils/AuthManager";
 | 
				
			||||||
import { UserInfo, Board, BoardStatus } from "@/APIClient";
 | 
					import {
 | 
				
			||||||
 | 
					  UserInfo,
 | 
				
			||||||
 | 
					  Board,
 | 
				
			||||||
 | 
					  BoardStatus,
 | 
				
			||||||
 | 
					  DataClient,
 | 
				
			||||||
 | 
					  JtagClient,
 | 
				
			||||||
 | 
					} from "@/APIClient";
 | 
				
			||||||
import { Alert, useAlertStore } from "@/components/Alert";
 | 
					import { Alert, useAlertStore } from "@/components/Alert";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  User,
 | 
					  User,
 | 
				
			||||||
@@ -319,7 +325,7 @@ const loadBoardInfo = async () => {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const client = AuthManager.createAuthenticatedDataClient();
 | 
					    const client = AuthManager.createClient(DataClient);
 | 
				
			||||||
    boardInfo.value = await client.getBoardByID(userInfo.value.boardID);
 | 
					    boardInfo.value = await client.getBoardByID(userInfo.value.boardID);
 | 
				
			||||||
  } catch (err) {
 | 
					  } catch (err) {
 | 
				
			||||||
    console.error("加载实验板信息失败:", err);
 | 
					    console.error("加载实验板信息失败:", err);
 | 
				
			||||||
@@ -335,7 +341,7 @@ const loadUserInfo = async (showSuccessMessage = false) => {
 | 
				
			|||||||
  try {
 | 
					  try {
 | 
				
			||||||
    await new Promise((resolve) => setTimeout(resolve, 200));
 | 
					    await new Promise((resolve) => setTimeout(resolve, 200));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const client = AuthManager.createAuthenticatedDataClient();
 | 
					    const client = AuthManager.createClient(DataClient);
 | 
				
			||||||
    userInfo.value = await client.getUserInfo();
 | 
					    userInfo.value = await client.getUserInfo();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // 如果有绑定的实验板ID,加载实验板信息
 | 
					    // 如果有绑定的实验板ID,加载实验板信息
 | 
				
			||||||
@@ -370,7 +376,7 @@ const applyBoard = async () => {
 | 
				
			|||||||
  alertStore?.info("正在申请实验板...");
 | 
					  alertStore?.info("正在申请实验板...");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const client = AuthManager.createAuthenticatedDataClient();
 | 
					    const client = AuthManager.createClient(DataClient);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // 获取可用的实验板
 | 
					    // 获取可用的实验板
 | 
				
			||||||
    const availableBoard = await client.getAvailableBoard(undefined);
 | 
					    const availableBoard = await client.getAvailableBoard(undefined);
 | 
				
			||||||
@@ -407,7 +413,7 @@ const testBoardConnection = async () => {
 | 
				
			|||||||
  alertStore?.info("正在测试连接...");
 | 
					  alertStore?.info("正在测试连接...");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const jtagClient = AuthManager.createAuthenticatedJtagClient();
 | 
					    const jtagClient = AuthManager.createClient(JtagClient);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // 使用JTAG客户端读取设备ID Code
 | 
					    // 使用JTAG客户端读取设备ID Code
 | 
				
			||||||
    const idCode = await jtagClient.getDeviceIDCode(
 | 
					    const idCode = await jtagClient.getDeviceIDCode(
 | 
				
			||||||
@@ -444,7 +450,7 @@ const unbindBoard = async () => {
 | 
				
			|||||||
  alertStore?.info("正在解绑实验板...");
 | 
					  alertStore?.info("正在解绑实验板...");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const client = AuthManager.createAuthenticatedDataClient();
 | 
					    const client = AuthManager.createClient(DataClient);
 | 
				
			||||||
    const success = await client.unbindBoard();
 | 
					    const success = await client.unbindBoard();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (success) {
 | 
					    if (success) {
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user