feat: 完善用户界面,添加绑定与解除绑定的功能
This commit is contained in:
		@@ -41,6 +41,11 @@ public class DataController : ControllerBase
 | 
			
		||||
        /// 用户关联的板卡ID
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public Guid BoardID { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 用户绑定板子的过期时间
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public DateTime? BoardExpireTime { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -148,6 +153,7 @@ public class DataController : ControllerBase
 | 
			
		||||
            Name = user.Name,
 | 
			
		||||
            EMail = user.EMail,
 | 
			
		||||
            BoardID = user.BoardID,
 | 
			
		||||
            BoardExpireTime = user.BoardExpireTime,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -194,32 +200,32 @@ public class DataController : ControllerBase
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取一个空闲的实验板(普通用户权限)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="durationHours">绑定持续时间(小时),默认为1小时</param>
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    [HttpGet("GetAvailableBoard")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(Database.Board), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public IActionResult GetAvailableBoard()
 | 
			
		||||
    public IActionResult GetAvailableBoard(int durationHours = 1)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            using var db = new Database.AppDataConnection();
 | 
			
		||||
            var boardOpt = db.GetAvailableBoard();
 | 
			
		||||
            if (!boardOpt.HasValue)
 | 
			
		||||
                return NotFound("没有可用的实验板");
 | 
			
		||||
 | 
			
		||||
            // 绑定用户与实验板
 | 
			
		||||
            var userName = User.Identity?.Name;
 | 
			
		||||
            if (string.IsNullOrEmpty(userName))
 | 
			
		||||
                return Unauthorized("未找到用户名信息");
 | 
			
		||||
 | 
			
		||||
            using var db = new Database.AppDataConnection();
 | 
			
		||||
            var userRet = db.GetUserByName(userName);
 | 
			
		||||
            if (!userRet.IsSuccessful || !userRet.Value.HasValue)
 | 
			
		||||
                return BadRequest("用户不存在");
 | 
			
		||||
 | 
			
		||||
            var user = userRet.Value.Value;
 | 
			
		||||
            db.BindUserToBoard(user.ID, boardOpt.Value.ID);
 | 
			
		||||
            var expireTime = DateTime.UtcNow.AddHours(durationHours);
 | 
			
		||||
            
 | 
			
		||||
            var boardOpt = db.GetAvailableBoard(user.ID, expireTime);
 | 
			
		||||
            if (!boardOpt.HasValue)
 | 
			
		||||
                return NotFound("没有可用的实验板");
 | 
			
		||||
 | 
			
		||||
            return Ok(boardOpt.Value);
 | 
			
		||||
        }
 | 
			
		||||
@@ -230,6 +236,67 @@ public class DataController : ControllerBase
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 解除当前用户绑定的实验板(普通用户权限)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    [HttpPost("UnbindBoard")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public IActionResult UnbindBoard()
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var userName = User.Identity?.Name;
 | 
			
		||||
            if (string.IsNullOrEmpty(userName))
 | 
			
		||||
                return Unauthorized("未找到用户名信息");
 | 
			
		||||
 | 
			
		||||
            using var db = new Database.AppDataConnection();
 | 
			
		||||
            var userRet = db.GetUserByName(userName);
 | 
			
		||||
            if (!userRet.IsSuccessful || !userRet.Value.HasValue)
 | 
			
		||||
                return BadRequest("用户不存在");
 | 
			
		||||
 | 
			
		||||
            var user = userRet.Value.Value;
 | 
			
		||||
            var result = db.UnbindUserFromBoard(user.ID);
 | 
			
		||||
            return Ok(result > 0);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error(ex, "解除实验板绑定时发生异常");
 | 
			
		||||
            return StatusCode(StatusCodes.Status500InternalServerError, "解除失败,请稍后重试");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 用户根据实验板ID获取实验板信息(普通用户权限)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    [HttpGet("GetBoardByID")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(Database.Board), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public IActionResult GetBoardByID(Guid id)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            using var db = new Database.AppDataConnection();
 | 
			
		||||
            var ret = db.GetBoardByID(id);
 | 
			
		||||
            if (!ret.IsSuccessful)
 | 
			
		||||
                return StatusCode(StatusCodes.Status500InternalServerError, "数据库操作失败");
 | 
			
		||||
            if (!ret.Value.HasValue)
 | 
			
		||||
                return NotFound("未找到对应的实验板");
 | 
			
		||||
            return Ok(ret.Value.Value);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error(ex, "获取实验板信息时发生异常");
 | 
			
		||||
            return StatusCode(StatusCodes.Status500InternalServerError, "获取失败,请稍后重试");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 新增板子(管理员权限)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,15 @@
 | 
			
		||||
using Microsoft.AspNetCore.Authorization;
 | 
			
		||||
using Microsoft.AspNetCore.Cors;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
 | 
			
		||||
namespace server.Controllers;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Jtag API
 | 
			
		||||
/// JTAG 控制器 - 提供 JTAG 相关的 API 操作
 | 
			
		||||
/// </summary>
 | 
			
		||||
[ApiController]
 | 
			
		||||
[Route("api/[controller]")]
 | 
			
		||||
[Authorize] // 添加用户认证要求
 | 
			
		||||
public class JtagController : ControllerBase
 | 
			
		||||
{
 | 
			
		||||
    private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
			
		||||
@@ -15,134 +17,193 @@ public class JtagController : ControllerBase
 | 
			
		||||
    private const string BITSTREAM_PATH = "bitstream/Jtag";
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 页面
 | 
			
		||||
    /// 控制器首页信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>控制器描述信息</returns>
 | 
			
		||||
    [HttpGet]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
 | 
			
		||||
    public string Index()
 | 
			
		||||
    {
 | 
			
		||||
        logger.Info($"User {User.Identity?.Name} accessed Jtag controller index");
 | 
			
		||||
        return "This is Jtag Controller";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取Jtag ID Code
 | 
			
		||||
    /// 获取 JTAG 设备的 ID Code
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="address"> 设备地址 </param>
 | 
			
		||||
    /// <param name="port"> 设备端口 </param>
 | 
			
		||||
    /// <param name="address">JTAG 设备地址</param>
 | 
			
		||||
    /// <param name="port">JTAG 设备端口</param>
 | 
			
		||||
    /// <returns>设备的 ID Code</returns>
 | 
			
		||||
    [HttpGet("GetDeviceIDCode")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(uint), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
 | 
			
		||||
    public async ValueTask<IResult> GetDeviceIDCode(string address, int port)
 | 
			
		||||
    {
 | 
			
		||||
        var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
 | 
			
		||||
        var ret = await jtagCtrl.ReadIDCode();
 | 
			
		||||
        logger.Info($"User {User.Identity?.Name} requesting device ID code from {address}:{port}");
 | 
			
		||||
 | 
			
		||||
        if (ret.IsSuccessful)
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            logger.Info($"Get device {address} ID code: 0x{ret.Value:X4}");
 | 
			
		||||
            return TypedResults.Ok(ret.Value);
 | 
			
		||||
            var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
 | 
			
		||||
            var ret = await jtagCtrl.ReadIDCode();
 | 
			
		||||
 | 
			
		||||
            if (ret.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Info($"User {User.Identity?.Name} successfully got device {address} ID code: 0x{ret.Value:X8}");
 | 
			
		||||
                return TypedResults.Ok(ret.Value);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"User {User.Identity?.Name} failed to get device {address} ID code: {ret.Error}");
 | 
			
		||||
                return TypedResults.InternalServerError(ret.Error);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error(ret.Error);
 | 
			
		||||
            return TypedResults.InternalServerError(ret.Error);
 | 
			
		||||
            logger.Error(ex, $"User {User.Identity?.Name} encountered exception while getting device {address} ID code");
 | 
			
		||||
            return TypedResults.InternalServerError(ex);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取状态寄存器
 | 
			
		||||
    /// 读取 JTAG 设备的状态寄存器
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="address"> 设备地址 </param>
 | 
			
		||||
    /// <param name="port"> 设备端口 </param>
 | 
			
		||||
    /// <param name="address">JTAG 设备地址</param>
 | 
			
		||||
    /// <param name="port">JTAG 设备端口</param>
 | 
			
		||||
    /// <returns>状态寄存器的原始值、二进制表示和解码值</returns>
 | 
			
		||||
    [HttpGet("ReadStatusReg")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
 | 
			
		||||
    public async ValueTask<IResult> ReadStatusReg(string address, int port)
 | 
			
		||||
    {
 | 
			
		||||
        var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
 | 
			
		||||
        var ret = await jtagCtrl.ReadStatusReg();
 | 
			
		||||
        logger.Info($"User {User.Identity?.Name} requesting status register from {address}:{port}");
 | 
			
		||||
 | 
			
		||||
        if (ret.IsSuccessful)
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var binaryValue = Common.String.Reverse(Convert.ToString(ret.Value, 2).PadLeft(32, '0'));
 | 
			
		||||
            var decodeValue = new Peripherals.JtagClient.JtagStatusReg(ret.Value);
 | 
			
		||||
            logger.Info($"Read device {address} Status Register: \n\t 0b{binaryValue} \n\t {decodeValue}");
 | 
			
		||||
            return TypedResults.Ok(new
 | 
			
		||||
            var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
 | 
			
		||||
            var ret = await jtagCtrl.ReadStatusReg();
 | 
			
		||||
 | 
			
		||||
            if (ret.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                original = ret.Value,
 | 
			
		||||
                binaryValue,
 | 
			
		||||
                decodeValue,
 | 
			
		||||
            });
 | 
			
		||||
                var binaryValue = Common.String.Reverse(Convert.ToString(ret.Value, 2).PadLeft(32, '0'));
 | 
			
		||||
                var decodeValue = new Peripherals.JtagClient.JtagStatusReg(ret.Value);
 | 
			
		||||
                logger.Info($"User {User.Identity?.Name} successfully read device {address} Status Register: \n\t 0b{binaryValue} \n\t {decodeValue}");
 | 
			
		||||
                return TypedResults.Ok(new
 | 
			
		||||
                {
 | 
			
		||||
                    original = ret.Value,
 | 
			
		||||
                    binaryValue,
 | 
			
		||||
                    decodeValue,
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"User {User.Identity?.Name} failed to read device {address} status register: {ret.Error}");
 | 
			
		||||
                return TypedResults.InternalServerError(ret.Error);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error(ret.Error);
 | 
			
		||||
            return TypedResults.InternalServerError(ret.Error);
 | 
			
		||||
            logger.Error(ex, $"User {User.Identity?.Name} encountered exception while reading device {address} status register");
 | 
			
		||||
            return TypedResults.InternalServerError(ex);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 上传比特流文件
 | 
			
		||||
    /// 上传比特流文件到服务器
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="address"> 设备地址 </param>
 | 
			
		||||
    /// <param name="address">目标设备地址</param>
 | 
			
		||||
    /// <param name="file">比特流文件</param>
 | 
			
		||||
    /// <returns>上传结果</returns>
 | 
			
		||||
    [HttpPost("UploadBitstream")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public async ValueTask<IResult> UploadBitstream(string address, IFormFile file)
 | 
			
		||||
    {
 | 
			
		||||
        logger.Info($"User {User.Identity?.Name} uploading bitstream for device {address}");
 | 
			
		||||
 | 
			
		||||
        if (file == null || file.Length == 0)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Warn($"User {User.Identity?.Name} attempted to upload empty file for device {address}");
 | 
			
		||||
            return TypedResults.BadRequest("未选择文件");
 | 
			
		||||
 | 
			
		||||
        // 生成安全的文件名(避免路径遍历攻击)
 | 
			
		||||
        var fileName = Path.GetRandomFileName();
 | 
			
		||||
        var uploadsFolder = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
 | 
			
		||||
 | 
			
		||||
        // 如果存在文件,则删除原文件再上传
 | 
			
		||||
        if (Directory.Exists(uploadsFolder))
 | 
			
		||||
        {
 | 
			
		||||
            Directory.Delete(uploadsFolder, true);
 | 
			
		||||
        }
 | 
			
		||||
        Directory.CreateDirectory(uploadsFolder);
 | 
			
		||||
 | 
			
		||||
        var filePath = Path.Combine(uploadsFolder, fileName);
 | 
			
		||||
 | 
			
		||||
        using (var stream = new FileStream(filePath, FileMode.Create))
 | 
			
		||||
        {
 | 
			
		||||
            await file.CopyToAsync(stream);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        logger.Info($"Device {address} Upload Bitstream Successfully");
 | 
			
		||||
        return TypedResults.Ok(true);
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            // 生成安全的文件名(避免路径遍历攻击)
 | 
			
		||||
            var fileName = Path.GetRandomFileName();
 | 
			
		||||
            var uploadsFolder = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
 | 
			
		||||
 | 
			
		||||
            // 如果存在文件,则删除原文件再上传
 | 
			
		||||
            if (Directory.Exists(uploadsFolder))
 | 
			
		||||
            {
 | 
			
		||||
                Directory.Delete(uploadsFolder, true);
 | 
			
		||||
                logger.Info($"User {User.Identity?.Name} removed existing bitstream folder for device {address}");
 | 
			
		||||
            }
 | 
			
		||||
            Directory.CreateDirectory(uploadsFolder);
 | 
			
		||||
 | 
			
		||||
            var filePath = Path.Combine(uploadsFolder, fileName);
 | 
			
		||||
 | 
			
		||||
            using (var stream = new FileStream(filePath, FileMode.Create))
 | 
			
		||||
            {
 | 
			
		||||
                await file.CopyToAsync(stream);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            logger.Info($"User {User.Identity?.Name} successfully uploaded bitstream for device {address}, file size: {file.Length} bytes");
 | 
			
		||||
            return TypedResults.Ok(true);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error(ex, $"User {User.Identity?.Name} failed to upload bitstream for device {address}");
 | 
			
		||||
            return TypedResults.InternalServerError(ex);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 通过Jtag下载比特流文件
 | 
			
		||||
    /// 通过 JTAG 下载比特流文件到 FPGA 设备
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="address"> 设备地址 </param>
 | 
			
		||||
    /// <param name="port"> 设备端口 </param>
 | 
			
		||||
    /// <param name="address">JTAG 设备地址</param>
 | 
			
		||||
    /// <param name="port">JTAG 设备端口</param>
 | 
			
		||||
    /// <returns>下载结果</returns>
 | 
			
		||||
    [HttpPost("DownloadBitstream")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
 | 
			
		||||
    [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
 | 
			
		||||
    public async ValueTask<IResult> DownloadBitstream(string address, int port)
 | 
			
		||||
    {
 | 
			
		||||
        logger.Info($"User {User.Identity?.Name} initiating bitstream download to device {address}:{port}");
 | 
			
		||||
 | 
			
		||||
        // 检查文件
 | 
			
		||||
        var fileDir = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
 | 
			
		||||
        if (!Directory.Exists(fileDir))
 | 
			
		||||
        {
 | 
			
		||||
            logger.Warn($"User {User.Identity?.Name} attempted to download non-existent bitstream for device {address}");
 | 
			
		||||
            return TypedResults.BadRequest("Empty bitstream, Please upload it first");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            // 读取文件
 | 
			
		||||
            var filePath = Directory.GetFiles(fileDir)[0];
 | 
			
		||||
            logger.Info($"User {User.Identity?.Name} reading bitstream file: {filePath}");
 | 
			
		||||
 | 
			
		||||
            using (var fileStream = System.IO.File.Open(filePath, System.IO.FileMode.Open))
 | 
			
		||||
            {
 | 
			
		||||
                if (fileStream is null || fileStream.Length <= 0)
 | 
			
		||||
                {
 | 
			
		||||
                    logger.Warn($"User {User.Identity?.Name} found invalid bitstream file for device {address}");
 | 
			
		||||
                    return TypedResults.BadRequest("Wrong bitstream, Please upload it again");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                logger.Info($"User {User.Identity?.Name} processing bitstream file of size: {fileStream.Length} bytes");
 | 
			
		||||
 | 
			
		||||
                // 定义缓冲区大小: 32KB
 | 
			
		||||
                byte[] buffer = new byte[32 * 1024];
 | 
			
		||||
@@ -158,7 +219,10 @@ public class JtagController : ControllerBase
 | 
			
		||||
                        // 反转 32bits
 | 
			
		||||
                        var retBuffer = Common.Number.ReverseBytes(buffer, 4);
 | 
			
		||||
                        if (!retBuffer.IsSuccessful)
 | 
			
		||||
                        {
 | 
			
		||||
                            logger.Error($"User {User.Identity?.Name} failed to reverse bytes: {retBuffer.Error}");
 | 
			
		||||
                            return TypedResults.InternalServerError(retBuffer.Error);
 | 
			
		||||
                        }
 | 
			
		||||
                        revBuffer = retBuffer.Value;
 | 
			
		||||
 | 
			
		||||
                        for (int i = 0; i < revBuffer.Length; i++)
 | 
			
		||||
@@ -172,6 +236,7 @@ public class JtagController : ControllerBase
 | 
			
		||||
 | 
			
		||||
                    // 将所有数据转换为字节数组(注意:如果文件非常大,可能不适合完全加载到内存)
 | 
			
		||||
                    var fileBytes = memoryStream.ToArray();
 | 
			
		||||
                    logger.Info($"User {User.Identity?.Name} processed {totalBytesRead} bytes for device {address}");
 | 
			
		||||
 | 
			
		||||
                    // 下载比特流
 | 
			
		||||
                    var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
 | 
			
		||||
@@ -179,100 +244,140 @@ public class JtagController : ControllerBase
 | 
			
		||||
 | 
			
		||||
                    if (ret.IsSuccessful)
 | 
			
		||||
                    {
 | 
			
		||||
                        logger.Info($"Device {address} dowload bitstream successfully");
 | 
			
		||||
                        logger.Info($"User {User.Identity?.Name} successfully downloaded bitstream to device {address}");
 | 
			
		||||
                        return TypedResults.Ok(ret.Value);
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        logger.Error(ret.Error);
 | 
			
		||||
                        logger.Error($"User {User.Identity?.Name} failed to download bitstream to device {address}: {ret.Error}");
 | 
			
		||||
                        return TypedResults.InternalServerError(ret.Error);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception error)
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            return TypedResults.InternalServerError(error);
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
            logger.Error(ex, $"User {User.Identity?.Name} encountered exception while downloading bitstream to device {address}");
 | 
			
		||||
            return TypedResults.InternalServerError(ex);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// [TODO:description]
 | 
			
		||||
    /// 执行边界扫描,获取所有端口状态
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="address">[TODO:parameter]</param>
 | 
			
		||||
    /// <param name="port">[TODO:parameter]</param>
 | 
			
		||||
    /// <returns>[TODO:return]</returns>
 | 
			
		||||
    /// <param name="address">JTAG 设备地址</param>
 | 
			
		||||
    /// <param name="port">JTAG 设备端口</param>
 | 
			
		||||
    /// <returns>边界扫描结果</returns>
 | 
			
		||||
    [HttpPost("BoundaryScanAllPorts")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
 | 
			
		||||
    [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
 | 
			
		||||
    public async ValueTask<IResult> BoundaryScanAllPorts(string address, int port)
 | 
			
		||||
    {
 | 
			
		||||
        var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
 | 
			
		||||
        var ret = await jtagCtrl.BoundaryScan();
 | 
			
		||||
        if (!ret.IsSuccessful)
 | 
			
		||||
        {
 | 
			
		||||
            if (ret.Error is ArgumentException)
 | 
			
		||||
                return TypedResults.BadRequest(ret.Error);
 | 
			
		||||
            else return TypedResults.InternalServerError(ret.Error);
 | 
			
		||||
        }
 | 
			
		||||
        logger.Info($"User {User.Identity?.Name} initiating boundary scan for all ports on device {address}:{port}");
 | 
			
		||||
 | 
			
		||||
        return TypedResults.Ok(ret.Value);
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
 | 
			
		||||
            var ret = await jtagCtrl.BoundaryScan();
 | 
			
		||||
 | 
			
		||||
            if (!ret.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"User {User.Identity?.Name} boundary scan failed for device {address}: {ret.Error}");
 | 
			
		||||
                if (ret.Error is ArgumentException)
 | 
			
		||||
                    return TypedResults.BadRequest(ret.Error);
 | 
			
		||||
                else
 | 
			
		||||
                    return TypedResults.InternalServerError(ret.Error);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            logger.Info($"User {User.Identity?.Name} successfully completed boundary scan for device {address}");
 | 
			
		||||
            return TypedResults.Ok(ret.Value);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error(ex, $"User {User.Identity?.Name} encountered exception during boundary scan for device {address}");
 | 
			
		||||
            return TypedResults.InternalServerError(ex);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// [TODO:description]
 | 
			
		||||
    /// 执行逻辑端口边界扫描
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="address">[TODO:parameter]</param>
 | 
			
		||||
    /// <param name="port">[TODO:parameter]</param>
 | 
			
		||||
    /// <returns>[TODO:return]</returns>
 | 
			
		||||
    /// <param name="address">JTAG 设备地址</param>
 | 
			
		||||
    /// <param name="port">JTAG 设备端口</param>
 | 
			
		||||
    /// <returns>逻辑端口状态字典</returns>
 | 
			
		||||
    [HttpPost("BoundaryScanLogicalPorts")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(Dictionary<string, bool>), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
 | 
			
		||||
    public async ValueTask<IResult> BoundaryScanLogicalPorts(string address, int port)
 | 
			
		||||
    {
 | 
			
		||||
        var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
 | 
			
		||||
        var ret = await jtagCtrl.BoundaryScanLogicalPorts();
 | 
			
		||||
        if (!ret.IsSuccessful)
 | 
			
		||||
        {
 | 
			
		||||
            if (ret.Error is ArgumentException)
 | 
			
		||||
                return TypedResults.BadRequest(ret.Error);
 | 
			
		||||
            else return TypedResults.InternalServerError(ret.Error);
 | 
			
		||||
        }
 | 
			
		||||
        logger.Info($"User {User.Identity?.Name} initiating logical ports boundary scan on device {address}:{port}");
 | 
			
		||||
 | 
			
		||||
        return TypedResults.Ok(ret.Value);
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
 | 
			
		||||
            var ret = await jtagCtrl.BoundaryScanLogicalPorts();
 | 
			
		||||
 | 
			
		||||
            if (!ret.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"User {User.Identity?.Name} logical ports boundary scan failed for device {address}: {ret.Error}");
 | 
			
		||||
                if (ret.Error is ArgumentException)
 | 
			
		||||
                    return TypedResults.BadRequest(ret.Error);
 | 
			
		||||
                else
 | 
			
		||||
                    return TypedResults.InternalServerError(ret.Error);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            logger.Info($"User {User.Identity?.Name} successfully completed logical ports boundary scan for device {address}, found {ret.Value?.Count} ports");
 | 
			
		||||
            return TypedResults.Ok(ret.Value);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error(ex, $"User {User.Identity?.Name} encountered exception during logical ports boundary scan for device {address}");
 | 
			
		||||
            return TypedResults.InternalServerError(ex);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// [TODO:description]
 | 
			
		||||
    /// 设置 JTAG 时钟速度
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="address">[TODO:parameter]</param>
 | 
			
		||||
    /// <param name="port">[TODO:parameter]</param>
 | 
			
		||||
    /// <param name="speed">[TODO:parameter]</param>
 | 
			
		||||
    /// <returns>[TODO:return]</returns>
 | 
			
		||||
    /// <param name="address">JTAG 设备地址</param>
 | 
			
		||||
    /// <param name="port">JTAG 设备端口</param>
 | 
			
		||||
    /// <param name="speed">时钟速度 (Hz)</param>
 | 
			
		||||
    /// <returns>设置结果</returns>
 | 
			
		||||
    [HttpPost("SetSpeed")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
 | 
			
		||||
    public async ValueTask<IResult> SetSpeed(string address, int port, UInt32 speed)
 | 
			
		||||
    {
 | 
			
		||||
        var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
 | 
			
		||||
        var ret = await jtagCtrl.SetSpeed(speed);
 | 
			
		||||
        if (!ret.IsSuccessful)
 | 
			
		||||
        {
 | 
			
		||||
            if (ret.Error is ArgumentException)
 | 
			
		||||
                return TypedResults.BadRequest(ret.Error);
 | 
			
		||||
            else return TypedResults.InternalServerError(ret.Error);
 | 
			
		||||
        }
 | 
			
		||||
        logger.Info($"User {User.Identity?.Name} setting JTAG speed to {speed} Hz for device {address}:{port}");
 | 
			
		||||
 | 
			
		||||
        return TypedResults.Ok(ret.Value);
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
 | 
			
		||||
            var ret = await jtagCtrl.SetSpeed(speed);
 | 
			
		||||
 | 
			
		||||
            if (!ret.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"User {User.Identity?.Name} failed to set speed for device {address}: {ret.Error}");
 | 
			
		||||
                if (ret.Error is ArgumentException)
 | 
			
		||||
                    return TypedResults.BadRequest(ret.Error);
 | 
			
		||||
                else
 | 
			
		||||
                    return TypedResults.InternalServerError(ret.Error);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            logger.Info($"User {User.Identity?.Name} successfully set JTAG speed to {speed} Hz for device {address}");
 | 
			
		||||
            return TypedResults.Ok(ret.Value);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error(ex, $"User {User.Identity?.Name} encountered exception while setting speed for device {address}");
 | 
			
		||||
            return TypedResults.InternalServerError(ex);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -46,6 +46,12 @@ public class User
 | 
			
		||||
    [Nullable]
 | 
			
		||||
    public Guid BoardID { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 用户绑定板子的过期时间
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [Nullable]
 | 
			
		||||
    public DateTime? BoardExpireTime { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 用户权限枚举
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -98,6 +104,18 @@ public class Board
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    public required BoardStatus Status { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 占用该板子的用户的唯一标识符
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [Nullable]
 | 
			
		||||
    public Guid OccupiedUserID { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 占用该板子的用户的用户名
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [Nullable]
 | 
			
		||||
    public string? OccupiedUserName { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// FPGA 板子的固件版本号
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -210,7 +228,7 @@ public class AppDataConnection : DataConnection
 | 
			
		||||
    /// <returns>包含用户信息的结果,如果未找到或出错则返回相应状态</returns>
 | 
			
		||||
    public Result<Optional<User>> GetUserByName(string name)
 | 
			
		||||
    {
 | 
			
		||||
        var user = this.User.Where((user) => user.Name == name).ToArray();
 | 
			
		||||
        var user = this.UserTable.Where((user) => user.Name == name).ToArray();
 | 
			
		||||
 | 
			
		||||
        if (user.Length > 1)
 | 
			
		||||
        {
 | 
			
		||||
@@ -235,7 +253,7 @@ public class AppDataConnection : DataConnection
 | 
			
		||||
    /// <returns>包含用户信息的结果,如果未找到或出错则返回相应状态</returns>
 | 
			
		||||
    public Result<Optional<User>> GetUserByEMail(string email)
 | 
			
		||||
    {
 | 
			
		||||
        var user = this.User.Where((user) => user.EMail == email).ToArray();
 | 
			
		||||
        var user = this.UserTable.Where((user) => user.EMail == email).ToArray();
 | 
			
		||||
 | 
			
		||||
        if (user.Length > 1)
 | 
			
		||||
        {
 | 
			
		||||
@@ -270,12 +288,12 @@ public class AppDataConnection : DataConnection
 | 
			
		||||
 | 
			
		||||
        var user = ret.Value.Value;
 | 
			
		||||
 | 
			
		||||
        if (user.Password == password) 
 | 
			
		||||
        if (user.Password == password)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Info($"用户 {name} 密码验证成功");
 | 
			
		||||
            return new(user);
 | 
			
		||||
        }
 | 
			
		||||
        else 
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            logger.Warn($"用户 {name} 密码验证失败");
 | 
			
		||||
            return new(Optional<User>.None);
 | 
			
		||||
@@ -287,15 +305,70 @@ public class AppDataConnection : DataConnection
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="userId">用户的唯一标识符</param>
 | 
			
		||||
    /// <param name="boardId">实验板的唯一标识符</param>
 | 
			
		||||
    /// <param name="expireTime">绑定过期时间</param>
 | 
			
		||||
    /// <returns>更新的记录数</returns>
 | 
			
		||||
    public int BindUserToBoard(Guid userId, Guid boardId)
 | 
			
		||||
    public int BindUserToBoard(Guid userId, Guid boardId, DateTime expireTime)
 | 
			
		||||
    {
 | 
			
		||||
        var result = this.User
 | 
			
		||||
        // 获取用户信息
 | 
			
		||||
        var user = this.UserTable.Where(u => u.ID == userId).FirstOrDefault();
 | 
			
		||||
        if (user == null)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"未找到用户: {userId}");
 | 
			
		||||
            return 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 更新用户的板子绑定信息
 | 
			
		||||
        var userResult = this.UserTable
 | 
			
		||||
            .Where(u => u.ID == userId)
 | 
			
		||||
            .Set(u => u.BoardID, boardId)
 | 
			
		||||
            .Set(u => u.BoardExpireTime, expireTime)
 | 
			
		||||
            .Update();
 | 
			
		||||
        logger.Info($"用户 {userId} 已绑定到实验板 {boardId}");
 | 
			
		||||
        return result;
 | 
			
		||||
 | 
			
		||||
        // 更新板子的用户绑定信息
 | 
			
		||||
        var boardResult = this.BoardTable
 | 
			
		||||
            .Where(b => b.ID == boardId)
 | 
			
		||||
            .Set(b => b.Status, Board.BoardStatus.Busy)
 | 
			
		||||
            .Set(b => b.OccupiedUserID, userId)
 | 
			
		||||
            .Set(b => b.OccupiedUserName, user.Name)
 | 
			
		||||
            .Update();
 | 
			
		||||
 | 
			
		||||
        logger.Info($"用户 {userId} ({user.Name}) 已绑定到实验板 {boardId},过期时间: {expireTime}");
 | 
			
		||||
        return userResult + boardResult;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 解除用户与实验板的绑定
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="userId">用户的唯一标识符</param>
 | 
			
		||||
    /// <returns>更新的记录数</returns>
 | 
			
		||||
    public int UnbindUserFromBoard(Guid userId)
 | 
			
		||||
    {
 | 
			
		||||
        // 获取用户当前绑定的板子ID
 | 
			
		||||
        var user = this.UserTable.Where(u => u.ID == userId).FirstOrDefault();
 | 
			
		||||
        Guid boardId = user?.BoardID ?? Guid.Empty;
 | 
			
		||||
 | 
			
		||||
        // 清空用户的板子绑定信息
 | 
			
		||||
        var userResult = this.UserTable
 | 
			
		||||
            .Where(u => u.ID == userId)
 | 
			
		||||
            .Set(u => u.BoardID, Guid.Empty)
 | 
			
		||||
            .Set(u => u.BoardExpireTime, (DateTime?)null)
 | 
			
		||||
            .Update();
 | 
			
		||||
 | 
			
		||||
        // 如果用户原本绑定了板子,则清空板子的用户绑定信息
 | 
			
		||||
        int boardResult = 0;
 | 
			
		||||
        if (boardId != Guid.Empty)
 | 
			
		||||
        {
 | 
			
		||||
            boardResult = this.BoardTable
 | 
			
		||||
                .Where(b => b.ID == boardId)
 | 
			
		||||
                .Set(b => b.Status, Board.BoardStatus.Available)
 | 
			
		||||
                .Set(b => b.OccupiedUserID, Guid.Empty)
 | 
			
		||||
                .Set(b => b.OccupiedUserName, (string?)null)
 | 
			
		||||
                .Update();
 | 
			
		||||
            logger.Info($"实验板 {boardId} 状态已设置为空闲,用户绑定信息已清空");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        logger.Info($"用户 {userId} 已解除实验板绑定");
 | 
			
		||||
        return userResult + boardResult;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -326,7 +399,26 @@ public class AppDataConnection : DataConnection
 | 
			
		||||
    /// <returns>删除的记录数</returns>
 | 
			
		||||
    public int DeleteBoardByName(string name)
 | 
			
		||||
    {
 | 
			
		||||
        var result = this.Board.Where(board => board.BoardName == name).Delete();
 | 
			
		||||
        // 先获取要删除的板子信息
 | 
			
		||||
        var board = this.BoardTable.Where(b => b.BoardName == name).FirstOrDefault();
 | 
			
		||||
        if (board == null)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Warn($"未找到名称为 {name} 的实验板");
 | 
			
		||||
            return 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 如果板子被占用,先解除绑定
 | 
			
		||||
        if (board.OccupiedUserID != Guid.Empty)
 | 
			
		||||
        {
 | 
			
		||||
            this.UserTable
 | 
			
		||||
                .Where(u => u.ID == board.OccupiedUserID)
 | 
			
		||||
                .Set(u => u.BoardID, Guid.Empty)
 | 
			
		||||
                .Set(u => u.BoardExpireTime, (DateTime?)null)
 | 
			
		||||
                .Update();
 | 
			
		||||
            logger.Info($"已解除用户 {board.OccupiedUserID} 与实验板 {name} 的绑定");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var result = this.BoardTable.Where(b => b.BoardName == name).Delete();
 | 
			
		||||
        logger.Info($"实验板已删除: {name},删除记录数: {result}");
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
@@ -338,18 +430,62 @@ public class AppDataConnection : DataConnection
 | 
			
		||||
    /// <returns>删除的记录数</returns>
 | 
			
		||||
    public int DeleteBoardByID(Guid id)
 | 
			
		||||
    {
 | 
			
		||||
        var result = this.Board.Where(board => board.ID == id).Delete();
 | 
			
		||||
        // 先获取要删除的板子信息
 | 
			
		||||
        var board = this.BoardTable.Where(b => b.ID == id).FirstOrDefault();
 | 
			
		||||
        if (board == null)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Warn($"未找到ID为 {id} 的实验板");
 | 
			
		||||
            return 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 如果板子被占用,先解除绑定
 | 
			
		||||
        if (board.OccupiedUserID != Guid.Empty)
 | 
			
		||||
        {
 | 
			
		||||
            this.UserTable
 | 
			
		||||
                .Where(u => u.ID == board.OccupiedUserID)
 | 
			
		||||
                .Set(u => u.BoardID, Guid.Empty)
 | 
			
		||||
                .Set(u => u.BoardExpireTime, (DateTime?)null)
 | 
			
		||||
                .Update();
 | 
			
		||||
            logger.Info($"已解除用户 {board.OccupiedUserID} 与实验板 {id} 的绑定");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var result = this.BoardTable.Where(b => b.ID == id).Delete();
 | 
			
		||||
        logger.Info($"实验板已删除: {id},删除记录数: {result}");
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 根据实验板ID获取实验板信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="id">实验板的唯一标识符</param>
 | 
			
		||||
    /// <returns>包含实验板信息的结果,如果未找到则返回空</returns>
 | 
			
		||||
    public Result<Optional<Board>> GetBoardByID(Guid id)
 | 
			
		||||
    {
 | 
			
		||||
        var boards = this.BoardTable.Where(board => board.ID == id).ToArray();
 | 
			
		||||
 | 
			
		||||
        if (boards.Length > 1)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"数据库中存在多个相同ID的实验板: {id}");
 | 
			
		||||
            return new(new Exception($"数据库中存在多个相同ID的实验板: {id}"));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (boards.Length == 0)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Info($"未找到ID对应的实验板: {id}");
 | 
			
		||||
            return new(Optional<Board>.None);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        logger.Debug($"成功获取实验板信息: {id}");
 | 
			
		||||
        return new(boards[0]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取所有实验板信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>所有实验板的数组</returns>
 | 
			
		||||
    public Board[] GetAllBoard()
 | 
			
		||||
    {
 | 
			
		||||
        var boards = this.Board.ToArray();
 | 
			
		||||
        var boards = this.BoardTable.ToArray();
 | 
			
		||||
        logger.Debug($"获取所有实验板,共 {boards.Length} 块");
 | 
			
		||||
        return boards;
 | 
			
		||||
    }
 | 
			
		||||
@@ -357,10 +493,12 @@ public class AppDataConnection : DataConnection
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取一块可用的实验板并将其状态设置为繁忙
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="userId">要分配板子的用户ID</param>
 | 
			
		||||
    /// <param name="expireTime">绑定过期时间</param>
 | 
			
		||||
    /// <returns>可用的实验板,如果没有可用的板子则返回空</returns>
 | 
			
		||||
    public Optional<Board> GetAvailableBoard()
 | 
			
		||||
    public Optional<Board> GetAvailableBoard(Guid userId, DateTime expireTime)
 | 
			
		||||
    {
 | 
			
		||||
        var boards = this.Board.Where(
 | 
			
		||||
        var boards = this.BoardTable.Where(
 | 
			
		||||
                (board) => board.Status == Database.Board.BoardStatus.Available
 | 
			
		||||
            ).ToArray();
 | 
			
		||||
 | 
			
		||||
@@ -372,12 +510,34 @@ public class AppDataConnection : DataConnection
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            var board = boards[0];
 | 
			
		||||
            board.Status = Database.Board.BoardStatus.Busy;
 | 
			
		||||
            this.Board
 | 
			
		||||
            var user = this.UserTable.Where(u => u.ID == userId).FirstOrDefault();
 | 
			
		||||
            
 | 
			
		||||
            if (user == null)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"未找到用户: {userId}");
 | 
			
		||||
                return new(null);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 更新板子状态和用户绑定信息
 | 
			
		||||
            this.BoardTable
 | 
			
		||||
                .Where(target => target.ID == board.ID)
 | 
			
		||||
                .Set(target => target.Status, board.Status)
 | 
			
		||||
                .Set(target => target.Status, Board.BoardStatus.Busy)
 | 
			
		||||
                .Set(target => target.OccupiedUserID, userId)
 | 
			
		||||
                .Set(target => target.OccupiedUserName, user.Name)
 | 
			
		||||
                .Update();
 | 
			
		||||
            logger.Info($"实验板 {board.BoardName} ({board.ID}) 已分配,状态更新为繁忙");
 | 
			
		||||
 | 
			
		||||
            // 更新用户的板子绑定信息
 | 
			
		||||
            this.UserTable
 | 
			
		||||
                .Where(u => u.ID == userId)
 | 
			
		||||
                .Set(u => u.BoardID, board.ID)
 | 
			
		||||
                .Set(u => u.BoardExpireTime, expireTime)
 | 
			
		||||
                .Update();
 | 
			
		||||
 | 
			
		||||
            board.Status = Database.Board.BoardStatus.Busy;
 | 
			
		||||
            board.OccupiedUserID = userId;
 | 
			
		||||
            board.OccupiedUserName = user.Name;
 | 
			
		||||
 | 
			
		||||
            logger.Info($"实验板 {board.BoardName} ({board.ID}) 已分配给用户 {user.Name} ({userId}),过期时间: {expireTime}");
 | 
			
		||||
            return new(board);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -385,10 +545,10 @@ public class AppDataConnection : DataConnection
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 用户表
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public ITable<User> User => this.GetTable<User>();
 | 
			
		||||
    public ITable<User> UserTable => this.GetTable<User>();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// FPGA 板子表
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public ITable<Board> Board => this.GetTable<Board>();
 | 
			
		||||
    public ITable<Board> BoardTable => this.GetTable<Board>();
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										237
									
								
								src/APIClient.ts
									
									
									
									
									
								
							
							
						
						
									
										237
									
								
								src/APIClient.ts
									
									
									
									
									
								
							@@ -651,9 +651,14 @@ export class DataClient {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取一个空闲的实验板(普通用户权限)
 | 
			
		||||
     * @param durationHours (optional) 绑定持续时间(小时),默认为1小时
 | 
			
		||||
     */
 | 
			
		||||
    getAvailableBoard(): Promise<Board> {
 | 
			
		||||
        let url_ = this.baseUrl + "/api/Data/GetAvailableBoard";
 | 
			
		||||
    getAvailableBoard(durationHours: number | undefined): Promise<Board> {
 | 
			
		||||
        let url_ = this.baseUrl + "/api/Data/GetAvailableBoard?";
 | 
			
		||||
        if (durationHours === null)
 | 
			
		||||
            throw new Error("The parameter 'durationHours' cannot be null.");
 | 
			
		||||
        else if (durationHours !== undefined)
 | 
			
		||||
            url_ += "durationHours=" + encodeURIComponent("" + durationHours) + "&";
 | 
			
		||||
        url_ = url_.replace(/[?&]$/, "");
 | 
			
		||||
 | 
			
		||||
        let options_: RequestInit = {
 | 
			
		||||
@@ -697,6 +702,108 @@ export class DataClient {
 | 
			
		||||
        return Promise.resolve<Board>(null as any);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 解除当前用户绑定的实验板(普通用户权限)
 | 
			
		||||
     */
 | 
			
		||||
    unbindBoard(): Promise<boolean> {
 | 
			
		||||
        let url_ = this.baseUrl + "/api/Data/UnbindBoard";
 | 
			
		||||
        url_ = url_.replace(/[?&]$/, "");
 | 
			
		||||
 | 
			
		||||
        let options_: RequestInit = {
 | 
			
		||||
            method: "POST",
 | 
			
		||||
            headers: {
 | 
			
		||||
                "Accept": "application/json"
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return this.http.fetch(url_, options_).then((_response: Response) => {
 | 
			
		||||
            return this.processUnbindBoard(_response);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected processUnbindBoard(response: Response): Promise<boolean> {
 | 
			
		||||
        const status = response.status;
 | 
			
		||||
        let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
 | 
			
		||||
        if (status === 200) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            let result200: any = null;
 | 
			
		||||
            let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
 | 
			
		||||
                result200 = resultData200 !== undefined ? resultData200 : <any>null;
 | 
			
		||||
    
 | 
			
		||||
            return result200;
 | 
			
		||||
            });
 | 
			
		||||
        } else if (status === 400) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            let result400: any = null;
 | 
			
		||||
            let resultData400 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
 | 
			
		||||
            result400 = ProblemDetails.fromJS(resultData400);
 | 
			
		||||
            return throwException("A server side error occurred.", status, _responseText, _headers, result400);
 | 
			
		||||
            });
 | 
			
		||||
        } else if (status === 500) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            return throwException("A server side error occurred.", status, _responseText, _headers);
 | 
			
		||||
            });
 | 
			
		||||
        } else if (status !== 200 && status !== 204) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            return throwException("An unexpected server error occurred.", status, _responseText, _headers);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        return Promise.resolve<boolean>(null as any);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 用户根据实验板ID获取实验板信息(普通用户权限)
 | 
			
		||||
     * @param id (optional) 
 | 
			
		||||
     */
 | 
			
		||||
    getBoardByID(id: string | undefined): Promise<Board> {
 | 
			
		||||
        let url_ = this.baseUrl + "/api/Data/GetBoardByID?";
 | 
			
		||||
        if (id === null)
 | 
			
		||||
            throw new Error("The parameter 'id' cannot be null.");
 | 
			
		||||
        else if (id !== undefined)
 | 
			
		||||
            url_ += "id=" + encodeURIComponent("" + id) + "&";
 | 
			
		||||
        url_ = url_.replace(/[?&]$/, "");
 | 
			
		||||
 | 
			
		||||
        let options_: RequestInit = {
 | 
			
		||||
            method: "GET",
 | 
			
		||||
            headers: {
 | 
			
		||||
                "Accept": "application/json"
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return this.http.fetch(url_, options_).then((_response: Response) => {
 | 
			
		||||
            return this.processGetBoardByID(_response);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected processGetBoardByID(response: Response): Promise<Board> {
 | 
			
		||||
        const status = response.status;
 | 
			
		||||
        let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
 | 
			
		||||
        if (status === 200) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            let result200: any = null;
 | 
			
		||||
            let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
 | 
			
		||||
            result200 = Board.fromJS(resultData200);
 | 
			
		||||
            return result200;
 | 
			
		||||
            });
 | 
			
		||||
        } else if (status === 404) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            let result404: any = null;
 | 
			
		||||
            let resultData404 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
 | 
			
		||||
            result404 = ProblemDetails.fromJS(resultData404);
 | 
			
		||||
            return throwException("A server side error occurred.", status, _responseText, _headers, result404);
 | 
			
		||||
            });
 | 
			
		||||
        } else if (status === 500) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            return throwException("A server side error occurred.", status, _responseText, _headers);
 | 
			
		||||
            });
 | 
			
		||||
        } else if (status !== 200 && status !== 204) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            return throwException("An unexpected server error occurred.", status, _responseText, _headers);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        return Promise.resolve<Board>(null as any);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 新增板子(管理员权限)
 | 
			
		||||
     * @param name (optional) 
 | 
			
		||||
@@ -1115,7 +1222,8 @@ export class JtagClient {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 页面
 | 
			
		||||
     * 控制器首页信息
 | 
			
		||||
     * @return 控制器描述信息
 | 
			
		||||
     */
 | 
			
		||||
    index(): Promise<string> {
 | 
			
		||||
        let url_ = this.baseUrl + "/api/Jtag";
 | 
			
		||||
@@ -1153,9 +1261,10 @@ export class JtagClient {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取Jtag ID Code
 | 
			
		||||
     * @param address (optional) 设备地址
 | 
			
		||||
     * @param port (optional) 设备端口
 | 
			
		||||
     * 获取 JTAG 设备的 ID Code
 | 
			
		||||
     * @param address (optional) JTAG 设备地址
 | 
			
		||||
     * @param port (optional) JTAG 设备端口
 | 
			
		||||
     * @return 设备的 ID Code
 | 
			
		||||
     */
 | 
			
		||||
    getDeviceIDCode(address: string | undefined, port: number | undefined): Promise<number> {
 | 
			
		||||
        let url_ = this.baseUrl + "/api/Jtag/GetDeviceIDCode?";
 | 
			
		||||
@@ -1199,6 +1308,13 @@ export class JtagClient {
 | 
			
		||||
            result500 = Exception.fromJS(resultData500);
 | 
			
		||||
            return throwException("A server side error occurred.", status, _responseText, _headers, result500);
 | 
			
		||||
            });
 | 
			
		||||
        } else if (status === 401) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            let result401: any = null;
 | 
			
		||||
            let resultData401 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
 | 
			
		||||
            result401 = ProblemDetails.fromJS(resultData401);
 | 
			
		||||
            return throwException("A server side error occurred.", status, _responseText, _headers, result401);
 | 
			
		||||
            });
 | 
			
		||||
        } else if (status !== 200 && status !== 204) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            return throwException("An unexpected server error occurred.", status, _responseText, _headers);
 | 
			
		||||
@@ -1208,9 +1324,10 @@ export class JtagClient {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取状态寄存器
 | 
			
		||||
     * @param address (optional) 设备地址
 | 
			
		||||
     * @param port (optional) 设备端口
 | 
			
		||||
     * 读取 JTAG 设备的状态寄存器
 | 
			
		||||
     * @param address (optional) JTAG 设备地址
 | 
			
		||||
     * @param port (optional) JTAG 设备端口
 | 
			
		||||
     * @return 状态寄存器的原始值、二进制表示和解码值
 | 
			
		||||
     */
 | 
			
		||||
    readStatusReg(address: string | undefined, port: number | undefined): Promise<void> {
 | 
			
		||||
        let url_ = this.baseUrl + "/api/Jtag/ReadStatusReg?";
 | 
			
		||||
@@ -1246,6 +1363,13 @@ export class JtagClient {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            return throwException("A server side error occurred.", status, _responseText, _headers);
 | 
			
		||||
            });
 | 
			
		||||
        } else if (status === 401) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            let result401: any = null;
 | 
			
		||||
            let resultData401 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
 | 
			
		||||
            result401 = ProblemDetails.fromJS(resultData401);
 | 
			
		||||
            return throwException("A server side error occurred.", status, _responseText, _headers, result401);
 | 
			
		||||
            });
 | 
			
		||||
        } else if (status !== 200 && status !== 204) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            return throwException("An unexpected server error occurred.", status, _responseText, _headers);
 | 
			
		||||
@@ -1255,9 +1379,10 @@ export class JtagClient {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 上传比特流文件
 | 
			
		||||
     * @param address (optional) 设备地址
 | 
			
		||||
     * 上传比特流文件到服务器
 | 
			
		||||
     * @param address (optional) 目标设备地址
 | 
			
		||||
     * @param file (optional) 比特流文件
 | 
			
		||||
     * @return 上传结果
 | 
			
		||||
     */
 | 
			
		||||
    uploadBitstream(address: string | undefined, file: FileParameter | undefined): Promise<boolean> {
 | 
			
		||||
        let url_ = this.baseUrl + "/api/Jtag/UploadBitstream?";
 | 
			
		||||
@@ -1305,6 +1430,17 @@ export class JtagClient {
 | 
			
		||||
    
 | 
			
		||||
            return throwException("A server side error occurred.", status, _responseText, _headers, result400);
 | 
			
		||||
            });
 | 
			
		||||
        } else if (status === 401) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            let result401: any = null;
 | 
			
		||||
            let resultData401 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
 | 
			
		||||
            result401 = ProblemDetails.fromJS(resultData401);
 | 
			
		||||
            return throwException("A server side error occurred.", status, _responseText, _headers, result401);
 | 
			
		||||
            });
 | 
			
		||||
        } else if (status === 500) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            return throwException("A server side error occurred.", status, _responseText, _headers);
 | 
			
		||||
            });
 | 
			
		||||
        } else if (status !== 200 && status !== 204) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            return throwException("An unexpected server error occurred.", status, _responseText, _headers);
 | 
			
		||||
@@ -1314,9 +1450,10 @@ export class JtagClient {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 通过Jtag下载比特流文件
 | 
			
		||||
     * @param address (optional) 设备地址
 | 
			
		||||
     * @param port (optional) 设备端口
 | 
			
		||||
     * 通过 JTAG 下载比特流文件到 FPGA 设备
 | 
			
		||||
     * @param address (optional) JTAG 设备地址
 | 
			
		||||
     * @param port (optional) JTAG 设备端口
 | 
			
		||||
     * @return 下载结果
 | 
			
		||||
     */
 | 
			
		||||
    downloadBitstream(address: string | undefined, port: number | undefined): Promise<boolean> {
 | 
			
		||||
        let url_ = this.baseUrl + "/api/Jtag/DownloadBitstream?";
 | 
			
		||||
@@ -1368,6 +1505,13 @@ export class JtagClient {
 | 
			
		||||
            result500 = Exception.fromJS(resultData500);
 | 
			
		||||
            return throwException("A server side error occurred.", status, _responseText, _headers, result500);
 | 
			
		||||
            });
 | 
			
		||||
        } else if (status === 401) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            let result401: any = null;
 | 
			
		||||
            let resultData401 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
 | 
			
		||||
            result401 = ProblemDetails.fromJS(resultData401);
 | 
			
		||||
            return throwException("A server side error occurred.", status, _responseText, _headers, result401);
 | 
			
		||||
            });
 | 
			
		||||
        } else if (status !== 200 && status !== 204) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            return throwException("An unexpected server error occurred.", status, _responseText, _headers);
 | 
			
		||||
@@ -1377,10 +1521,10 @@ export class JtagClient {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * [TODO:description]
 | 
			
		||||
     * @param address (optional) [TODO:parameter]
 | 
			
		||||
     * @param port (optional) [TODO:parameter]
 | 
			
		||||
     * @return [TODO:return]
 | 
			
		||||
     * 执行边界扫描,获取所有端口状态
 | 
			
		||||
     * @param address (optional) JTAG 设备地址
 | 
			
		||||
     * @param port (optional) JTAG 设备端口
 | 
			
		||||
     * @return 边界扫描结果
 | 
			
		||||
     */
 | 
			
		||||
    boundaryScanAllPorts(address: string | undefined, port: number | undefined): Promise<boolean> {
 | 
			
		||||
        let url_ = this.baseUrl + "/api/Jtag/BoundaryScanAllPorts?";
 | 
			
		||||
@@ -1432,6 +1576,13 @@ export class JtagClient {
 | 
			
		||||
            result500 = Exception.fromJS(resultData500);
 | 
			
		||||
            return throwException("A server side error occurred.", status, _responseText, _headers, result500);
 | 
			
		||||
            });
 | 
			
		||||
        } else if (status === 401) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            let result401: any = null;
 | 
			
		||||
            let resultData401 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
 | 
			
		||||
            result401 = ProblemDetails.fromJS(resultData401);
 | 
			
		||||
            return throwException("A server side error occurred.", status, _responseText, _headers, result401);
 | 
			
		||||
            });
 | 
			
		||||
        } else if (status !== 200 && status !== 204) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            return throwException("An unexpected server error occurred.", status, _responseText, _headers);
 | 
			
		||||
@@ -1441,10 +1592,10 @@ export class JtagClient {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * [TODO:description]
 | 
			
		||||
     * @param address (optional) [TODO:parameter]
 | 
			
		||||
     * @param port (optional) [TODO:parameter]
 | 
			
		||||
     * @return [TODO:return]
 | 
			
		||||
     * 执行逻辑端口边界扫描
 | 
			
		||||
     * @param address (optional) JTAG 设备地址
 | 
			
		||||
     * @param port (optional) JTAG 设备端口
 | 
			
		||||
     * @return 逻辑端口状态字典
 | 
			
		||||
     */
 | 
			
		||||
    boundaryScanLogicalPorts(address: string | undefined, port: number | undefined): Promise<{ [key: string]: boolean; }> {
 | 
			
		||||
        let url_ = this.baseUrl + "/api/Jtag/BoundaryScanLogicalPorts?";
 | 
			
		||||
@@ -1496,6 +1647,13 @@ export class JtagClient {
 | 
			
		||||
            result500 = Exception.fromJS(resultData500);
 | 
			
		||||
            return throwException("A server side error occurred.", status, _responseText, _headers, result500);
 | 
			
		||||
            });
 | 
			
		||||
        } else if (status === 401) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            let result401: any = null;
 | 
			
		||||
            let resultData401 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
 | 
			
		||||
            result401 = ProblemDetails.fromJS(resultData401);
 | 
			
		||||
            return throwException("A server side error occurred.", status, _responseText, _headers, result401);
 | 
			
		||||
            });
 | 
			
		||||
        } else if (status !== 200 && status !== 204) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            return throwException("An unexpected server error occurred.", status, _responseText, _headers);
 | 
			
		||||
@@ -1505,11 +1663,11 @@ export class JtagClient {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * [TODO:description]
 | 
			
		||||
     * @param address (optional) [TODO:parameter]
 | 
			
		||||
     * @param port (optional) [TODO:parameter]
 | 
			
		||||
     * @param speed (optional) [TODO:parameter]
 | 
			
		||||
     * @return [TODO:return]
 | 
			
		||||
     * 设置 JTAG 时钟速度
 | 
			
		||||
     * @param address (optional) JTAG 设备地址
 | 
			
		||||
     * @param port (optional) JTAG 设备端口
 | 
			
		||||
     * @param speed (optional) 时钟速度 (Hz)
 | 
			
		||||
     * @return 设置结果
 | 
			
		||||
     */
 | 
			
		||||
    setSpeed(address: string | undefined, port: number | undefined, speed: number | undefined): Promise<boolean> {
 | 
			
		||||
        let url_ = this.baseUrl + "/api/Jtag/SetSpeed?";
 | 
			
		||||
@@ -1557,6 +1715,13 @@ export class JtagClient {
 | 
			
		||||
            result500 = Exception.fromJS(resultData500);
 | 
			
		||||
            return throwException("A server side error occurred.", status, _responseText, _headers, result500);
 | 
			
		||||
            });
 | 
			
		||||
        } else if (status === 401) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            let result401: any = null;
 | 
			
		||||
            let resultData401 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
 | 
			
		||||
            result401 = ProblemDetails.fromJS(resultData401);
 | 
			
		||||
            return throwException("A server side error occurred.", status, _responseText, _headers, result401);
 | 
			
		||||
            });
 | 
			
		||||
        } else if (status !== 200 && status !== 204) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            return throwException("An unexpected server error occurred.", status, _responseText, _headers);
 | 
			
		||||
@@ -2715,6 +2880,8 @@ export class UserInfo implements IUserInfo {
 | 
			
		||||
    eMail!: string;
 | 
			
		||||
    /** 用户关联的板卡ID */
 | 
			
		||||
    boardID!: string;
 | 
			
		||||
    /** 用户绑定板子的过期时间 */
 | 
			
		||||
    boardExpireTime?: Date | undefined;
 | 
			
		||||
 | 
			
		||||
    constructor(data?: IUserInfo) {
 | 
			
		||||
        if (data) {
 | 
			
		||||
@@ -2731,6 +2898,7 @@ export class UserInfo implements IUserInfo {
 | 
			
		||||
            this.name = _data["name"];
 | 
			
		||||
            this.eMail = _data["eMail"];
 | 
			
		||||
            this.boardID = _data["boardID"];
 | 
			
		||||
            this.boardExpireTime = _data["boardExpireTime"] ? new Date(_data["boardExpireTime"].toString()) : <any>undefined;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -2747,6 +2915,7 @@ export class UserInfo implements IUserInfo {
 | 
			
		||||
        data["name"] = this.name;
 | 
			
		||||
        data["eMail"] = this.eMail;
 | 
			
		||||
        data["boardID"] = this.boardID;
 | 
			
		||||
        data["boardExpireTime"] = this.boardExpireTime ? this.boardExpireTime.toISOString() : <any>undefined;
 | 
			
		||||
        return data;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -2761,6 +2930,8 @@ export interface IUserInfo {
 | 
			
		||||
    eMail: string;
 | 
			
		||||
    /** 用户关联的板卡ID */
 | 
			
		||||
    boardID: string;
 | 
			
		||||
    /** 用户绑定板子的过期时间 */
 | 
			
		||||
    boardExpireTime?: Date | undefined;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** FPGA 板子类,表示板子信息 */
 | 
			
		||||
@@ -2775,6 +2946,10 @@ export class Board implements IBoard {
 | 
			
		||||
    port!: number;
 | 
			
		||||
    /** FPGA 板子的当前状态 */
 | 
			
		||||
    status!: BoardStatus;
 | 
			
		||||
    /** 占用该板子的用户的唯一标识符 */
 | 
			
		||||
    occupiedUserID!: string;
 | 
			
		||||
    /** 占用该板子的用户的用户名 */
 | 
			
		||||
    occupiedUserName?: string | undefined;
 | 
			
		||||
    /** FPGA 板子的固件版本号 */
 | 
			
		||||
    firmVersion!: string;
 | 
			
		||||
 | 
			
		||||
@@ -2794,6 +2969,8 @@ export class Board implements IBoard {
 | 
			
		||||
            this.ipAddr = _data["ipAddr"];
 | 
			
		||||
            this.port = _data["port"];
 | 
			
		||||
            this.status = _data["status"];
 | 
			
		||||
            this.occupiedUserID = _data["occupiedUserID"];
 | 
			
		||||
            this.occupiedUserName = _data["occupiedUserName"];
 | 
			
		||||
            this.firmVersion = _data["firmVersion"];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -2812,6 +2989,8 @@ export class Board implements IBoard {
 | 
			
		||||
        data["ipAddr"] = this.ipAddr;
 | 
			
		||||
        data["port"] = this.port;
 | 
			
		||||
        data["status"] = this.status;
 | 
			
		||||
        data["occupiedUserID"] = this.occupiedUserID;
 | 
			
		||||
        data["occupiedUserName"] = this.occupiedUserName;
 | 
			
		||||
        data["firmVersion"] = this.firmVersion;
 | 
			
		||||
        return data;
 | 
			
		||||
    }
 | 
			
		||||
@@ -2829,6 +3008,10 @@ export interface IBoard {
 | 
			
		||||
    port: number;
 | 
			
		||||
    /** FPGA 板子的当前状态 */
 | 
			
		||||
    status: BoardStatus;
 | 
			
		||||
    /** 占用该板子的用户的唯一标识符 */
 | 
			
		||||
    occupiedUserID: string;
 | 
			
		||||
    /** 占用该板子的用户的用户名 */
 | 
			
		||||
    occupiedUserName?: string | undefined;
 | 
			
		||||
    /** FPGA 板子的固件版本号 */
 | 
			
		||||
    firmVersion: string;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -152,7 +152,7 @@ const loadUserInfo = async () => {
 | 
			
		||||
  try {
 | 
			
		||||
    const authenticated = await AuthManager.isAuthenticated();
 | 
			
		||||
    if (authenticated) {
 | 
			
		||||
      const client = AuthManager.createAuthenticatedClient();
 | 
			
		||||
      const client = AuthManager.createAuthenticatedDataClient();
 | 
			
		||||
      const userInfo = await client.getUserInfo();
 | 
			
		||||
      userName.value = userInfo.name;
 | 
			
		||||
      isLoggedIn.value = true;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,28 @@
 | 
			
		||||
import { DataClient } from "@/APIClient";
 | 
			
		||||
import {
 | 
			
		||||
  DataClient,
 | 
			
		||||
  VideoStreamClient,
 | 
			
		||||
  BsdlParserClient,
 | 
			
		||||
  DDSClient,
 | 
			
		||||
  JtagClient,
 | 
			
		||||
  MatrixKeyClient,
 | 
			
		||||
  PowerClient,
 | 
			
		||||
  RemoteUpdateClient,
 | 
			
		||||
  TutorialClient,
 | 
			
		||||
  UDPClient,
 | 
			
		||||
} from "@/APIClient";
 | 
			
		||||
 | 
			
		||||
// 支持的客户端类型联合类型
 | 
			
		||||
type SupportedClient =
 | 
			
		||||
  | DataClient
 | 
			
		||||
  | VideoStreamClient
 | 
			
		||||
  | BsdlParserClient
 | 
			
		||||
  | DDSClient
 | 
			
		||||
  | JtagClient
 | 
			
		||||
  | MatrixKeyClient
 | 
			
		||||
  | PowerClient
 | 
			
		||||
  | RemoteUpdateClient
 | 
			
		||||
  | TutorialClient
 | 
			
		||||
  | UDPClient;
 | 
			
		||||
 | 
			
		||||
export class AuthManager {
 | 
			
		||||
  // 存储token到localStorage
 | 
			
		||||
@@ -21,30 +45,110 @@ export class AuthManager {
 | 
			
		||||
    return await AuthManager.verifyToken();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 为HTTP请求添加Authorization header
 | 
			
		||||
  public static addAuthHeader(client: any): void {
 | 
			
		||||
  // 通用的为HTTP请求添加Authorization header的方法
 | 
			
		||||
  public static addAuthHeader(client: SupportedClient): void {
 | 
			
		||||
    const token = AuthManager.getToken();
 | 
			
		||||
    if (token && client.http) {
 | 
			
		||||
      const originalFetch = client.http.fetch;
 | 
			
		||||
      client.http.fetch = (url: RequestInfo, init?: RequestInit) => {
 | 
			
		||||
    if (token) {
 | 
			
		||||
      // 创建一个自定义的 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 = {};
 | 
			
		||||
 | 
			
		||||
        // 添加Authorization header
 | 
			
		||||
        if (typeof init.headers === "object" && init.headers !== null) {
 | 
			
		||||
          (init.headers as any)["Authorization"] = `Bearer ${token}`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return originalFetch(url, init);
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
        return (window as any).fetch(url, init);
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 创建已配置认证的API客户端
 | 
			
		||||
  public static createAuthenticatedClient(): DataClient {
 | 
			
		||||
    const client = new DataClient();
 | 
			
		||||
    AuthManager.addAuthHeader(client);
 | 
			
		||||
    return client;
 | 
			
		||||
  // 通用的创建已认证客户端的方法(使用泛型)
 | 
			
		||||
  public static createAuthenticatedClient<T extends SupportedClient>(
 | 
			
		||||
    ClientClass: new (baseUrl?: string, http?: any) => T,
 | 
			
		||||
  ): T {
 | 
			
		||||
    const customHttp = AuthManager.createAuthenticatedHttp();
 | 
			
		||||
    return customHttp
 | 
			
		||||
      ? new ClientClass(undefined, customHttp)
 | 
			
		||||
      : new ClientClass();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 便捷方法:创建已配置认证的各种客户端
 | 
			
		||||
  public static createAuthenticatedDataClient(): DataClient {
 | 
			
		||||
    return AuthManager.createAuthenticatedClient(DataClient);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 登录函数
 | 
			
		||||
@@ -60,7 +164,7 @@ export class AuthManager {
 | 
			
		||||
        AuthManager.setToken(token);
 | 
			
		||||
 | 
			
		||||
        // 验证token
 | 
			
		||||
        const authClient = AuthManager.createAuthenticatedClient();
 | 
			
		||||
        const authClient = AuthManager.createAuthenticatedDataClient();
 | 
			
		||||
        await authClient.testAuth();
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
@@ -85,7 +189,7 @@ export class AuthManager {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const client = AuthManager.createAuthenticatedClient();
 | 
			
		||||
      const client = AuthManager.createAuthenticatedDataClient();
 | 
			
		||||
      await client.testAuth();
 | 
			
		||||
      return true;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
@@ -102,7 +206,7 @@ export class AuthManager {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const client = AuthManager.createAuthenticatedClient();
 | 
			
		||||
      const client = AuthManager.createAuthenticatedDataClient();
 | 
			
		||||
      await client.testAdminAuth();
 | 
			
		||||
      return true;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
@@ -119,4 +223,10 @@ export class AuthManager {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 检查客户端是否已配置认证
 | 
			
		||||
  public static isClientAuthenticated(client: SupportedClient): boolean {
 | 
			
		||||
    const token = AuthManager.getToken();
 | 
			
		||||
    return !!token;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -41,7 +41,7 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
 | 
			
		||||
        return { success: false, error: "权限不足" };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const client = AuthManager.createAuthenticatedClient();
 | 
			
		||||
      const client = AuthManager.createAuthenticatedDataClient();
 | 
			
		||||
      const result = await client.getAllBoards();
 | 
			
		||||
 | 
			
		||||
      if (result) {
 | 
			
		||||
@@ -91,7 +91,7 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
 | 
			
		||||
        return { success: false, error: "参数不完整" };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const client = AuthManager.createAuthenticatedClient();
 | 
			
		||||
      const client = AuthManager.createAuthenticatedDataClient();
 | 
			
		||||
      const boardId = await client.addBoard(name, ipAddr, port);
 | 
			
		||||
 | 
			
		||||
      if (boardId) {
 | 
			
		||||
@@ -130,7 +130,7 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
 | 
			
		||||
        return { success: false, error: "板卡ID不能为空" };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const client = AuthManager.createAuthenticatedClient();
 | 
			
		||||
      const client = AuthManager.createAuthenticatedDataClient();
 | 
			
		||||
      const result = await client.deleteBoard(boardId);
 | 
			
		||||
 | 
			
		||||
      if (result > 0) {
 | 
			
		||||
 
 | 
			
		||||
@@ -84,6 +84,13 @@
 | 
			
		||||
      @add-template="handleAddTemplate"
 | 
			
		||||
      @close="showComponentsMenu = false"
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
    <!-- 实验板申请对话框 -->
 | 
			
		||||
    <RequestBoardDialog
 | 
			
		||||
      :open="showRequestBoardDialog"
 | 
			
		||||
      @close="handleRequestBoardClose"
 | 
			
		||||
      @success="handleRequestBoardSuccess"
 | 
			
		||||
    />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
@@ -96,10 +103,13 @@ import ComponentSelector from "@/components/LabCanvas/ComponentSelector.vue";
 | 
			
		||||
import PropertyPanel from "@/components/PropertyPanel.vue";
 | 
			
		||||
import MarkdownRenderer from "@/components/MarkdownRenderer.vue";
 | 
			
		||||
import BottomBar from "@/views/Project/BottomBar.vue";
 | 
			
		||||
import RequestBoardDialog from "@/views/Project/RequestBoardDialog.vue";
 | 
			
		||||
import { useProvideComponentManager } from "@/components/LabCanvas";
 | 
			
		||||
import type { DiagramData } from "@/components/LabCanvas";
 | 
			
		||||
import { useAlertStore } from "@/components/Alert";
 | 
			
		||||
import { AuthManager } from "@/utils/AuthManager";
 | 
			
		||||
import { useEquipments } from "@/stores/equipments";
 | 
			
		||||
import type { Board } from "@/APIClient";
 | 
			
		||||
 | 
			
		||||
import { useRoute } from "vue-router";
 | 
			
		||||
const route = useRoute();
 | 
			
		||||
@@ -108,8 +118,14 @@ const router = useRouter();
 | 
			
		||||
// 提供组件管理服务
 | 
			
		||||
const componentManager = useProvideComponentManager();
 | 
			
		||||
 | 
			
		||||
// 设备管理store
 | 
			
		||||
const equipments = useEquipments();
 | 
			
		||||
 | 
			
		||||
const alert = useAlertStore();
 | 
			
		||||
 | 
			
		||||
// --- 实验板申请对话框 ---
 | 
			
		||||
const showRequestBoardDialog = ref(false);
 | 
			
		||||
 | 
			
		||||
// --- 文档面板控制 ---
 | 
			
		||||
const showDocPanel = ref(false);
 | 
			
		||||
const documentContent = ref("");
 | 
			
		||||
@@ -208,6 +224,62 @@ function updateComponentDirectProp(
 | 
			
		||||
  componentManager.updateComponentDirectProp(componentId, propName, value);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- 实验板管理 ---
 | 
			
		||||
// 检查并初始化用户实验板
 | 
			
		||||
async function checkAndInitializeBoard() {
 | 
			
		||||
  try {
 | 
			
		||||
    const client = AuthManager.createAuthenticatedDataClient();
 | 
			
		||||
    const userInfo = await client.getUserInfo();
 | 
			
		||||
    
 | 
			
		||||
    if (userInfo.boardID && userInfo.boardID.trim() !== '') {
 | 
			
		||||
      // 用户已绑定实验板,获取实验板信息并更新到equipment
 | 
			
		||||
      try {
 | 
			
		||||
        const board = await client.getBoardByID(userInfo.boardID);
 | 
			
		||||
        updateEquipmentFromBoard(board);
 | 
			
		||||
        alert?.show(`实验板 ${board.boardName} 已连接`, "success");
 | 
			
		||||
      } catch (boardError) {
 | 
			
		||||
        console.error('获取实验板信息失败:', boardError);
 | 
			
		||||
        alert?.show("获取实验板信息失败", "error");
 | 
			
		||||
        showRequestBoardDialog.value = true;
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      // 用户未绑定实验板,显示申请对话框
 | 
			
		||||
      showRequestBoardDialog.value = true;
 | 
			
		||||
    }
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('检查用户实验板失败:', error);
 | 
			
		||||
    alert?.show("检查用户信息失败", "error");
 | 
			
		||||
    showRequestBoardDialog.value = true;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 根据实验板信息更新equipment store
 | 
			
		||||
function updateEquipmentFromBoard(board: Board) {
 | 
			
		||||
  equipments.setAddr(board.ipAddr);
 | 
			
		||||
  equipments.setPort(board.port);
 | 
			
		||||
  
 | 
			
		||||
  console.log(`实验板信息已更新到equipment store:`, {
 | 
			
		||||
    address: board.ipAddr,
 | 
			
		||||
    port: board.port,
 | 
			
		||||
    boardName: board.boardName,
 | 
			
		||||
    boardId: board.id
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 处理申请实验板对话框关闭
 | 
			
		||||
function handleRequestBoardClose() {
 | 
			
		||||
  showRequestBoardDialog.value = false;
 | 
			
		||||
  // 如果用户取消申请,可以选择返回上一页或显示警告
 | 
			
		||||
  router.push('/');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 处理申请实验板成功
 | 
			
		||||
function handleRequestBoardSuccess(board: Board) {
 | 
			
		||||
  showRequestBoardDialog.value = false;
 | 
			
		||||
  updateEquipmentFromBoard(board);
 | 
			
		||||
  alert?.show(`实验板 ${board.boardName} 申请成功!`, "success");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- 生命周期钩子 ---
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
  // 验证用户身份
 | 
			
		||||
@@ -224,6 +296,9 @@ onMounted(async () => {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 检查并初始化用户实验板
 | 
			
		||||
  await checkAndInitializeBoard();
 | 
			
		||||
 | 
			
		||||
  // 检查是否有例程参数,如果有则自动打开文档面板
 | 
			
		||||
  if (route.query.tutorial) {
 | 
			
		||||
    showDocPanel.value = true;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										179
									
								
								src/views/Project/RequestBoardDialog.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								src/views/Project/RequestBoardDialog.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,179 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div
 | 
			
		||||
    v-if="open"
 | 
			
		||||
    class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"
 | 
			
		||||
  >
 | 
			
		||||
    <div class="bg-base-100 rounded-lg shadow-xl max-w-md w-full mx-4">
 | 
			
		||||
      <div class="p-6">
 | 
			
		||||
        <h2 class="text-xl font-bold mb-4">申请实验板</h2>
 | 
			
		||||
 | 
			
		||||
        <div v-if="!loading && !hasBoard" class="space-y-4">
 | 
			
		||||
          <p class="text-base-content">
 | 
			
		||||
            检测到您尚未绑定实验板,请申请一个可用的实验板以继续实验。
 | 
			
		||||
          </p>
 | 
			
		||||
 | 
			
		||||
          <div class="flex justify-end space-x-2">
 | 
			
		||||
            <button
 | 
			
		||||
              @click="$emit('close')"
 | 
			
		||||
              class="btn btn-ghost"
 | 
			
		||||
              :disabled="requesting"
 | 
			
		||||
            >
 | 
			
		||||
              取消
 | 
			
		||||
            </button>
 | 
			
		||||
            <button
 | 
			
		||||
              @click="requestBoard"
 | 
			
		||||
              class="btn btn-primary"
 | 
			
		||||
              :disabled="requesting"
 | 
			
		||||
            >
 | 
			
		||||
              <span
 | 
			
		||||
                v-if="requesting"
 | 
			
		||||
                class="loading loading-spinner loading-sm"
 | 
			
		||||
              ></span>
 | 
			
		||||
              {{ requesting ? "申请中..." : "申请实验板" }}
 | 
			
		||||
            </button>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div v-else-if="loading" class="text-center py-8">
 | 
			
		||||
          <span class="loading loading-spinner loading-lg"></span>
 | 
			
		||||
          <p class="mt-2">检查实验板状态中...</p>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div v-else-if="hasBoard" class="space-y-4">
 | 
			
		||||
          <div class="alert alert-success">
 | 
			
		||||
            <CheckCircle class="shrink-0 h-6 w-6" />
 | 
			
		||||
            <span>实验板绑定成功!</span>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div class="bg-base-200 p-4 rounded">
 | 
			
		||||
            <h3 class="font-semibold mb-2">实验板信息</h3>
 | 
			
		||||
            <div class="space-y-1 text-sm">
 | 
			
		||||
              <p>
 | 
			
		||||
                <span class="font-medium">名称:</span>
 | 
			
		||||
                {{ boardInfo?.boardName }}
 | 
			
		||||
              </p>
 | 
			
		||||
              <p><span class="font-medium">ID:</span> {{ boardInfo?.id }}</p>
 | 
			
		||||
              <p>
 | 
			
		||||
                <span class="font-medium">地址:</span>
 | 
			
		||||
                {{ boardInfo?.ipAddr }}:{{ boardInfo?.port }}
 | 
			
		||||
              </p>
 | 
			
		||||
              <p>
 | 
			
		||||
                <span class="font-medium">状态:</span>
 | 
			
		||||
                <span class="badge badge-success">{{
 | 
			
		||||
                  boardInfo?.status === 1 ? "可用" : "忙碌"
 | 
			
		||||
                }}</span>
 | 
			
		||||
              </p>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div class="flex justify-end">
 | 
			
		||||
            <button
 | 
			
		||||
              v-if="boardInfo"
 | 
			
		||||
              @click="$emit('success', boardInfo)"
 | 
			
		||||
              class="btn btn-primary"
 | 
			
		||||
            >
 | 
			
		||||
              开始实验
 | 
			
		||||
            </button>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div v-if="error" class="alert alert-error mt-4">
 | 
			
		||||
          <XCircle class="shrink-0 h-6 w-6" />
 | 
			
		||||
          <span>{{ error }}</span>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref, watch } from "vue";
 | 
			
		||||
import { CheckCircle, XCircle } from "lucide-vue-next";
 | 
			
		||||
import { AuthManager } from "@/utils/AuthManager";
 | 
			
		||||
import type { Board } from "@/APIClient";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  open: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface Emits {
 | 
			
		||||
  (e: "close"): void;
 | 
			
		||||
  (e: "success", board: Board): void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = defineProps<Props>();
 | 
			
		||||
const emit = defineEmits<Emits>();
 | 
			
		||||
 | 
			
		||||
const loading = ref(false);
 | 
			
		||||
const requesting = ref(false);
 | 
			
		||||
const hasBoard = ref(false);
 | 
			
		||||
const boardInfo = ref<Board | null>(null);
 | 
			
		||||
const error = ref<string>("");
 | 
			
		||||
 | 
			
		||||
// 监听对话框打开状态,自动检查用户信息
 | 
			
		||||
watch(
 | 
			
		||||
  () => props.open,
 | 
			
		||||
  async (newOpen) => {
 | 
			
		||||
    if (newOpen) {
 | 
			
		||||
      await checkUserBoard();
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
// 检查用户是否已绑定实验板
 | 
			
		||||
async function checkUserBoard() {
 | 
			
		||||
  loading.value = true;
 | 
			
		||||
  error.value = "";
 | 
			
		||||
  hasBoard.value = false;
 | 
			
		||||
  boardInfo.value = null;
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    const client = AuthManager.createAuthenticatedDataClient();
 | 
			
		||||
    const userInfo = await client.getUserInfo();
 | 
			
		||||
 | 
			
		||||
    if (userInfo.boardID && userInfo.boardID.trim() !== "") {
 | 
			
		||||
      // 用户已绑定实验板,获取实验板信息
 | 
			
		||||
      try {
 | 
			
		||||
        const board = await client.getBoardByID(userInfo.boardID);
 | 
			
		||||
        boardInfo.value = board;
 | 
			
		||||
        hasBoard.value = true;
 | 
			
		||||
      } catch (boardError) {
 | 
			
		||||
        console.error("获取实验板信息失败:", boardError);
 | 
			
		||||
        error.value = "获取实验板信息失败,请重试";
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
    console.error("检查用户信息失败:", err);
 | 
			
		||||
    error.value = "检查用户信息失败,请重试";
 | 
			
		||||
  } finally {
 | 
			
		||||
    loading.value = false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 申请实验板
 | 
			
		||||
async function requestBoard() {
 | 
			
		||||
  requesting.value = true;
 | 
			
		||||
  error.value = "";
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    const client = AuthManager.createAuthenticatedDataClient();
 | 
			
		||||
    const board = await client.getAvailableBoard();
 | 
			
		||||
 | 
			
		||||
    if (board) {
 | 
			
		||||
      boardInfo.value = board;
 | 
			
		||||
      hasBoard.value = true;
 | 
			
		||||
    } else {
 | 
			
		||||
      error.value = "当前没有可用的实验板,请稍后重试";
 | 
			
		||||
    }
 | 
			
		||||
  } catch (err: any) {
 | 
			
		||||
    console.error("申请实验板失败:", err);
 | 
			
		||||
    if (err.status === 404) {
 | 
			
		||||
      error.value = "当前没有可用的实验板,请稍后重试";
 | 
			
		||||
    } else {
 | 
			
		||||
      error.value = "申请实验板失败,请重试";
 | 
			
		||||
    }
 | 
			
		||||
  } finally {
 | 
			
		||||
    requesting.value = false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
@@ -14,10 +14,9 @@
 | 
			
		||||
      </li>
 | 
			
		||||
    </ul>
 | 
			
		||||
    <div class="divider divider-horizontal h-full"></div>
 | 
			
		||||
    <div class="card bg-base-200 w-300 rounded-2xl p-7">
 | 
			
		||||
    <div class="card bg-base-300 w-300 rounded-2xl p-7">
 | 
			
		||||
      <div v-if="activePage === 1">
 | 
			
		||||
        <h2 class="card-title">用户信息</h2>
 | 
			
		||||
        <p>这里是用户信息页面的内容。</p>
 | 
			
		||||
        <UserInfo />
 | 
			
		||||
      </div>
 | 
			
		||||
      <div v-else-if="activePage === 100">
 | 
			
		||||
        <BoardTable />
 | 
			
		||||
@@ -31,6 +30,7 @@ import BoardTable from "./BoardTable.vue";
 | 
			
		||||
import { toNumber } from "lodash";
 | 
			
		||||
import { onMounted, ref } from "vue";
 | 
			
		||||
import { AuthManager } from "@/utils/AuthManager";
 | 
			
		||||
import UserInfo from "./UserInfo.vue";
 | 
			
		||||
 | 
			
		||||
const activePage = ref(1);
 | 
			
		||||
const isAdmin = ref(false);
 | 
			
		||||
@@ -40,9 +40,9 @@ function setActivePage(event: Event) {
 | 
			
		||||
  activePage.value = toNumber(target.id);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(async ()=>{
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
  isAdmin.value = await AuthManager.verifyAdminAuth();
 | 
			
		||||
})
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										590
									
								
								src/views/User/UserInfo.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										590
									
								
								src/views/User/UserInfo.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,590 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="space-y-6">
 | 
			
		||||
    <!-- 页面标题 -->
 | 
			
		||||
    <div class="flex items-center gap-3">
 | 
			
		||||
      <User class="w-8 h-8 text-primary" />
 | 
			
		||||
      <h1 class="text-3xl font-bold">用户信息</h1>
 | 
			
		||||
      <!-- 刷新按钮图标 -->
 | 
			
		||||
      <button
 | 
			
		||||
        @click="refreshAllInfo"
 | 
			
		||||
        class="btn btn-ghost btn-sm ml-auto"
 | 
			
		||||
        :disabled="loading"
 | 
			
		||||
        title="刷新信息"
 | 
			
		||||
      >
 | 
			
		||||
        <RefreshCw class="w-5 h-5" :class="{ 'animate-spin': loading }" />
 | 
			
		||||
      </button>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- 全局加载状态 -->
 | 
			
		||||
    <div v-if="loading" class="flex justify-center items-center py-12">
 | 
			
		||||
      <span class="loading loading-spinner loading-lg text-primary m-2"> </span>
 | 
			
		||||
      加载中...
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- 错误状态 -->
 | 
			
		||||
    <div v-else-if="error" class="alert alert-error">
 | 
			
		||||
      <AlertCircle class="w-5 h-5" />
 | 
			
		||||
      <span>{{ error }}</span>
 | 
			
		||||
      <button @click="refreshAllInfo" class="btn btn-sm btn-outline">
 | 
			
		||||
        <RefreshCw class="w-4 h-4" />
 | 
			
		||||
        重试
 | 
			
		||||
      </button>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- 用户信息内容 -->
 | 
			
		||||
    <div v-else class="grid grid-cols-1 lg:grid-cols-2 gap-6">
 | 
			
		||||
      <!-- 用户基本信息卡片 -->
 | 
			
		||||
      <div class="card bg-base-200 shadow-lg">
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
          <div class="flex items-center gap-3 mb-4">
 | 
			
		||||
            <UserCircle class="w-6 h-6 text-primary" />
 | 
			
		||||
            <h2 class="card-title">基本信息</h2>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div class="space-y-4">
 | 
			
		||||
            <!-- 用户ID -->
 | 
			
		||||
            <div class="flex items-center gap-3 p-3 bg-base-100 rounded-lg">
 | 
			
		||||
              <IdCard class="w-5 h-5 text-secondary" />
 | 
			
		||||
              <div class="flex-1">
 | 
			
		||||
                <div class="text-sm text-base-content/70">用户ID</div>
 | 
			
		||||
                <div class="font-mono text-sm">{{ userInfo?.id || "N/A" }}</div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <button
 | 
			
		||||
                @click="copyToClipboard(userInfo?.id)"
 | 
			
		||||
                class="btn btn-ghost btn-sm"
 | 
			
		||||
                title="复制ID"
 | 
			
		||||
              >
 | 
			
		||||
                <Copy class="w-4 h-4" />
 | 
			
		||||
              </button>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- 用户名 -->
 | 
			
		||||
            <div class="flex items-center gap-3 p-3 bg-base-100 rounded-lg">
 | 
			
		||||
              <User class="w-5 h-5 text-secondary" />
 | 
			
		||||
              <div class="flex-1">
 | 
			
		||||
                <div class="text-sm text-base-content/70">用户名</div>
 | 
			
		||||
                <div class="font-semibold">{{ userInfo?.name || "N/A" }}</div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- 邮箱 -->
 | 
			
		||||
            <div class="flex items-center gap-3 p-3 bg-base-100 rounded-lg">
 | 
			
		||||
              <Mail class="w-5 h-5 text-secondary" />
 | 
			
		||||
              <div class="flex-1">
 | 
			
		||||
                <div class="text-sm text-base-content/70">邮箱地址</div>
 | 
			
		||||
                <div class="font-mono text-sm">
 | 
			
		||||
                  {{ userInfo?.eMail || "N/A" }}
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <button
 | 
			
		||||
                @click="copyToClipboard(userInfo?.eMail)"
 | 
			
		||||
                class="btn btn-ghost btn-sm"
 | 
			
		||||
                title="复制邮箱"
 | 
			
		||||
              >
 | 
			
		||||
                <Copy class="w-4 h-4" />
 | 
			
		||||
              </button>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- 账户状态 -->
 | 
			
		||||
            <div class="flex items-center gap-3 p-3 bg-base-100 rounded-lg">
 | 
			
		||||
              <Shield class="w-5 h-5 text-secondary" />
 | 
			
		||||
              <div class="flex-1">
 | 
			
		||||
                <div class="text-sm text-base-content/70">账户状态</div>
 | 
			
		||||
                <div class="badge badge-success">已认证</div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- 绑定过期时间 -->
 | 
			
		||||
            <div
 | 
			
		||||
              v-if="userInfo?.boardExpireTime"
 | 
			
		||||
              class="flex items-center gap-3 p-3 bg-base-100 rounded-lg"
 | 
			
		||||
            >
 | 
			
		||||
              <Clock class="w-5 h-5 text-secondary" />
 | 
			
		||||
              <div class="flex-1">
 | 
			
		||||
                <div class="text-sm text-base-content/70">绑定过期时间</div>
 | 
			
		||||
                <div class="font-mono text-sm">
 | 
			
		||||
                  {{ formatExpireTime(userInfo.boardExpireTime) }}
 | 
			
		||||
                </div>
 | 
			
		||||
                <div
 | 
			
		||||
                  class="text-xs mt-1"
 | 
			
		||||
                  :class="getExpireTimeStatusClass(userInfo.boardExpireTime)"
 | 
			
		||||
                >
 | 
			
		||||
                  {{ getExpireTimeStatus(userInfo.boardExpireTime) }}
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div
 | 
			
		||||
                class="badge badge-sm"
 | 
			
		||||
                :class="getExpireTimeBadgeClass(userInfo.boardExpireTime)"
 | 
			
		||||
              >
 | 
			
		||||
                {{ getTimeRemaining(userInfo.boardExpireTime) }}
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <!-- 实验板信息卡片 -->
 | 
			
		||||
      <div class="card bg-base-200 shadow-lg">
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
          <div class="flex items-center justify-between gap-3 mb-4">
 | 
			
		||||
            <div class="flex items-center gap-3">
 | 
			
		||||
              <Cpu class="w-6 h-6 text-primary" />
 | 
			
		||||
              <h2 class="card-title">绑定实验板</h2>
 | 
			
		||||
            </div>
 | 
			
		||||
            <!-- 操作按钮 - 只有在有绑定实验板时才显示 -->
 | 
			
		||||
            <div v-if="boardInfo" class="flex items-center gap-3">
 | 
			
		||||
              <button
 | 
			
		||||
                @click="testBoardConnection"
 | 
			
		||||
                class="btn btn-secondary btn-sm"
 | 
			
		||||
                :disabled="testingConnection"
 | 
			
		||||
              >
 | 
			
		||||
                <Zap
 | 
			
		||||
                  class="w-4 h-4"
 | 
			
		||||
                  :class="{ 'animate-pulse': testingConnection }"
 | 
			
		||||
                />
 | 
			
		||||
                {{ testingConnection ? "测试中..." : "测试连接" }}
 | 
			
		||||
              </button>
 | 
			
		||||
              <button
 | 
			
		||||
                @click="unbindBoard"
 | 
			
		||||
                class="btn btn-error btn-outline btn-sm"
 | 
			
		||||
                :disabled="unbindingBoard"
 | 
			
		||||
              >
 | 
			
		||||
                <Unlink2
 | 
			
		||||
                  class="w-4 h-4"
 | 
			
		||||
                  :class="{ 'animate-pulse': unbindingBoard }"
 | 
			
		||||
                />
 | 
			
		||||
                {{ unbindingBoard ? "解绑中..." : "解绑实验板" }}
 | 
			
		||||
              </button>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <!-- 无实验板绑定 -->
 | 
			
		||||
          <div v-if="!boardInfo" class="text-center py-8">
 | 
			
		||||
            <Unlink class="w-12 h-12 text-base-content/50 mx-auto mb-4" />
 | 
			
		||||
            <div class="text-base-content/70 mb-4">暂无绑定的实验板</div>
 | 
			
		||||
            <!-- 申请实验板按钮 -->
 | 
			
		||||
            <button
 | 
			
		||||
              @click="applyBoard"
 | 
			
		||||
              class="btn btn-primary"
 | 
			
		||||
              :disabled="applyingBoard"
 | 
			
		||||
            >
 | 
			
		||||
              <Plus
 | 
			
		||||
                class="w-4 h-4"
 | 
			
		||||
                :class="{ 'animate-pulse': applyingBoard }"
 | 
			
		||||
              />
 | 
			
		||||
              {{ applyingBoard ? "申请中..." : "申请实验板" }}
 | 
			
		||||
            </button>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <!-- 实验板信息 -->
 | 
			
		||||
          <div v-else class="space-y-4">
 | 
			
		||||
            <!-- 实验板ID -->
 | 
			
		||||
            <div class="flex items-center gap-3 p-3 bg-base-100 rounded-lg">
 | 
			
		||||
              <IdCard class="w-5 h-5 text-secondary" />
 | 
			
		||||
              <div class="flex-1">
 | 
			
		||||
                <div class="text-sm text-base-content/70">实验板ID</div>
 | 
			
		||||
                <div class="font-mono text-sm">
 | 
			
		||||
                  {{ boardInfo?.id || "N/A" }}
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <button
 | 
			
		||||
                @click="copyToClipboard(boardInfo?.id)"
 | 
			
		||||
                class="btn btn-ghost btn-sm"
 | 
			
		||||
                title="复制ID"
 | 
			
		||||
              >
 | 
			
		||||
                <Copy class="w-4 h-4" />
 | 
			
		||||
              </button>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- 实验板名称 -->
 | 
			
		||||
            <div class="flex items-center gap-3 p-3 bg-base-100 rounded-lg">
 | 
			
		||||
              <Tag class="w-5 h-5 text-secondary" />
 | 
			
		||||
              <div class="flex-1">
 | 
			
		||||
                <div class="text-sm text-base-content/70">实验板名称</div>
 | 
			
		||||
                <div class="font-semibold">
 | 
			
		||||
                  {{ boardInfo?.boardName || "N/A" }}
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- IP地址 -->
 | 
			
		||||
            <div class="flex items-center gap-3 p-3 bg-base-100 rounded-lg">
 | 
			
		||||
              <Globe class="w-5 h-5 text-secondary" />
 | 
			
		||||
              <div class="flex-1">
 | 
			
		||||
                <div class="text-sm text-base-content/70">IP地址</div>
 | 
			
		||||
                <div class="font-mono text-sm">
 | 
			
		||||
                  {{ boardInfo?.ipAddr || "N/A" }}
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <button
 | 
			
		||||
                @click="copyToClipboard(boardInfo?.ipAddr)"
 | 
			
		||||
                class="btn btn-ghost btn-sm"
 | 
			
		||||
                title="复制IP地址"
 | 
			
		||||
              >
 | 
			
		||||
                <Copy class="w-4 h-4" />
 | 
			
		||||
              </button>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- 端口 -->
 | 
			
		||||
            <div class="flex items-center gap-3 p-3 bg-base-100 rounded-lg">
 | 
			
		||||
              <Server class="w-5 h-5 text-secondary" />
 | 
			
		||||
              <div class="flex-1">
 | 
			
		||||
                <div class="text-sm text-base-content/70">端口</div>
 | 
			
		||||
                <div class="font-mono text-sm">
 | 
			
		||||
                  {{ boardInfo?.port || "N/A" }}
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- 状态 -->
 | 
			
		||||
            <div class="flex items-center gap-3 p-3 bg-base-100 rounded-lg">
 | 
			
		||||
              <Activity class="w-5 h-5 text-secondary" />
 | 
			
		||||
              <div class="flex-1">
 | 
			
		||||
                <div class="text-sm text-base-content/70">状态</div>
 | 
			
		||||
                <div
 | 
			
		||||
                  class="badge"
 | 
			
		||||
                  :class="getBoardStatusClass(boardInfo?.status)"
 | 
			
		||||
                >
 | 
			
		||||
                  {{ getBoardStatusText(boardInfo?.status) }}
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- 固件版本 -->
 | 
			
		||||
            <div class="flex items-center gap-3 p-3 bg-base-100 rounded-lg">
 | 
			
		||||
              <Settings class="w-5 h-5 text-secondary" />
 | 
			
		||||
              <div class="flex-1">
 | 
			
		||||
                <div class="text-sm text-base-content/70">固件版本</div>
 | 
			
		||||
                <div class="font-mono text-sm">
 | 
			
		||||
                  {{ boardInfo?.firmVersion || "N/A" }}
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- 使用自定义 Alert 组件 -->
 | 
			
		||||
    <Alert />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref, onMounted } from "vue";
 | 
			
		||||
import { AuthManager } from "@/utils/AuthManager";
 | 
			
		||||
import { UserInfo, Board, BoardStatus } from "@/APIClient";
 | 
			
		||||
import { Alert, useAlertStore } from "@/components/Alert";
 | 
			
		||||
import {
 | 
			
		||||
  User,
 | 
			
		||||
  UserCircle,
 | 
			
		||||
  Mail,
 | 
			
		||||
  IdCard,
 | 
			
		||||
  Copy,
 | 
			
		||||
  Shield,
 | 
			
		||||
  Cpu,
 | 
			
		||||
  Globe,
 | 
			
		||||
  Server,
 | 
			
		||||
  Activity,
 | 
			
		||||
  Settings,
 | 
			
		||||
  Tag,
 | 
			
		||||
  RefreshCw,
 | 
			
		||||
  AlertCircle,
 | 
			
		||||
  Unlink,
 | 
			
		||||
  Unlink2,
 | 
			
		||||
  Zap,
 | 
			
		||||
  Plus,
 | 
			
		||||
  Clock,
 | 
			
		||||
} from "lucide-vue-next";
 | 
			
		||||
 | 
			
		||||
// 响应式数据
 | 
			
		||||
const loading = ref(false);
 | 
			
		||||
const error = ref("");
 | 
			
		||||
const userInfo = ref<UserInfo | null>(null);
 | 
			
		||||
const boardInfo = ref<Board | null>(null);
 | 
			
		||||
 | 
			
		||||
// 操作状态
 | 
			
		||||
const testingConnection = ref(false);
 | 
			
		||||
const unbindingBoard = ref(false);
 | 
			
		||||
const applyingBoard = ref(false);
 | 
			
		||||
 | 
			
		||||
// 使用自定义 Alert
 | 
			
		||||
const alertStore = useAlertStore();
 | 
			
		||||
 | 
			
		||||
// 加载实验板信息
 | 
			
		||||
const loadBoardInfo = async () => {
 | 
			
		||||
  if (!userInfo.value?.boardID) {
 | 
			
		||||
    boardInfo.value = null;
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    const client = AuthManager.createAuthenticatedDataClient();
 | 
			
		||||
    boardInfo.value = await client.getBoardByID(userInfo.value.boardID);
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
    console.error("加载实验板信息失败:", err);
 | 
			
		||||
    boardInfo.value = null;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 统一的信息加载函数(合并了原来的 loadUserInfo 和 refreshAllInfo)
 | 
			
		||||
const loadUserInfo = async (showSuccessMessage = false) => {
 | 
			
		||||
  loading.value = true;
 | 
			
		||||
  error.value = "";
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    await new Promise((resolve) => setTimeout(resolve, 200));
 | 
			
		||||
 | 
			
		||||
    const client = AuthManager.createAuthenticatedDataClient();
 | 
			
		||||
    userInfo.value = await client.getUserInfo();
 | 
			
		||||
 | 
			
		||||
    // 如果有绑定的实验板ID,加载实验板信息
 | 
			
		||||
    if (userInfo.value?.boardID) {
 | 
			
		||||
      await loadBoardInfo();
 | 
			
		||||
    } else {
 | 
			
		||||
      boardInfo.value = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (showSuccessMessage) {
 | 
			
		||||
      alertStore?.success("信息刷新成功");
 | 
			
		||||
    }
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
    console.error("加载用户信息失败:", err);
 | 
			
		||||
    error.value = "加载用户信息失败,请检查网络连接或重新登录";
 | 
			
		||||
    if (showSuccessMessage) {
 | 
			
		||||
      alertStore?.error("刷新信息失败,请检查网络连接");
 | 
			
		||||
    }
 | 
			
		||||
  } finally {
 | 
			
		||||
    loading.value = false;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 刷新所有信息(调用统一的加载函数,显示成功消息)
 | 
			
		||||
const refreshAllInfo = async () => {
 | 
			
		||||
  await loadUserInfo(true);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 申请实验板
 | 
			
		||||
const applyBoard = async () => {
 | 
			
		||||
  applyingBoard.value = true;
 | 
			
		||||
  alertStore?.info("正在申请实验板...");
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    const client = AuthManager.createAuthenticatedDataClient();
 | 
			
		||||
 | 
			
		||||
    // 获取可用的实验板
 | 
			
		||||
    const availableBoard = await client.getAvailableBoard(undefined);
 | 
			
		||||
 | 
			
		||||
    if (availableBoard) {
 | 
			
		||||
      alertStore?.success(`成功申请到实验板: ${availableBoard.boardName}`);
 | 
			
		||||
 | 
			
		||||
      // 重新加载用户信息以获取最新的绑定状态
 | 
			
		||||
      await loadUserInfo();
 | 
			
		||||
    } else {
 | 
			
		||||
      alertStore?.warn("当前没有可用的实验板,请稍后再试");
 | 
			
		||||
    }
 | 
			
		||||
  } catch (err: any) {
 | 
			
		||||
    console.error("申请实验板失败:", err);
 | 
			
		||||
 | 
			
		||||
    // 根据错误状态码提供更友好的错误信息
 | 
			
		||||
    if (err?.status === 404) {
 | 
			
		||||
      alertStore?.warn("当前没有可用的实验板,请稍后再试");
 | 
			
		||||
    } else if (err?.status === 400) {
 | 
			
		||||
      alertStore?.error("您已经绑定了实验板,无需重复申请");
 | 
			
		||||
    } else {
 | 
			
		||||
      alertStore?.error("申请实验板失败,请检查网络连接或稍后重试");
 | 
			
		||||
    }
 | 
			
		||||
  } finally {
 | 
			
		||||
    applyingBoard.value = false;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 测试实验板连接
 | 
			
		||||
const testBoardConnection = async () => {
 | 
			
		||||
  if (!boardInfo.value) return;
 | 
			
		||||
 | 
			
		||||
  testingConnection.value = true;
 | 
			
		||||
  alertStore?.info("正在测试连接...");
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    const jtagClient = AuthManager.createAuthenticatedJtagClient();
 | 
			
		||||
 | 
			
		||||
    // 使用JTAG客户端读取设备ID Code
 | 
			
		||||
    const idCode = await jtagClient.getDeviceIDCode(
 | 
			
		||||
      boardInfo.value.ipAddr,
 | 
			
		||||
      boardInfo.value.port,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // 检查ID Code是否有效(非0xFFFFFFFF表示连接成功)
 | 
			
		||||
    if (idCode !== 0xffffffff && idCode !== 0) {
 | 
			
		||||
      alertStore?.success(
 | 
			
		||||
        `连接测试成功,设备ID: 0x${idCode.toString(16).toUpperCase()}`,
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      alertStore?.warn("连接测试失败,未检测到有效设备");
 | 
			
		||||
    }
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
    console.error("连接测试失败:", err);
 | 
			
		||||
    alertStore?.error("连接测试失败,请检查实验板是否在线");
 | 
			
		||||
  } finally {
 | 
			
		||||
    testingConnection.value = false;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 解绑实验板
 | 
			
		||||
const unbindBoard = async () => {
 | 
			
		||||
  if (!boardInfo.value) return;
 | 
			
		||||
 | 
			
		||||
  // 确认对话框
 | 
			
		||||
  if (!confirm("确定要解绑当前实验板吗?解绑后需要重新绑定才能使用。")) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  unbindingBoard.value = true;
 | 
			
		||||
  alertStore?.info("正在解绑实验板...");
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    const client = AuthManager.createAuthenticatedDataClient();
 | 
			
		||||
    const success = await client.unbindBoard();
 | 
			
		||||
 | 
			
		||||
    if (success) {
 | 
			
		||||
      alertStore?.success("实验板解绑成功");
 | 
			
		||||
      // 清空实验板信息并重新加载用户信息
 | 
			
		||||
      boardInfo.value = null;
 | 
			
		||||
      await loadUserInfo();
 | 
			
		||||
    } else {
 | 
			
		||||
      alertStore?.error("实验板解绑失败");
 | 
			
		||||
    }
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
    console.error("解绑实验板失败:", err);
 | 
			
		||||
    alertStore?.error("解绑实验板失败,请稍后重试");
 | 
			
		||||
  } finally {
 | 
			
		||||
    unbindingBoard.value = false;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 复制到剪贴板
 | 
			
		||||
const copyToClipboard = async (text?: string) => {
 | 
			
		||||
  if (!text) return;
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    await navigator.clipboard.writeText(text);
 | 
			
		||||
    alertStore?.success("已复制到剪贴板");
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
    alertStore?.error("复制失败");
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 时间相关的工具函数
 | 
			
		||||
const formatExpireTime = (expireTime: Date) => {
 | 
			
		||||
  return new Date(expireTime).toLocaleString("zh-CN", {
 | 
			
		||||
    year: "numeric",
 | 
			
		||||
    month: "2-digit",
 | 
			
		||||
    day: "2-digit",
 | 
			
		||||
    hour: "2-digit",
 | 
			
		||||
    minute: "2-digit",
 | 
			
		||||
    second: "2-digit",
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getTimeRemaining = (expireTime: Date) => {
 | 
			
		||||
  const now = new Date();
 | 
			
		||||
  const expire = new Date(expireTime);
 | 
			
		||||
  const timeDiff = expire.getTime() - now.getTime();
 | 
			
		||||
 | 
			
		||||
  if (timeDiff <= 0) {
 | 
			
		||||
    return "已过期";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const hours = Math.floor(timeDiff / (1000 * 60 * 60));
 | 
			
		||||
  const minutes = Math.floor((timeDiff % (1000 * 60 * 60)) / (1000 * 60));
 | 
			
		||||
 | 
			
		||||
  if (hours > 0) {
 | 
			
		||||
    return `剩余 ${hours}小时${minutes}分钟`;
 | 
			
		||||
  } else {
 | 
			
		||||
    return `剩余 ${minutes}分钟`;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 获取过期时间相关状态的统一函数
 | 
			
		||||
const getExpireTimeInfo = (expireTime: Date) => {
 | 
			
		||||
  const now = new Date();
 | 
			
		||||
  const expire = new Date(expireTime);
 | 
			
		||||
  const timeDiff = expire.getTime() - now.getTime();
 | 
			
		||||
 | 
			
		||||
  if (timeDiff <= 0) {
 | 
			
		||||
    return {
 | 
			
		||||
      status: "已过期",
 | 
			
		||||
      statusClass: "text-error",
 | 
			
		||||
      badgeClass: "badge-error",
 | 
			
		||||
    };
 | 
			
		||||
  } else if (timeDiff <= 30 * 60 * 1000) {
 | 
			
		||||
    return {
 | 
			
		||||
      status: "即将过期",
 | 
			
		||||
      statusClass: "text-warning",
 | 
			
		||||
      badgeClass: "badge-warning",
 | 
			
		||||
    };
 | 
			
		||||
  } else if (timeDiff <= 60 * 60 * 1000) {
 | 
			
		||||
    return {
 | 
			
		||||
      status: "临近过期",
 | 
			
		||||
      statusClass: "text-warning",
 | 
			
		||||
      badgeClass: "badge-warning",
 | 
			
		||||
    };
 | 
			
		||||
  } else {
 | 
			
		||||
    return {
 | 
			
		||||
      status: "正常",
 | 
			
		||||
      statusClass: "text-success",
 | 
			
		||||
      badgeClass: "badge-success",
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 使用统一函数的便捷方法
 | 
			
		||||
const getExpireTimeStatus = (expireTime: Date) =>
 | 
			
		||||
  getExpireTimeInfo(expireTime).status;
 | 
			
		||||
const getExpireTimeStatusClass = (expireTime: Date) =>
 | 
			
		||||
  getExpireTimeInfo(expireTime).statusClass;
 | 
			
		||||
const getExpireTimeBadgeClass = (expireTime: Date) =>
 | 
			
		||||
  getExpireTimeInfo(expireTime).badgeClass;
 | 
			
		||||
 | 
			
		||||
// 获取实验板状态相关信息的统一函数
 | 
			
		||||
const getBoardStatusInfo = (status?: BoardStatus) => {
 | 
			
		||||
  switch (status) {
 | 
			
		||||
    case BoardStatus.Available:
 | 
			
		||||
      return { text: "可用", class: "badge-success" };
 | 
			
		||||
    case BoardStatus.Busy:
 | 
			
		||||
      return { text: "使用中", class: "badge-warning" };
 | 
			
		||||
    default:
 | 
			
		||||
      return { text: "未知", class: "badge-neutral" };
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 使用统一函数的便捷方法
 | 
			
		||||
const getBoardStatusClass = (status?: BoardStatus) =>
 | 
			
		||||
  getBoardStatusInfo(status).class;
 | 
			
		||||
const getBoardStatusText = (status?: BoardStatus) =>
 | 
			
		||||
  getBoardStatusInfo(status).text;
 | 
			
		||||
 | 
			
		||||
// 组件挂载时加载数据
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  loadUserInfo();
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
/* 添加一些自定义样式优化 */
 | 
			
		||||
.card {
 | 
			
		||||
  transition: all 0.2s ease;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card:hover {
 | 
			
		||||
  transform: translateY(-2px);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 响应式优化 */
 | 
			
		||||
@media (max-width: 768px) {
 | 
			
		||||
  .grid-cols-1.lg\:grid-cols-2 {
 | 
			
		||||
    grid-template-columns: 1fr;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
		Reference in New Issue
	
	Block a user