feat: 完善用户界面,添加绑定与解除绑定的功能

This commit is contained in:
SikongJueluo 2025-07-12 17:46:23 +08:00
parent 0fb0c4e395
commit f253a33c83
No known key found for this signature in database
11 changed files with 1654 additions and 185 deletions

View File

@ -41,6 +41,11 @@ public class DataController : ControllerBase
/// 用户关联的板卡ID /// 用户关联的板卡ID
/// </summary> /// </summary>
public Guid BoardID { get; set; } public Guid BoardID { get; set; }
/// <summary>
/// 用户绑定板子的过期时间
/// </summary>
public DateTime? BoardExpireTime { get; set; }
} }
/// <summary> /// <summary>
@ -148,6 +153,7 @@ public class DataController : ControllerBase
Name = user.Name, Name = user.Name,
EMail = user.EMail, EMail = user.EMail,
BoardID = user.BoardID, BoardID = user.BoardID,
BoardExpireTime = user.BoardExpireTime,
}); });
} }
@ -194,32 +200,32 @@ public class DataController : ControllerBase
/// <summary> /// <summary>
/// 获取一个空闲的实验板(普通用户权限) /// 获取一个空闲的实验板(普通用户权限)
/// </summary> /// </summary>
/// <param name="durationHours">绑定持续时间小时默认为1小时</param>
[Authorize] [Authorize]
[HttpGet("GetAvailableBoard")] [HttpGet("GetAvailableBoard")]
[EnableCors("Users")] [EnableCors("Users")]
[ProducesResponseType(typeof(Database.Board), StatusCodes.Status200OK)] [ProducesResponseType(typeof(Database.Board), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)] [ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult GetAvailableBoard() public IActionResult GetAvailableBoard(int durationHours = 1)
{ {
try try
{ {
using var db = new Database.AppDataConnection();
var boardOpt = db.GetAvailableBoard();
if (!boardOpt.HasValue)
return NotFound("没有可用的实验板");
// 绑定用户与实验板
var userName = User.Identity?.Name; var userName = User.Identity?.Name;
if (string.IsNullOrEmpty(userName)) if (string.IsNullOrEmpty(userName))
return Unauthorized("未找到用户名信息"); return Unauthorized("未找到用户名信息");
using var db = new Database.AppDataConnection();
var userRet = db.GetUserByName(userName); var userRet = db.GetUserByName(userName);
if (!userRet.IsSuccessful || !userRet.Value.HasValue) if (!userRet.IsSuccessful || !userRet.Value.HasValue)
return BadRequest("用户不存在"); return BadRequest("用户不存在");
var user = userRet.Value.Value; 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); 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>
/// 新增板子(管理员权限) /// 新增板子(管理员权限)
/// </summary> /// </summary>

View File

@ -1,13 +1,15 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace server.Controllers; namespace server.Controllers;
/// <summary> /// <summary>
/// Jtag API /// JTAG 控制器 - 提供 JTAG 相关的 API 操作
/// </summary> /// </summary>
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/[controller]")]
[Authorize] // 添加用户认证要求
public class JtagController : ControllerBase public class JtagController : ControllerBase
{ {
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
@ -15,49 +17,72 @@ public class JtagController : ControllerBase
private const string BITSTREAM_PATH = "bitstream/Jtag"; private const string BITSTREAM_PATH = "bitstream/Jtag";
/// <summary> /// <summary>
/// 页面 /// 控制器首页信息
/// </summary> /// </summary>
/// <returns>控制器描述信息</returns>
[HttpGet] [HttpGet]
[EnableCors("Users")]
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
public string Index() public string Index()
{ {
logger.Info($"User {User.Identity?.Name} accessed Jtag controller index");
return "This is Jtag Controller"; return "This is Jtag Controller";
} }
/// <summary> /// <summary>
/// 获取Jtag ID Code /// 获取 JTAG 设备的 ID Code
/// </summary> /// </summary>
/// <param name="address"> 设备地址 </param> /// <param name="address">JTAG 设备地址</param>
/// <param name="port"> 设备端口 </param> /// <param name="port">JTAG 设备端口</param>
/// <returns>设备的 ID Code</returns>
[HttpGet("GetDeviceIDCode")] [HttpGet("GetDeviceIDCode")]
[EnableCors("Users")] [EnableCors("Users")]
[ProducesResponseType(typeof(uint), StatusCodes.Status200OK)] [ProducesResponseType(typeof(uint), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async ValueTask<IResult> GetDeviceIDCode(string address, int port) public async ValueTask<IResult> GetDeviceIDCode(string address, int port)
{
logger.Info($"User {User.Identity?.Name} requesting device ID code from {address}:{port}");
try
{ {
var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port); var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
var ret = await jtagCtrl.ReadIDCode(); var ret = await jtagCtrl.ReadIDCode();
if (ret.IsSuccessful) if (ret.IsSuccessful)
{ {
logger.Info($"Get device {address} ID code: 0x{ret.Value:X4}"); logger.Info($"User {User.Identity?.Name} successfully got device {address} ID code: 0x{ret.Value:X8}");
return TypedResults.Ok(ret.Value); return TypedResults.Ok(ret.Value);
} }
else else
{ {
logger.Error(ret.Error); logger.Error($"User {User.Identity?.Name} failed to get device {address} ID code: {ret.Error}");
return TypedResults.InternalServerError(ret.Error); return TypedResults.InternalServerError(ret.Error);
} }
} }
catch (Exception ex)
{
logger.Error(ex, $"User {User.Identity?.Name} encountered exception while getting device {address} ID code");
return TypedResults.InternalServerError(ex);
}
}
/// <summary> /// <summary>
/// 获取状态寄存器 /// 读取 JTAG 设备的状态寄存器
/// </summary> /// </summary>
/// <param name="address"> 设备地址 </param> /// <param name="address">JTAG 设备地址</param>
/// <param name="port"> 设备端口 </param> /// <param name="port">JTAG 设备端口</param>
/// <returns>状态寄存器的原始值、二进制表示和解码值</returns>
[HttpGet("ReadStatusReg")] [HttpGet("ReadStatusReg")]
[EnableCors("Users")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)] [ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async ValueTask<IResult> ReadStatusReg(string address, int port) public async ValueTask<IResult> ReadStatusReg(string address, int port)
{
logger.Info($"User {User.Identity?.Name} requesting status register from {address}:{port}");
try
{ {
var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port); var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
var ret = await jtagCtrl.ReadStatusReg(); var ret = await jtagCtrl.ReadStatusReg();
@ -66,7 +91,7 @@ public class JtagController : ControllerBase
{ {
var binaryValue = Common.String.Reverse(Convert.ToString(ret.Value, 2).PadLeft(32, '0')); var binaryValue = Common.String.Reverse(Convert.ToString(ret.Value, 2).PadLeft(32, '0'));
var decodeValue = new Peripherals.JtagClient.JtagStatusReg(ret.Value); var decodeValue = new Peripherals.JtagClient.JtagStatusReg(ret.Value);
logger.Info($"Read device {address} Status Register: \n\t 0b{binaryValue} \n\t {decodeValue}"); logger.Info($"User {User.Identity?.Name} successfully read device {address} Status Register: \n\t 0b{binaryValue} \n\t {decodeValue}");
return TypedResults.Ok(new return TypedResults.Ok(new
{ {
original = ret.Value, original = ret.Value,
@ -76,25 +101,41 @@ public class JtagController : ControllerBase
} }
else else
{ {
logger.Error(ret.Error); logger.Error($"User {User.Identity?.Name} failed to read device {address} status register: {ret.Error}");
return TypedResults.InternalServerError(ret.Error); return TypedResults.InternalServerError(ret.Error);
} }
} }
catch (Exception ex)
{
logger.Error(ex, $"User {User.Identity?.Name} encountered exception while reading device {address} status register");
return TypedResults.InternalServerError(ex);
}
}
/// <summary> /// <summary>
/// 上传比特流文件 /// 上传比特流文件到服务器
/// </summary> /// </summary>
/// <param name="address"> 设备地址 </param> /// <param name="address">目标设备地址</param>
/// <param name="file">比特流文件</param> /// <param name="file">比特流文件</param>
/// <returns>上传结果</returns>
[HttpPost("UploadBitstream")] [HttpPost("UploadBitstream")]
[EnableCors("Users")] [EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)] [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> UploadBitstream(string address, IFormFile file) public async ValueTask<IResult> UploadBitstream(string address, IFormFile file)
{ {
if (file == null || file.Length == 0) logger.Info($"User {User.Identity?.Name} uploading bitstream for device {address}");
return TypedResults.BadRequest("未选择文件");
if (file == null || file.Length == 0)
{
logger.Warn($"User {User.Identity?.Name} attempted to upload empty file for device {address}");
return TypedResults.BadRequest("未选择文件");
}
try
{
// 生成安全的文件名(避免路径遍历攻击) // 生成安全的文件名(避免路径遍历攻击)
var fileName = Path.GetRandomFileName(); var fileName = Path.GetRandomFileName();
var uploadsFolder = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}"); var uploadsFolder = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
@ -103,6 +144,7 @@ public class JtagController : ControllerBase
if (Directory.Exists(uploadsFolder)) if (Directory.Exists(uploadsFolder))
{ {
Directory.Delete(uploadsFolder, true); Directory.Delete(uploadsFolder, true);
logger.Info($"User {User.Identity?.Name} removed existing bitstream folder for device {address}");
} }
Directory.CreateDirectory(uploadsFolder); Directory.CreateDirectory(uploadsFolder);
@ -113,36 +155,55 @@ public class JtagController : ControllerBase
await file.CopyToAsync(stream); await file.CopyToAsync(stream);
} }
logger.Info($"Device {address} Upload Bitstream Successfully"); logger.Info($"User {User.Identity?.Name} successfully uploaded bitstream for device {address}, file size: {file.Length} bytes");
return TypedResults.Ok(true); 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> /// <summary>
/// 通过Jtag下载比特流文件 /// 通过 JTAG 下载比特流文件到 FPGA 设备
/// </summary> /// </summary>
/// <param name="address"> 设备地址 </param> /// <param name="address">JTAG 设备地址</param>
/// <param name="port"> 设备端口 </param> /// <param name="port">JTAG 设备端口</param>
/// <returns>下载结果</returns>
[HttpPost("DownloadBitstream")] [HttpPost("DownloadBitstream")]
[EnableCors("Users")] [EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)] [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async ValueTask<IResult> DownloadBitstream(string address, int port) 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}"); var fileDir = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
if (!Directory.Exists(fileDir)) 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"); return TypedResults.BadRequest("Empty bitstream, Please upload it first");
}
try try
{ {
// 读取文件 // 读取文件
var filePath = Directory.GetFiles(fileDir)[0]; 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)) using (var fileStream = System.IO.File.Open(filePath, System.IO.FileMode.Open))
{ {
if (fileStream is null || fileStream.Length <= 0) 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"); return TypedResults.BadRequest("Wrong bitstream, Please upload it again");
}
logger.Info($"User {User.Identity?.Name} processing bitstream file of size: {fileStream.Length} bytes");
// 定义缓冲区大小: 32KB // 定义缓冲区大小: 32KB
byte[] buffer = new byte[32 * 1024]; byte[] buffer = new byte[32 * 1024];
@ -158,7 +219,10 @@ public class JtagController : ControllerBase
// 反转 32bits // 反转 32bits
var retBuffer = Common.Number.ReverseBytes(buffer, 4); var retBuffer = Common.Number.ReverseBytes(buffer, 4);
if (!retBuffer.IsSuccessful) if (!retBuffer.IsSuccessful)
{
logger.Error($"User {User.Identity?.Name} failed to reverse bytes: {retBuffer.Error}");
return TypedResults.InternalServerError(retBuffer.Error); return TypedResults.InternalServerError(retBuffer.Error);
}
revBuffer = retBuffer.Value; revBuffer = retBuffer.Value;
for (int i = 0; i < revBuffer.Length; i++) for (int i = 0; i < revBuffer.Length; i++)
@ -172,6 +236,7 @@ public class JtagController : ControllerBase
// 将所有数据转换为字节数组(注意:如果文件非常大,可能不适合完全加载到内存) // 将所有数据转换为字节数组(注意:如果文件非常大,可能不适合完全加载到内存)
var fileBytes = memoryStream.ToArray(); var fileBytes = memoryStream.ToArray();
logger.Info($"User {User.Identity?.Name} processed {totalBytesRead} bytes for device {address}");
// 下载比特流 // 下载比特流
var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port); var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
@ -179,100 +244,140 @@ public class JtagController : ControllerBase
if (ret.IsSuccessful) 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); return TypedResults.Ok(ret.Value);
} }
else 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); return TypedResults.InternalServerError(ret.Error);
} }
} }
} }
} }
catch (Exception error) catch (Exception ex)
{ {
return TypedResults.InternalServerError(error); logger.Error(ex, $"User {User.Identity?.Name} encountered exception while downloading bitstream to device {address}");
} return TypedResults.InternalServerError(ex);
finally
{
} }
} }
/// <summary> /// <summary>
/// [TODO:description] /// 执行边界扫描,获取所有端口状态
/// </summary> /// </summary>
/// <param name="address">[TODO:parameter]</param> /// <param name="address">JTAG 设备地址</param>
/// <param name="port">[TODO:parameter]</param> /// <param name="port">JTAG 设备端口</param>
/// <returns>[TODO:return]</returns> /// <returns>边界扫描结果</returns>
[HttpPost("BoundaryScanAllPorts")] [HttpPost("BoundaryScanAllPorts")]
[EnableCors("Users")] [EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)] [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async ValueTask<IResult> BoundaryScanAllPorts(string address, int port) public async ValueTask<IResult> BoundaryScanAllPorts(string address, int port)
{
logger.Info($"User {User.Identity?.Name} initiating boundary scan for all ports on device {address}:{port}");
try
{ {
var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port); var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
var ret = await jtagCtrl.BoundaryScan(); var ret = await jtagCtrl.BoundaryScan();
if (!ret.IsSuccessful) if (!ret.IsSuccessful)
{ {
logger.Error($"User {User.Identity?.Name} boundary scan failed for device {address}: {ret.Error}");
if (ret.Error is ArgumentException) if (ret.Error is ArgumentException)
return TypedResults.BadRequest(ret.Error); return TypedResults.BadRequest(ret.Error);
else return TypedResults.InternalServerError(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); 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> /// <summary>
/// [TODO:description] /// 执行逻辑端口边界扫描
/// </summary> /// </summary>
/// <param name="address">[TODO:parameter]</param> /// <param name="address">JTAG 设备地址</param>
/// <param name="port">[TODO:parameter]</param> /// <param name="port">JTAG 设备端口</param>
/// <returns>[TODO:return]</returns> /// <returns>逻辑端口状态字典</returns>
[HttpPost("BoundaryScanLogicalPorts")] [HttpPost("BoundaryScanLogicalPorts")]
[EnableCors("Users")] [EnableCors("Users")]
[ProducesResponseType(typeof(Dictionary<string, bool>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(Dictionary<string, bool>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async ValueTask<IResult> BoundaryScanLogicalPorts(string address, int port) public async ValueTask<IResult> BoundaryScanLogicalPorts(string address, int port)
{
logger.Info($"User {User.Identity?.Name} initiating logical ports boundary scan on device {address}:{port}");
try
{ {
var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port); var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
var ret = await jtagCtrl.BoundaryScanLogicalPorts(); var ret = await jtagCtrl.BoundaryScanLogicalPorts();
if (!ret.IsSuccessful) if (!ret.IsSuccessful)
{ {
logger.Error($"User {User.Identity?.Name} logical ports boundary scan failed for device {address}: {ret.Error}");
if (ret.Error is ArgumentException) if (ret.Error is ArgumentException)
return TypedResults.BadRequest(ret.Error); return TypedResults.BadRequest(ret.Error);
else return TypedResults.InternalServerError(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); 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> /// <summary>
/// [TODO:description] /// 设置 JTAG 时钟速度
/// </summary> /// </summary>
/// <param name="address">[TODO:parameter]</param> /// <param name="address">JTAG 设备地址</param>
/// <param name="port">[TODO:parameter]</param> /// <param name="port">JTAG 设备端口</param>
/// <param name="speed">[TODO:parameter]</param> /// <param name="speed">时钟速度 (Hz)</param>
/// <returns>[TODO:return]</returns> /// <returns>设置结果</returns>
[HttpPost("SetSpeed")] [HttpPost("SetSpeed")]
[EnableCors("Users")] [EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)] [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async ValueTask<IResult> SetSpeed(string address, int port, UInt32 speed) public async ValueTask<IResult> SetSpeed(string address, int port, UInt32 speed)
{
logger.Info($"User {User.Identity?.Name} setting JTAG speed to {speed} Hz for device {address}:{port}");
try
{ {
var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port); var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
var ret = await jtagCtrl.SetSpeed(speed); var ret = await jtagCtrl.SetSpeed(speed);
if (!ret.IsSuccessful) if (!ret.IsSuccessful)
{ {
logger.Error($"User {User.Identity?.Name} failed to set speed for device {address}: {ret.Error}");
if (ret.Error is ArgumentException) if (ret.Error is ArgumentException)
return TypedResults.BadRequest(ret.Error); return TypedResults.BadRequest(ret.Error);
else return TypedResults.InternalServerError(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); 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);
}
}
} }

View File

@ -46,6 +46,12 @@ public class User
[Nullable] [Nullable]
public Guid BoardID { get; set; } public Guid BoardID { get; set; }
/// <summary>
/// 用户绑定板子的过期时间
/// </summary>
[Nullable]
public DateTime? BoardExpireTime { get; set; }
/// <summary> /// <summary>
/// 用户权限枚举 /// 用户权限枚举
/// </summary> /// </summary>
@ -98,6 +104,18 @@ public class Board
[NotNull] [NotNull]
public required BoardStatus Status { get; set; } public required BoardStatus Status { get; set; }
/// <summary>
/// 占用该板子的用户的唯一标识符
/// </summary>
[Nullable]
public Guid OccupiedUserID { get; set; }
/// <summary>
/// 占用该板子的用户的用户名
/// </summary>
[Nullable]
public string? OccupiedUserName { get; set; }
/// <summary> /// <summary>
/// FPGA 板子的固件版本号 /// FPGA 板子的固件版本号
/// </summary> /// </summary>
@ -210,7 +228,7 @@ public class AppDataConnection : DataConnection
/// <returns>包含用户信息的结果,如果未找到或出错则返回相应状态</returns> /// <returns>包含用户信息的结果,如果未找到或出错则返回相应状态</returns>
public Result<Optional<User>> GetUserByName(string name) 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) if (user.Length > 1)
{ {
@ -235,7 +253,7 @@ public class AppDataConnection : DataConnection
/// <returns>包含用户信息的结果,如果未找到或出错则返回相应状态</returns> /// <returns>包含用户信息的结果,如果未找到或出错则返回相应状态</returns>
public Result<Optional<User>> GetUserByEMail(string email) 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) if (user.Length > 1)
{ {
@ -287,15 +305,70 @@ public class AppDataConnection : DataConnection
/// </summary> /// </summary>
/// <param name="userId">用户的唯一标识符</param> /// <param name="userId">用户的唯一标识符</param>
/// <param name="boardId">实验板的唯一标识符</param> /// <param name="boardId">实验板的唯一标识符</param>
/// <param name="expireTime">绑定过期时间</param>
/// <returns>更新的记录数</returns> /// <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) .Where(u => u.ID == userId)
.Set(u => u.BoardID, boardId) .Set(u => u.BoardID, boardId)
.Set(u => u.BoardExpireTime, expireTime)
.Update(); .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> /// <summary>
@ -326,7 +399,26 @@ public class AppDataConnection : DataConnection
/// <returns>删除的记录数</returns> /// <returns>删除的记录数</returns>
public int DeleteBoardByName(string name) 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}"); logger.Info($"实验板已删除: {name},删除记录数: {result}");
return result; return result;
} }
@ -338,18 +430,62 @@ public class AppDataConnection : DataConnection
/// <returns>删除的记录数</returns> /// <returns>删除的记录数</returns>
public int DeleteBoardByID(Guid id) 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}"); logger.Info($"实验板已删除: {id},删除记录数: {result}");
return 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>
/// 获取所有实验板信息 /// 获取所有实验板信息
/// </summary> /// </summary>
/// <returns>所有实验板的数组</returns> /// <returns>所有实验板的数组</returns>
public Board[] GetAllBoard() public Board[] GetAllBoard()
{ {
var boards = this.Board.ToArray(); var boards = this.BoardTable.ToArray();
logger.Debug($"获取所有实验板,共 {boards.Length} 块"); logger.Debug($"获取所有实验板,共 {boards.Length} 块");
return boards; return boards;
} }
@ -357,10 +493,12 @@ public class AppDataConnection : DataConnection
/// <summary> /// <summary>
/// 获取一块可用的实验板并将其状态设置为繁忙 /// 获取一块可用的实验板并将其状态设置为繁忙
/// </summary> /// </summary>
/// <param name="userId">要分配板子的用户ID</param>
/// <param name="expireTime">绑定过期时间</param>
/// <returns>可用的实验板,如果没有可用的板子则返回空</returns> /// <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 (board) => board.Status == Database.Board.BoardStatus.Available
).ToArray(); ).ToArray();
@ -372,12 +510,34 @@ public class AppDataConnection : DataConnection
else else
{ {
var board = boards[0]; var board = boards[0];
board.Status = Database.Board.BoardStatus.Busy; var user = this.UserTable.Where(u => u.ID == userId).FirstOrDefault();
this.Board
if (user == null)
{
logger.Error($"未找到用户: {userId}");
return new(null);
}
// 更新板子状态和用户绑定信息
this.BoardTable
.Where(target => target.ID == board.ID) .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(); .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); return new(board);
} }
} }
@ -385,10 +545,10 @@ public class AppDataConnection : DataConnection
/// <summary> /// <summary>
/// 用户表 /// 用户表
/// </summary> /// </summary>
public ITable<User> User => this.GetTable<User>(); public ITable<User> UserTable => this.GetTable<User>();
/// <summary> /// <summary>
/// FPGA 板子表 /// FPGA 板子表
/// </summary> /// </summary>
public ITable<Board> Board => this.GetTable<Board>(); public ITable<Board> BoardTable => this.GetTable<Board>();
} }

View File

@ -651,9 +651,14 @@ export class DataClient {
/** /**
* *
* @param durationHours (optional) 1
*/ */
getAvailableBoard(): Promise<Board> { getAvailableBoard(durationHours: number | undefined): Promise<Board> {
let url_ = this.baseUrl + "/api/Data/GetAvailableBoard"; 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(/[?&]$/, ""); url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = { let options_: RequestInit = {
@ -697,6 +702,108 @@ export class DataClient {
return Promise.resolve<Board>(null as any); 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) * @param name (optional)
@ -1115,7 +1222,8 @@ export class JtagClient {
} }
/** /**
* *
* @return
*/ */
index(): Promise<string> { index(): Promise<string> {
let url_ = this.baseUrl + "/api/Jtag"; let url_ = this.baseUrl + "/api/Jtag";
@ -1153,9 +1261,10 @@ export class JtagClient {
} }
/** /**
* Jtag ID Code * JTAG ID Code
* @param address (optional) * @param address (optional) JTAG
* @param port (optional) * @param port (optional) JTAG
* @return ID Code
*/ */
getDeviceIDCode(address: string | undefined, port: number | undefined): Promise<number> { getDeviceIDCode(address: string | undefined, port: number | undefined): Promise<number> {
let url_ = this.baseUrl + "/api/Jtag/GetDeviceIDCode?"; let url_ = this.baseUrl + "/api/Jtag/GetDeviceIDCode?";
@ -1199,6 +1308,13 @@ export class JtagClient {
result500 = Exception.fromJS(resultData500); result500 = Exception.fromJS(resultData500);
return throwException("A server side error occurred.", status, _responseText, _headers, result500); 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) { } else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => { return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers); return throwException("An unexpected server error occurred.", status, _responseText, _headers);
@ -1208,9 +1324,10 @@ export class JtagClient {
} }
/** /**
* * JTAG
* @param address (optional) * @param address (optional) JTAG
* @param port (optional) * @param port (optional) JTAG
* @return
*/ */
readStatusReg(address: string | undefined, port: number | undefined): Promise<void> { readStatusReg(address: string | undefined, port: number | undefined): Promise<void> {
let url_ = this.baseUrl + "/api/Jtag/ReadStatusReg?"; let url_ = this.baseUrl + "/api/Jtag/ReadStatusReg?";
@ -1246,6 +1363,13 @@ export class JtagClient {
return response.text().then((_responseText) => { return response.text().then((_responseText) => {
return throwException("A server side error occurred.", status, _responseText, _headers); 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) { } else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => { return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers); 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) * @param file (optional)
* @return
*/ */
uploadBitstream(address: string | undefined, file: FileParameter | undefined): Promise<boolean> { uploadBitstream(address: string | undefined, file: FileParameter | undefined): Promise<boolean> {
let url_ = this.baseUrl + "/api/Jtag/UploadBitstream?"; 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); 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) { } else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => { return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers); return throwException("An unexpected server error occurred.", status, _responseText, _headers);
@ -1314,9 +1450,10 @@ export class JtagClient {
} }
/** /**
* Jtag下载比特流文件 * JTAG FPGA
* @param address (optional) * @param address (optional) JTAG
* @param port (optional) * @param port (optional) JTAG
* @return
*/ */
downloadBitstream(address: string | undefined, port: number | undefined): Promise<boolean> { downloadBitstream(address: string | undefined, port: number | undefined): Promise<boolean> {
let url_ = this.baseUrl + "/api/Jtag/DownloadBitstream?"; let url_ = this.baseUrl + "/api/Jtag/DownloadBitstream?";
@ -1368,6 +1505,13 @@ export class JtagClient {
result500 = Exception.fromJS(resultData500); result500 = Exception.fromJS(resultData500);
return throwException("A server side error occurred.", status, _responseText, _headers, result500); 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) { } else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => { return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers); 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 address (optional) JTAG
* @param port (optional) [TODO:parameter] * @param port (optional) JTAG
* @return [TODO:return] * @return
*/ */
boundaryScanAllPorts(address: string | undefined, port: number | undefined): Promise<boolean> { boundaryScanAllPorts(address: string | undefined, port: number | undefined): Promise<boolean> {
let url_ = this.baseUrl + "/api/Jtag/BoundaryScanAllPorts?"; let url_ = this.baseUrl + "/api/Jtag/BoundaryScanAllPorts?";
@ -1432,6 +1576,13 @@ export class JtagClient {
result500 = Exception.fromJS(resultData500); result500 = Exception.fromJS(resultData500);
return throwException("A server side error occurred.", status, _responseText, _headers, result500); 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) { } else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => { return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers); 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 address (optional) JTAG
* @param port (optional) [TODO:parameter] * @param port (optional) JTAG
* @return [TODO:return] * @return
*/ */
boundaryScanLogicalPorts(address: string | undefined, port: number | undefined): Promise<{ [key: string]: boolean; }> { boundaryScanLogicalPorts(address: string | undefined, port: number | undefined): Promise<{ [key: string]: boolean; }> {
let url_ = this.baseUrl + "/api/Jtag/BoundaryScanLogicalPorts?"; let url_ = this.baseUrl + "/api/Jtag/BoundaryScanLogicalPorts?";
@ -1496,6 +1647,13 @@ export class JtagClient {
result500 = Exception.fromJS(resultData500); result500 = Exception.fromJS(resultData500);
return throwException("A server side error occurred.", status, _responseText, _headers, result500); 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) { } else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => { return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers); return throwException("An unexpected server error occurred.", status, _responseText, _headers);
@ -1505,11 +1663,11 @@ export class JtagClient {
} }
/** /**
* [TODO:description] * JTAG
* @param address (optional) [TODO:parameter] * @param address (optional) JTAG
* @param port (optional) [TODO:parameter] * @param port (optional) JTAG
* @param speed (optional) [TODO:parameter] * @param speed (optional) (Hz)
* @return [TODO:return] * @return
*/ */
setSpeed(address: string | undefined, port: number | undefined, speed: number | undefined): Promise<boolean> { setSpeed(address: string | undefined, port: number | undefined, speed: number | undefined): Promise<boolean> {
let url_ = this.baseUrl + "/api/Jtag/SetSpeed?"; let url_ = this.baseUrl + "/api/Jtag/SetSpeed?";
@ -1557,6 +1715,13 @@ export class JtagClient {
result500 = Exception.fromJS(resultData500); result500 = Exception.fromJS(resultData500);
return throwException("A server side error occurred.", status, _responseText, _headers, result500); 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) { } else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => { return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers); return throwException("An unexpected server error occurred.", status, _responseText, _headers);
@ -2715,6 +2880,8 @@ export class UserInfo implements IUserInfo {
eMail!: string; eMail!: string;
/** 用户关联的板卡ID */ /** 用户关联的板卡ID */
boardID!: string; boardID!: string;
/** 用户绑定板子的过期时间 */
boardExpireTime?: Date | undefined;
constructor(data?: IUserInfo) { constructor(data?: IUserInfo) {
if (data) { if (data) {
@ -2731,6 +2898,7 @@ export class UserInfo implements IUserInfo {
this.name = _data["name"]; this.name = _data["name"];
this.eMail = _data["eMail"]; this.eMail = _data["eMail"];
this.boardID = _data["boardID"]; 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["name"] = this.name;
data["eMail"] = this.eMail; data["eMail"] = this.eMail;
data["boardID"] = this.boardID; data["boardID"] = this.boardID;
data["boardExpireTime"] = this.boardExpireTime ? this.boardExpireTime.toISOString() : <any>undefined;
return data; return data;
} }
} }
@ -2761,6 +2930,8 @@ export interface IUserInfo {
eMail: string; eMail: string;
/** 用户关联的板卡ID */ /** 用户关联的板卡ID */
boardID: string; boardID: string;
/** 用户绑定板子的过期时间 */
boardExpireTime?: Date | undefined;
} }
/** FPGA 板子类,表示板子信息 */ /** FPGA 板子类,表示板子信息 */
@ -2775,6 +2946,10 @@ export class Board implements IBoard {
port!: number; port!: number;
/** FPGA 板子的当前状态 */ /** FPGA 板子的当前状态 */
status!: BoardStatus; status!: BoardStatus;
/** 占用该板子的用户的唯一标识符 */
occupiedUserID!: string;
/** 占用该板子的用户的用户名 */
occupiedUserName?: string | undefined;
/** FPGA 板子的固件版本号 */ /** FPGA 板子的固件版本号 */
firmVersion!: string; firmVersion!: string;
@ -2794,6 +2969,8 @@ export class Board implements IBoard {
this.ipAddr = _data["ipAddr"]; this.ipAddr = _data["ipAddr"];
this.port = _data["port"]; this.port = _data["port"];
this.status = _data["status"]; this.status = _data["status"];
this.occupiedUserID = _data["occupiedUserID"];
this.occupiedUserName = _data["occupiedUserName"];
this.firmVersion = _data["firmVersion"]; this.firmVersion = _data["firmVersion"];
} }
} }
@ -2812,6 +2989,8 @@ export class Board implements IBoard {
data["ipAddr"] = this.ipAddr; data["ipAddr"] = this.ipAddr;
data["port"] = this.port; data["port"] = this.port;
data["status"] = this.status; data["status"] = this.status;
data["occupiedUserID"] = this.occupiedUserID;
data["occupiedUserName"] = this.occupiedUserName;
data["firmVersion"] = this.firmVersion; data["firmVersion"] = this.firmVersion;
return data; return data;
} }
@ -2829,6 +3008,10 @@ export interface IBoard {
port: number; port: number;
/** FPGA 板子的当前状态 */ /** FPGA 板子的当前状态 */
status: BoardStatus; status: BoardStatus;
/** 占用该板子的用户的唯一标识符 */
occupiedUserID: string;
/** 占用该板子的用户的用户名 */
occupiedUserName?: string | undefined;
/** FPGA 板子的固件版本号 */ /** FPGA 板子的固件版本号 */
firmVersion: string; firmVersion: string;
} }

View File

@ -152,7 +152,7 @@ const loadUserInfo = async () => {
try { try {
const authenticated = await AuthManager.isAuthenticated(); const authenticated = await AuthManager.isAuthenticated();
if (authenticated) { if (authenticated) {
const client = AuthManager.createAuthenticatedClient(); const client = AuthManager.createAuthenticatedDataClient();
const userInfo = await client.getUserInfo(); const userInfo = await client.getUserInfo();
userName.value = userInfo.name; userName.value = userInfo.name;
isLoggedIn.value = true; isLoggedIn.value = true;

View File

@ -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 { export class AuthManager {
// 存储token到localStorage // 存储token到localStorage
@ -21,12 +45,13 @@ export class AuthManager {
return await AuthManager.verifyToken(); return await AuthManager.verifyToken();
} }
// 为HTTP请求添加Authorization header // 通用的为HTTP请求添加Authorization header的方法
public static addAuthHeader(client: any): void { public static addAuthHeader(client: SupportedClient): void {
const token = AuthManager.getToken(); const token = AuthManager.getToken();
if (token && client.http) { if (token) {
const originalFetch = client.http.fetch; // 创建一个自定义的 http 对象,包装原有的 fetch 方法
client.http.fetch = (url: RequestInfo, init?: RequestInit) => { const customHttp = {
fetch: (url: RequestInfo, init?: RequestInit) => {
if (!init) init = {}; if (!init) init = {};
if (!init.headers) init.headers = {}; if (!init.headers) init.headers = {};
@ -35,16 +60,95 @@ export class AuthManager {
(init.headers as any)["Authorization"] = `Bearer ${token}`; (init.headers as any)["Authorization"] = `Bearer ${token}`;
} }
return originalFetch(url, init); // 使用全局 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);
} }
} }
// 创建已配置认证的API客户端 // 私有方法创建带认证的HTTP客户端
public static createAuthenticatedClient(): DataClient { private static createAuthenticatedHttp() {
const client = new DataClient(); const token = AuthManager.getToken();
AuthManager.addAuthHeader(client); if (!token) {
return client; return null;
}
return {
fetch: (url: RequestInfo, init?: RequestInit) => {
if (!init) init = {};
if (!init.headers) init.headers = {};
if (typeof init.headers === "object" && init.headers !== null) {
(init.headers as any)["Authorization"] = `Bearer ${token}`;
}
return (window as any).fetch(url, init);
},
};
}
// 通用的创建已认证客户端的方法(使用泛型)
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); AuthManager.setToken(token);
// 验证token // 验证token
const authClient = AuthManager.createAuthenticatedClient(); const authClient = AuthManager.createAuthenticatedDataClient();
await authClient.testAuth(); await authClient.testAuth();
return true; return true;
@ -85,7 +189,7 @@ export class AuthManager {
return false; return false;
} }
const client = AuthManager.createAuthenticatedClient(); const client = AuthManager.createAuthenticatedDataClient();
await client.testAuth(); await client.testAuth();
return true; return true;
} catch (error) { } catch (error) {
@ -102,7 +206,7 @@ export class AuthManager {
return false; return false;
} }
const client = AuthManager.createAuthenticatedClient(); const client = AuthManager.createAuthenticatedDataClient();
await client.testAdminAuth(); await client.testAdminAuth();
return true; return true;
} catch (error) { } catch (error) {
@ -119,4 +223,10 @@ export class AuthManager {
return false; return false;
} }
} }
// 检查客户端是否已配置认证
public static isClientAuthenticated(client: SupportedClient): boolean {
const token = AuthManager.getToken();
return !!token;
}
} }

View File

@ -41,7 +41,7 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
return { success: false, error: "权限不足" }; return { success: false, error: "权限不足" };
} }
const client = AuthManager.createAuthenticatedClient(); const client = AuthManager.createAuthenticatedDataClient();
const result = await client.getAllBoards(); const result = await client.getAllBoards();
if (result) { if (result) {
@ -91,7 +91,7 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
return { success: false, error: "参数不完整" }; return { success: false, error: "参数不完整" };
} }
const client = AuthManager.createAuthenticatedClient(); const client = AuthManager.createAuthenticatedDataClient();
const boardId = await client.addBoard(name, ipAddr, port); const boardId = await client.addBoard(name, ipAddr, port);
if (boardId) { if (boardId) {
@ -130,7 +130,7 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
return { success: false, error: "板卡ID不能为空" }; return { success: false, error: "板卡ID不能为空" };
} }
const client = AuthManager.createAuthenticatedClient(); const client = AuthManager.createAuthenticatedDataClient();
const result = await client.deleteBoard(boardId); const result = await client.deleteBoard(boardId);
if (result > 0) { if (result > 0) {

View File

@ -84,6 +84,13 @@
@add-template="handleAddTemplate" @add-template="handleAddTemplate"
@close="showComponentsMenu = false" @close="showComponentsMenu = false"
/> />
<!-- 实验板申请对话框 -->
<RequestBoardDialog
:open="showRequestBoardDialog"
@close="handleRequestBoardClose"
@success="handleRequestBoardSuccess"
/>
</div> </div>
</template> </template>
@ -96,10 +103,13 @@ import ComponentSelector from "@/components/LabCanvas/ComponentSelector.vue";
import PropertyPanel from "@/components/PropertyPanel.vue"; import PropertyPanel from "@/components/PropertyPanel.vue";
import MarkdownRenderer from "@/components/MarkdownRenderer.vue"; import MarkdownRenderer from "@/components/MarkdownRenderer.vue";
import BottomBar from "@/views/Project/BottomBar.vue"; import BottomBar from "@/views/Project/BottomBar.vue";
import RequestBoardDialog from "@/views/Project/RequestBoardDialog.vue";
import { useProvideComponentManager } from "@/components/LabCanvas"; import { useProvideComponentManager } from "@/components/LabCanvas";
import type { DiagramData } from "@/components/LabCanvas"; import type { DiagramData } from "@/components/LabCanvas";
import { useAlertStore } from "@/components/Alert"; import { useAlertStore } from "@/components/Alert";
import { AuthManager } from "@/utils/AuthManager"; import { AuthManager } from "@/utils/AuthManager";
import { useEquipments } from "@/stores/equipments";
import type { Board } from "@/APIClient";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
const route = useRoute(); const route = useRoute();
@ -108,8 +118,14 @@ const router = useRouter();
// //
const componentManager = useProvideComponentManager(); const componentManager = useProvideComponentManager();
// store
const equipments = useEquipments();
const alert = useAlertStore(); const alert = useAlertStore();
// --- ---
const showRequestBoardDialog = ref(false);
// --- --- // --- ---
const showDocPanel = ref(false); const showDocPanel = ref(false);
const documentContent = ref(""); const documentContent = ref("");
@ -208,6 +224,62 @@ function updateComponentDirectProp(
componentManager.updateComponentDirectProp(componentId, propName, value); 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 () => { onMounted(async () => {
// //
@ -224,6 +296,9 @@ onMounted(async () => {
return; return;
} }
//
await checkAndInitializeBoard();
// //
if (route.query.tutorial) { if (route.query.tutorial) {
showDocPanel.value = true; showDocPanel.value = true;

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

View File

@ -14,10 +14,9 @@
</li> </li>
</ul> </ul>
<div class="divider divider-horizontal h-full"></div> <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"> <div v-if="activePage === 1">
<h2 class="card-title">用户信息</h2> <UserInfo />
<p>这里是用户信息页面的内容</p>
</div> </div>
<div v-else-if="activePage === 100"> <div v-else-if="activePage === 100">
<BoardTable /> <BoardTable />
@ -31,6 +30,7 @@ import BoardTable from "./BoardTable.vue";
import { toNumber } from "lodash"; import { toNumber } from "lodash";
import { onMounted, ref } from "vue"; import { onMounted, ref } from "vue";
import { AuthManager } from "@/utils/AuthManager"; import { AuthManager } from "@/utils/AuthManager";
import UserInfo from "./UserInfo.vue";
const activePage = ref(1); const activePage = ref(1);
const isAdmin = ref(false); const isAdmin = ref(false);
@ -42,7 +42,7 @@ function setActivePage(event: Event) {
onMounted(async () => { onMounted(async () => {
isAdmin.value = await AuthManager.verifyAdminAuth(); isAdmin.value = await AuthManager.verifyAdminAuth();
}) });
</script> </script>
<style scoped> <style scoped>

590
src/views/User/UserInfo.vue Normal file
View 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();
// 使JTAGID Code
const idCode = await jtagClient.getDeviceIDCode(
boardInfo.value.ipAddr,
boardInfo.value.port,
);
// ID Code0xFFFFFFFF
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>