From 9904fecbeee73443368e13d48c4e6c4f55d556b6 Mon Sep 17 00:00:00 2001 From: alivender <13898766233@163.com> Date: Sat, 2 Aug 2025 13:14:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=BB=9F=E4=B8=80=E8=B5=84=E6=BA=90?= =?UTF-8?q?=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/GenerateWebAPI.ts | 5 +- server/src/Controllers/ExamController.cs | 163 ---- server/src/Controllers/JtagController.cs | 211 ++--- server/src/Controllers/ResourceController.cs | 377 ++++++++ server/src/Database.cs | 240 +++-- src/APIClient.ts | 832 +++++++++--------- .../LabCanvas/composable/diagramManager.ts | 6 +- src/components/MarkdownRenderer.vue | 10 +- src/components/TutorialCarousel.vue | 7 +- src/components/UploadCard.vue | 80 +- src/components/equipments/MotherBoardCaps.vue | 8 +- src/stores/equipments.ts | 33 +- src/utils/AuthManager.ts | 8 +- src/views/ExamView.vue | 22 +- src/views/Project/Index.vue | 14 +- 15 files changed, 1178 insertions(+), 838 deletions(-) create mode 100644 server/src/Controllers/ResourceController.cs diff --git a/scripts/GenerateWebAPI.ts b/scripts/GenerateWebAPI.ts index 1b84950..2595ee6 100644 --- a/scripts/GenerateWebAPI.ts +++ b/scripts/GenerateWebAPI.ts @@ -303,8 +303,11 @@ async function generateApiClient(): Promise { async function generateSignalRClient(): Promise { console.log("Generating SignalR TypeScript client..."); try { + // TypedSignalR.Client.TypeScript.Analyzer 会在编译时自动生成客户端 + // 我们只需要确保服务器项目构建一次即可生成 TypeScript 客户端 const { stdout, stderr } = await execAsync( - "dotnet tsrts --project ./server/server.csproj --output ./src", + "dotnet build --configuration Release", + { cwd: "./server" } ); if (stdout) console.log(stdout); if (stderr) console.error(stderr); diff --git a/server/src/Controllers/ExamController.cs b/server/src/Controllers/ExamController.cs index cc3c540..178907f 100644 --- a/server/src/Controllers/ExamController.cs +++ b/server/src/Controllers/ExamController.cs @@ -101,22 +101,6 @@ public class ExamController : ControllerBase public bool IsVisibleToUsers { get; set; } = true; } - /// - /// 资源信息类 - /// - public class ResourceInfo - { - /// - /// 资源ID - /// - public int ID { get; set; } - - /// - /// 资源名称 - /// - public required string Name { get; set; } - } - /// /// 创建实验请求类 /// @@ -304,151 +288,4 @@ public class ExamController : ControllerBase return StatusCode(StatusCodes.Status500InternalServerError, $"创建实验失败: {ex.Message}"); } } - - /// - /// 添加实验资源 - /// - /// 实验ID - /// 资源类型 - /// 资源文件 - /// 添加结果 - [Authorize("Admin")] - [HttpPost("{examId}/resources/{resourceType}")] - [EnableCors("Users")] - [ProducesResponseType(typeof(ResourceInfo), StatusCodes.Status201Created)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task AddExamResource(string examId, string resourceType, IFormFile file) - { - if (string.IsNullOrWhiteSpace(examId) || string.IsNullOrWhiteSpace(resourceType) || file == null) - return BadRequest("实验ID、资源类型和文件不能为空"); - - try - { - using var db = new Database.AppDataConnection(); - - // 读取文件数据 - using var memoryStream = new MemoryStream(); - await file.CopyToAsync(memoryStream); - var fileData = memoryStream.ToArray(); - - var result = db.AddExamResource(examId, resourceType, file.FileName, fileData); - - if (!result.IsSuccessful) - { - if (result.Error.Message.Contains("不存在")) - return NotFound(result.Error.Message); - if (result.Error.Message.Contains("已存在")) - return Conflict(result.Error.Message); - - logger.Error($"添加实验资源时出错: {result.Error.Message}"); - return StatusCode(StatusCodes.Status500InternalServerError, $"添加实验资源失败: {result.Error.Message}"); - } - - var resource = result.Value; - var resourceInfo = new ResourceInfo - { - ID = resource.ID, - Name = resource.ResourceName - }; - - logger.Info($"成功添加实验资源: {examId}/{resourceType}/{file.FileName}"); - return CreatedAtAction(nameof(GetExamResourceById), new { resourceId = resource.ID }, resourceInfo); - } - catch (Exception ex) - { - logger.Error($"添加实验资源 {examId}/{resourceType}/{file.FileName} 时出错: {ex.Message}"); - return StatusCode(StatusCodes.Status500InternalServerError, $"添加实验资源失败: {ex.Message}"); - } - } - - /// - /// 获取指定实验ID的指定资源类型的所有资源的ID和名称 - /// - /// 实验ID - /// 资源类型 - /// 资源列表 - [Authorize] - [HttpGet("{examId}/resources/{resourceType}")] - [EnableCors("Users")] - [ProducesResponseType(typeof(ResourceInfo[]), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public IActionResult GetExamResourceList(string examId, string resourceType) - { - if (string.IsNullOrWhiteSpace(examId) || string.IsNullOrWhiteSpace(resourceType)) - return BadRequest("实验ID和资源类型不能为空"); - - try - { - using var db = new Database.AppDataConnection(); - var result = db.GetExamResourceList(examId, resourceType); - - if (!result.IsSuccessful) - { - logger.Error($"获取实验资源列表时出错: {result.Error.Message}"); - return StatusCode(StatusCodes.Status500InternalServerError, $"获取实验资源列表失败: {result.Error.Message}"); - } - - var resources = result.Value.Select(r => new ResourceInfo - { - ID = r.ID, - Name = r.Name - }).ToArray(); - - logger.Info($"成功获取实验资源列表: {examId}/{resourceType},共 {resources.Length} 个资源"); - return Ok(resources); - } - catch (Exception ex) - { - logger.Error($"获取实验资源列表 {examId}/{resourceType} 时出错: {ex.Message}"); - return StatusCode(StatusCodes.Status500InternalServerError, $"获取实验资源列表失败: {ex.Message}"); - } - } - - /// - /// 根据资源ID下载资源 - /// - /// 资源ID - /// 资源文件 - [HttpGet("resources/{resourceId}")] - [EnableCors("Users")] - [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public IActionResult GetExamResourceById(int resourceId) - { - try - { - using var db = new Database.AppDataConnection(); - var result = db.GetExamResourceById(resourceId); - - if (!result.IsSuccessful) - { - logger.Error($"获取资源时出错: {result.Error.Message}"); - return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {result.Error.Message}"); - } - - if (!result.Value.HasValue) - { - logger.Warn($"资源不存在: {resourceId}"); - return NotFound($"资源 {resourceId} 不存在"); - } - - var resource = result.Value.Value; - logger.Info($"成功获取资源: {resourceId} ({resource.ResourceName})"); - return File(resource.Data, resource.MimeType, resource.ResourceName); - } - catch (Exception ex) - { - logger.Error($"获取资源 {resourceId} 时出错: {ex.Message}"); - return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {ex.Message}"); - } - } } diff --git a/server/src/Controllers/JtagController.cs b/server/src/Controllers/JtagController.cs index 74fedd5..458e8ba 100644 --- a/server/src/Controllers/JtagController.cs +++ b/server/src/Controllers/JtagController.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Mvc; +using Database; namespace server.Controllers; @@ -14,8 +15,6 @@ public class JtagController : ControllerBase { private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); - private const string BITSTREAM_PATH = "bitstream/Jtag"; - /// /// 控制器首页信息 /// @@ -112,64 +111,12 @@ public class JtagController : ControllerBase } } - /// - /// 上传比特流文件到服务器 - /// - /// 目标设备地址 - /// 比特流文件 - /// 上传结果 - [HttpPost("UploadBitstream")] - [EnableCors("Users")] - [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async ValueTask 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("未选择文件"); - } - - 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); - } - } - /// /// 通过 JTAG 下载比特流文件到 FPGA 设备 /// /// JTAG 设备地址 /// JTAG 设备端口 + /// 比特流ID /// 下载结果 [HttpPost("DownloadBitstream")] [EnableCors("Users")] @@ -177,87 +124,111 @@ public class JtagController : ControllerBase [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public async ValueTask DownloadBitstream(string address, int port) + public async ValueTask DownloadBitstream(string address, int port, int bitstreamId) { - 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"); - } + logger.Info($"User {User.Identity?.Name} initiating bitstream download to device {address}:{port} using bitstream ID: {bitstreamId}"); 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)) + // 获取当前用户名 + var username = User.Identity?.Name; + if (string.IsNullOrEmpty(username)) { - if (fileStream is null || fileStream.Length <= 0) + logger.Warn("Anonymous user attempted to download bitstream"); + return TypedResults.Unauthorized(); + } + + // 从数据库获取用户信息 + using var db = new Database.AppDataConnection(); + var userResult = db.GetUserByName(username); + if (!userResult.IsSuccessful || !userResult.Value.HasValue) + { + logger.Error($"User {username} not found in database"); + return TypedResults.BadRequest("用户不存在"); + } + + var user = userResult.Value.Value; + + // 从数据库获取比特流 + var bitstreamResult = db.GetResourceById(bitstreamId); + + if (!bitstreamResult.IsSuccessful) + { + logger.Error($"User {username} failed to get bitstream from database: {bitstreamResult.Error}"); + return TypedResults.InternalServerError($"数据库查询失败: {bitstreamResult.Error?.Message}"); + } + + if (!bitstreamResult.Value.HasValue) + { + logger.Warn($"User {username} attempted to download non-existent bitstream ID: {bitstreamId}"); + return TypedResults.BadRequest("比特流不存在"); + } + + var bitstream = bitstreamResult.Value.Value; + + // 处理比特流数据 + var fileBytes = bitstream.Data; + if (fileBytes == null || fileBytes.Length == 0) + { + logger.Warn($"User {username} found empty bitstream data for ID: {bitstreamId}"); + return TypedResults.BadRequest("比特流数据为空,请重新上传"); + } + + logger.Info($"User {username} processing bitstream file of size: {fileBytes.Length} bytes"); + + // 定义缓冲区大小: 32KB + byte[] buffer = new byte[32 * 1024]; + byte[] revBuffer = new byte[32 * 1024]; + long totalBytesProcessed = 0; + + // 使用内存流处理文件 + using (var inputStream = new MemoryStream(fileBytes)) + using (var outputStream = new MemoryStream()) + { + int bytesRead; + while ((bytesRead = await inputStream.ReadAsync(buffer, 0, buffer.Length)) > 0) { - logger.Warn($"User {User.Identity?.Name} found invalid bitstream file for device {address}"); - return TypedResults.BadRequest("Wrong bitstream, Please upload it again"); + // 反转 32bits + var retBuffer = Common.Number.ReverseBytes(buffer, 4); + if (!retBuffer.IsSuccessful) + { + logger.Error($"User {username} failed to reverse bytes: {retBuffer.Error}"); + return TypedResults.InternalServerError(retBuffer.Error); + } + revBuffer = retBuffer.Value; + + for (int i = 0; i < revBuffer.Length; i++) + { + revBuffer[i] = Common.Number.ReverseBits(revBuffer[i]); + } + + await outputStream.WriteAsync(revBuffer, 0, bytesRead); + totalBytesProcessed += bytesRead; } - logger.Info($"User {User.Identity?.Name} processing bitstream file of size: {fileStream.Length} bytes"); + // 获取处理后的数据 + var processedBytes = outputStream.ToArray(); + logger.Info($"User {username} processed {totalBytesProcessed} bytes for device {address}"); - // 定义缓冲区大小: 32KB - byte[] buffer = new byte[32 * 1024]; - byte[] revBuffer = new byte[32 * 1024]; - long totalBytesRead = 0; + // 下载比特流 + var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port); + var ret = await jtagCtrl.DownloadBitstream(processedBytes); - // 使用异步流读取文件 - using (var memoryStream = new MemoryStream()) + if (ret.IsSuccessful) { - int bytesRead; - while ((bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length)) > 0) - { - // 反转 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++) - { - revBuffer[i] = Common.Number.ReverseBits(revBuffer[i]); - } - - await memoryStream.WriteAsync(revBuffer, 0, bytesRead); - totalBytesRead += bytesRead; - } - - // 将所有数据转换为字节数组(注意:如果文件非常大,可能不适合完全加载到内存) - 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 ret = await jtagCtrl.DownloadBitstream(fileBytes); - - if (ret.IsSuccessful) - { - logger.Info($"User {User.Identity?.Name} successfully downloaded bitstream to device {address}"); - return TypedResults.Ok(ret.Value); - } - else - { - logger.Error($"User {User.Identity?.Name} failed to download bitstream to device {address}: {ret.Error}"); - return TypedResults.InternalServerError(ret.Error); - } + logger.Info($"User {username} successfully downloaded bitstream '{bitstream.ResourceName}' to device {address}"); + return TypedResults.Ok(ret.Value); + } + else + { + logger.Error($"User {username} failed to download bitstream to device {address}: {ret.Error}"); + return TypedResults.InternalServerError(ret.Error); } } } catch (Exception ex) { - logger.Error(ex, $"User {User.Identity?.Name} encountered exception while downloading bitstream to device {address}"); + logger.Error(ex, $"User encountered exception while downloading bitstream to device {address}"); return TypedResults.InternalServerError(ex); } } diff --git a/server/src/Controllers/ResourceController.cs b/server/src/Controllers/ResourceController.cs new file mode 100644 index 0000000..af8c9db --- /dev/null +++ b/server/src/Controllers/ResourceController.cs @@ -0,0 +1,377 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Mvc; +using DotNext; +using Database; + +namespace server.Controllers; + +/// +/// 资源控制器 - 提供统一的资源管理API +/// +[ApiController] +[Route("api/[controller]")] +public class ResourceController : ControllerBase +{ + private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); + + /// + /// 资源信息类 + /// + public class ResourceInfo + { + /// + /// 资源ID + /// + public int ID { get; set; } + + /// + /// 资源名称 + /// + public required string Name { get; set; } + + /// + /// 资源类型 + /// + public required string Type { get; set; } + + /// + /// 资源用途(template/user) + /// + public required string Purpose { get; set; } + + /// + /// 上传时间 + /// + public DateTime UploadTime { get; set; } + + /// + /// 所属实验ID(可选) + /// + public string? ExamID { get; set; } + + /// + /// MIME类型 + /// + public string? MimeType { get; set; } + } + + /// + /// 添加资源请求类 + /// + public class AddResourceRequest + { + /// + /// 资源类型 + /// + public required string ResourceType { get; set; } + + /// + /// 资源用途(template/user) + /// + public required string ResourcePurpose { get; set; } + + /// + /// 所属实验ID(可选) + /// + public string? ExamID { get; set; } + } + + /// + /// 添加资源(文件上传) + /// + /// 添加资源请求 + /// 资源文件 + /// 添加结果 + [Authorize] + [HttpPost] + [EnableCors("Users")] + [ProducesResponseType(typeof(ResourceInfo), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task AddResource([FromForm] AddResourceRequest request, IFormFile file) + { + if (string.IsNullOrWhiteSpace(request.ResourceType) || string.IsNullOrWhiteSpace(request.ResourcePurpose) || file == null) + return BadRequest("资源类型、资源用途和文件不能为空"); + + // 验证资源用途 + if (request.ResourcePurpose != Resource.ResourcePurposes.Template && request.ResourcePurpose != Resource.ResourcePurposes.User) + return BadRequest($"无效的资源用途: {request.ResourcePurpose}"); + + // 模板资源需要管理员权限 + if (request.ResourcePurpose == Resource.ResourcePurposes.Template && !User.IsInRole("Admin")) + return Forbid("只有管理员可以添加模板资源"); + + try + { + using var db = new Database.AppDataConnection(); + + // 获取当前用户ID + var userName = User.Identity?.Name; + if (string.IsNullOrEmpty(userName)) + return Unauthorized("无法获取用户信息"); + + var userResult = db.GetUserByName(userName); + if (!userResult.IsSuccessful || !userResult.Value.HasValue) + return Unauthorized("用户不存在"); + + var user = userResult.Value.Value; + + // 读取文件数据 + using var memoryStream = new MemoryStream(); + await file.CopyToAsync(memoryStream); + var fileData = memoryStream.ToArray(); + + var result = db.AddResource(user.ID, request.ResourceType, request.ResourcePurpose, file.FileName, fileData, request.ExamID); + + if (!result.IsSuccessful) + { + if (result.Error.Message.Contains("不存在")) + return NotFound(result.Error.Message); + + logger.Error($"添加资源时出错: {result.Error.Message}"); + return StatusCode(StatusCodes.Status500InternalServerError, $"添加资源失败: {result.Error.Message}"); + } + + var resource = result.Value; + var resourceInfo = new ResourceInfo + { + ID = resource.ID, + Name = resource.ResourceName, + Type = resource.ResourceType, + Purpose = resource.ResourcePurpose, + UploadTime = resource.UploadTime, + ExamID = resource.ExamID, + MimeType = resource.MimeType + }; + + logger.Info($"成功添加资源: {request.ResourceType}/{request.ResourcePurpose}/{file.FileName}"); + return CreatedAtAction(nameof(GetResourceById), new { resourceId = resource.ID }, resourceInfo); + } + catch (Exception ex) + { + logger.Error($"添加资源 {request.ResourceType}/{request.ResourcePurpose}/{file.FileName} 时出错: {ex.Message}"); + return StatusCode(StatusCodes.Status500InternalServerError, $"添加资源失败: {ex.Message}"); + } + } + + /// + /// 获取资源列表 + /// + /// 实验ID(可选) + /// 资源类型(可选) + /// 资源用途(可选) + /// 资源列表 + [Authorize] + [HttpGet] + [EnableCors("Users")] + [ProducesResponseType(typeof(ResourceInfo[]), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public IActionResult GetResourceList([FromQuery] string? examId = null, [FromQuery] string? resourceType = null, [FromQuery] string? resourcePurpose = null) + { + try + { + using var db = new Database.AppDataConnection(); + + // 获取当前用户ID + var userName = User.Identity?.Name; + if (string.IsNullOrEmpty(userName)) + return Unauthorized("无法获取用户信息"); + + var userResult = db.GetUserByName(userName); + if (!userResult.IsSuccessful || !userResult.Value.HasValue) + return Unauthorized("用户不存在"); + + var user = userResult.Value.Value; + + // 普通用户只能查看自己的资源和模板资源 + Guid? userId = null; + if (!User.IsInRole("Admin")) + { + // 如果指定了用户资源用途,则只查看自己的资源 + if (resourcePurpose == Resource.ResourcePurposes.User) + { + userId = user.ID; + } + // 如果指定了模板资源用途,则不限制用户ID + else if (resourcePurpose == Resource.ResourcePurposes.Template) + { + userId = null; + } + // 如果没有指定用途,则查看自己的用户资源和所有模板资源 + else + { + // 这种情况下需要分别查询并合并结果 + var userResourcesResult = db.GetFullResourceList(examId, resourceType, Resource.ResourcePurposes.User, user.ID); + var templateResourcesResult = db.GetFullResourceList(examId, resourceType, Resource.ResourcePurposes.Template, null); + + if (!userResourcesResult.IsSuccessful || !templateResourcesResult.IsSuccessful) + { + logger.Error($"获取资源列表时出错"); + return StatusCode(StatusCodes.Status500InternalServerError, "获取资源列表失败"); + } + + var allResources = userResourcesResult.Value.Concat(templateResourcesResult.Value) + .OrderByDescending(r => r.UploadTime); + var mergedResourceInfos = allResources.Select(r => new ResourceInfo + { + ID = r.ID, + Name = r.ResourceName, + Type = r.ResourceType, + Purpose = r.ResourcePurpose, + UploadTime = r.UploadTime, + ExamID = r.ExamID, + MimeType = r.MimeType + }).ToArray(); + + logger.Info($"成功获取资源列表,共 {mergedResourceInfos.Length} 个资源"); + return Ok(mergedResourceInfos); + } + } + + var result = db.GetFullResourceList(examId, resourceType, resourcePurpose, userId); + + if (!result.IsSuccessful) + { + logger.Error($"获取资源列表时出错: {result.Error.Message}"); + return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源列表失败: {result.Error.Message}"); + } + + var resources = result.Value.Select(r => new ResourceInfo + { + ID = r.ID, + Name = r.ResourceName, + Type = r.ResourceType, + Purpose = r.ResourcePurpose, + UploadTime = r.UploadTime, + ExamID = r.ExamID, + MimeType = r.MimeType + }).ToArray(); + + logger.Info($"成功获取资源列表,共 {resources.Length} 个资源"); + return Ok(resources); + } + catch (Exception ex) + { + logger.Error($"获取资源列表时出错: {ex.Message}"); + return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源列表失败: {ex.Message}"); + } + } + + /// + /// 根据资源ID下载资源 + /// + /// 资源ID + /// 资源文件 + [HttpGet("{resourceId}")] + [EnableCors("Users")] + [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public IActionResult GetResourceById(int resourceId) + { + try + { + using var db = new Database.AppDataConnection(); + var result = db.GetResourceById(resourceId); + + if (!result.IsSuccessful) + { + logger.Error($"获取资源时出错: {result.Error.Message}"); + return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {result.Error.Message}"); + } + + if (!result.Value.HasValue) + { + logger.Warn($"资源不存在: {resourceId}"); + return NotFound($"资源 {resourceId} 不存在"); + } + + var resource = result.Value.Value; + logger.Info($"成功获取资源: {resourceId} ({resource.ResourceName})"); + return File(resource.Data, resource.MimeType ?? "application/octet-stream", resource.ResourceName); + } + catch (Exception ex) + { + logger.Error($"获取资源 {resourceId} 时出错: {ex.Message}"); + return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {ex.Message}"); + } + } + + /// + /// 删除资源 + /// + /// 资源ID + /// 删除结果 + [Authorize] + [HttpDelete("{resourceId}")] + [EnableCors("Users")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public IActionResult DeleteResource(int resourceId) + { + try + { + using var db = new Database.AppDataConnection(); + + // 获取当前用户信息 + var userName = User.Identity?.Name; + if (string.IsNullOrEmpty(userName)) + return Unauthorized("无法获取用户信息"); + + var userResult = db.GetUserByName(userName); + if (!userResult.IsSuccessful || !userResult.Value.HasValue) + return Unauthorized("用户不存在"); + + var user = userResult.Value.Value; + + // 先获取资源信息以验证权限 + var resourceResult = db.GetResourceById(resourceId); + if (!resourceResult.IsSuccessful) + { + logger.Error($"获取资源时出错: {resourceResult.Error.Message}"); + return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {resourceResult.Error.Message}"); + } + + if (!resourceResult.Value.HasValue) + { + logger.Warn($"资源不存在: {resourceId}"); + return NotFound($"资源 {resourceId} 不存在"); + } + + var resource = resourceResult.Value.Value; + + // 权限检查:管理员可以删除所有资源,普通用户只能删除自己的用户资源 + if (!User.IsInRole("Admin")) + { + if (resource.ResourcePurpose == Resource.ResourcePurposes.Template) + return Forbid("普通用户不能删除模板资源"); + + if (resource.UserID != user.ID) + return Forbid("只能删除自己的资源"); + } + + var deleteResult = db.DeleteResource(resourceId); + if (!deleteResult.IsSuccessful) + { + logger.Error($"删除资源时出错: {deleteResult.Error.Message}"); + return StatusCode(StatusCodes.Status500InternalServerError, $"删除资源失败: {deleteResult.Error.Message}"); + } + + logger.Info($"成功删除资源: {resourceId} ({resource.ResourceName})"); + return NoContent(); + } + catch (Exception ex) + { + logger.Error($"删除资源 {resourceId} 时出错: {ex.Message}"); + return StatusCode(StatusCodes.Status500InternalServerError, $"删除资源失败: {ex.Message}"); + } + } +} diff --git a/server/src/Database.cs b/server/src/Database.cs index 97105b5..9f50902 100644 --- a/server/src/Database.cs +++ b/server/src/Database.cs @@ -229,9 +229,9 @@ public class Exam } /// -/// 实验资源表(图片等) +/// 资源类,统一管理实验资源、用户比特流等各类资源 /// -public class ExamResource +public class Resource { /// /// 资源的唯一标识符 @@ -240,17 +240,29 @@ public class ExamResource public int ID { get; set; } /// - /// 所属实验ID + /// 上传资源的用户ID /// [NotNull] - public required string ExamID { get; set; } + public required Guid UserID { get; set; } /// - /// 资源类型(images, markdown, bitstream, diagram, project) + /// 所属实验ID(可选,如果不属于特定实验则为空) + /// + [Nullable] + public string? ExamID { get; set; } + + /// + /// 资源类型(images, markdown, bitstream, diagram, project等) /// [NotNull] public required string ResourceType { get; set; } + /// + /// 资源用途:template(模板)或 user(用户上传) + /// + [NotNull] + public required string ResourcePurpose { get; set; } + /// /// 资源名称(包含文件扩展名) /// @@ -264,10 +276,10 @@ public class ExamResource public required byte[] Data { get; set; } /// - /// 资源创建时间 + /// 资源创建/上传时间 /// [NotNull] - public DateTime CreatedTime { get; set; } = DateTime.Now; + public DateTime UploadTime { get; set; } = DateTime.Now; /// /// 资源的MIME类型 @@ -305,6 +317,22 @@ public class ExamResource /// public const string Project = "project"; } + + /// + /// 资源用途枚举 + /// + public static class ResourcePurposes + { + /// + /// 模板资源,通常由管理员上传,供用户参考 + /// + public const string Template = "template"; + + /// + /// 用户上传的资源 + /// + public const string User = "user"; + } } /// @@ -355,7 +383,7 @@ public class AppDataConnection : DataConnection this.CreateTable(); this.CreateTable(); this.CreateTable(); - this.CreateTable(); + this.CreateTable(); logger.Info("数据库表创建完成"); } @@ -368,7 +396,7 @@ public class AppDataConnection : DataConnection this.DropTable(); this.DropTable(); this.DropTable(); - this.DropTable(); + this.DropTable(); logger.Warn("所有数据库表已删除"); } @@ -828,9 +856,9 @@ public class AppDataConnection : DataConnection public ITable ExamTable => this.GetTable(); /// - /// 实验资源表 + /// 资源表(统一管理实验资源、用户比特流等) /// - public ITable ExamResourceTable => this.GetTable(); + public ITable ResourceTable => this.GetTable(); /// /// 创建新实验 @@ -933,34 +961,44 @@ public class AppDataConnection : DataConnection } /// - /// 添加实验资源 + /// 添加资源 /// - /// 所属实验ID + /// 上传用户ID /// 资源类型 + /// 资源用途(template 或 user) /// 资源名称 /// 资源二进制数据 + /// 所属实验ID(可选) /// MIME类型(可选,将根据文件扩展名自动确定) /// 创建的资源 - public Result AddExamResource(string examId, string resourceType, string resourceName, byte[] data, string? mimeType = null) + public Result AddResource(Guid userId, string resourceType, string resourcePurpose, string resourceName, byte[] data, string? examId = null, string? mimeType = null) { try { - // 验证实验是否存在 - var exam = this.ExamTable.Where(e => e.ID == examId).FirstOrDefault(); - if (exam == null) + // 验证用户是否存在 + var user = this.UserTable.Where(u => u.ID == userId).FirstOrDefault(); + if (user == null) { - logger.Error($"实验不存在: {examId}"); - return new(new Exception($"实验不存在: {examId}")); + logger.Error($"用户不存在: {userId}"); + return new(new Exception($"用户不存在: {userId}")); } - // 检查资源是否已存在 - var existingResource = this.ExamResourceTable - .Where(r => r.ExamID == examId && r.ResourceType == resourceType && r.ResourceName == resourceName) - .FirstOrDefault(); - if (existingResource != null) + // 如果指定了实验ID,验证实验是否存在 + if (!string.IsNullOrEmpty(examId)) { - logger.Error($"资源已存在: {examId}/{resourceType}/{resourceName}"); - return new(new Exception($"资源已存在: {examId}/{resourceType}/{resourceName}")); + var exam = this.ExamTable.Where(e => e.ID == examId).FirstOrDefault(); + if (exam == null) + { + logger.Error($"实验不存在: {examId}"); + return new(new Exception($"实验不存在: {examId}")); + } + } + + // 验证资源用途 + if (resourcePurpose != Resource.ResourcePurposes.Template && resourcePurpose != Resource.ResourcePurposes.User) + { + logger.Error($"无效的资源用途: {resourcePurpose}"); + return new(new Exception($"无效的资源用途: {resourcePurpose}")); } // 如果未指定MIME类型,根据文件扩展名自动确定 @@ -970,49 +1008,126 @@ public class AppDataConnection : DataConnection mimeType = GetMimeTypeFromExtension(extension, resourceName); } - var resource = new ExamResource + var resource = new Resource { + UserID = userId, ExamID = examId, ResourceType = resourceType, + ResourcePurpose = resourcePurpose, ResourceName = resourceName, Data = data, MimeType = mimeType, - CreatedTime = DateTime.Now + UploadTime = DateTime.Now }; - this.Insert(resource); - logger.Info($"新资源已添加: {examId}/{resourceType}/{resourceName} ({data.Length} bytes)"); + var insertedId = this.InsertWithIdentity(resource); + resource.ID = Convert.ToInt32(insertedId); + + logger.Info($"新资源已添加: {userId}/{resourceType}/{resourcePurpose}/{resourceName} ({data.Length} bytes)" + + (examId != null ? $" [实验: {examId}]" : "") + $" [ID: {resource.ID}]"); return new(resource); } catch (Exception ex) { - logger.Error($"添加实验资源时出错: {ex.Message}"); + logger.Error($"添加资源时出错: {ex.Message}"); return new(ex); } } /// - /// 获取指定实验ID的指定资源类型的所有资源的ID和名称 - /// - /// 实验ID + /// 获取资源信息列表(返回ID和名称) /// 资源类型 + /// 实验ID(可选) + /// 资源用途(可选) + /// 用户ID(可选) + /// /// 资源信息列表 - public Result<(int ID, string Name)[]> GetExamResourceList(string examId, string resourceType) + public Result<(int ID, string Name)[]> GetResourceList(string resourceType, string? examId = null, string? resourcePurpose = null, Guid? userId = null) { try { - var resources = this.ExamResourceTable - .Where(r => r.ExamID == examId && r.ResourceType == resourceType) + var query = this.ResourceTable.Where(r => r.ResourceType == resourceType); + + if (examId != null) + { + query = query.Where(r => r.ExamID == examId); + } + + if (resourcePurpose != null) + { + query = query.Where(r => r.ResourcePurpose == resourcePurpose); + } + + if (userId != null) + { + query = query.Where(r => r.UserID == userId); + } + + var resources = query .Select(r => new { r.ID, r.ResourceName }) .ToArray(); var result = resources.Select(r => (r.ID, r.ResourceName)).ToArray(); - logger.Info($"获取实验资源列表: {examId}/{resourceType},共 {result.Length} 个资源"); + logger.Info($"获取资源列表: {resourceType}" + + (examId != null ? $"/{examId}" : "") + + (resourcePurpose != null ? $"/{resourcePurpose}" : "") + + (userId != null ? $"/{userId}" : "") + + $",共 {result.Length} 个资源"); return new(result); } catch (Exception ex) { - logger.Error($"获取实验资源列表时出错: {ex.Message}"); + logger.Error($"获取资源列表时出错: {ex.Message}"); + return new(ex); + } + } + + /// + /// 获取完整的资源列表 + /// + /// 实验ID(可选) + /// 资源类型(可选) + /// 资源用途(可选) + /// 用户ID(可选) + /// 完整的资源对象列表 + public Result> GetFullResourceList(string? examId = null, string? resourceType = null, string? resourcePurpose = null, Guid? userId = null) + { + try + { + var query = this.ResourceTable.AsQueryable(); + + if (examId != null) + { + query = query.Where(r => r.ExamID == examId); + } + + if (resourceType != null) + { + query = query.Where(r => r.ResourceType == resourceType); + } + + if (resourcePurpose != null) + { + query = query.Where(r => r.ResourcePurpose == resourcePurpose); + } + + if (userId != null) + { + query = query.Where(r => r.UserID == userId); + } + + var resources = query.OrderByDescending(r => r.UploadTime).ToList(); + logger.Info($"获取完整资源列表" + + (examId != null ? $" [实验: {examId}]" : "") + + (resourceType != null ? $" [类型: {resourceType}]" : "") + + (resourcePurpose != null ? $" [用途: {resourcePurpose}]" : "") + + (userId != null ? $" [用户: {userId}]" : "") + + $",共 {resources.Count} 个资源"); + return new(resources); + } + catch (Exception ex) + { + logger.Error($"获取完整资源列表时出错: {ex.Message}"); return new(ex); } } @@ -1022,16 +1137,16 @@ public class AppDataConnection : DataConnection /// /// 资源ID /// 资源数据 - public Result> GetExamResourceById(int resourceId) + public Result> GetResourceById(int resourceId) { try { - var resource = this.ExamResourceTable.Where(r => r.ID == resourceId).FirstOrDefault(); + var resource = this.ResourceTable.Where(r => r.ID == resourceId).FirstOrDefault(); if (resource == null) { logger.Info($"未找到资源: {resourceId}"); - return new(Optional.None); + return new(Optional.None); } logger.Info($"成功获取资源: {resourceId} ({resource.ResourceName})"); @@ -1045,15 +1160,15 @@ public class AppDataConnection : DataConnection } /// - /// 删除实验资源 + /// 删除资源 /// /// 资源ID /// 删除的记录数 - public Result DeleteExamResource(int resourceId) + public Result DeleteResource(int resourceId) { try { - var result = this.ExamResourceTable.Where(r => r.ID == resourceId).Delete(); + var result = this.ResourceTable.Where(r => r.ID == resourceId).Delete(); logger.Info($"资源已删除: {resourceId},删除记录数: {result}"); return new(result); } @@ -1132,29 +1247,20 @@ public class AppDataConnection : DataConnection } /// - /// 删除所有实验 + /// 根据文件扩展名获取比特流MIME类型 /// - /// 删除的实验数量 - public int DeleteAllExams() + /// 文件扩展名 + /// MIME类型 + private string GetBitstreamMimeType(string extension) { - // 先删除所有实验资源 - var resourceDeleteCount = this.DeleteAllExamResources(); - logger.Info($"已删除所有实验资源,共删除 {resourceDeleteCount} 个资源"); - - // 再删除所有实验 - var examDeleteCount = this.ExamTable.Delete(); - logger.Info($"已删除所有实验,共删除 {examDeleteCount} 个实验"); - return examDeleteCount; - } - - /// - /// 删除所有实验资源 - /// - /// 删除的资源数量 - public int DeleteAllExamResources() - { - var deleteCount = this.ExamResourceTable.Delete(); - logger.Info($"已删除所有实验资源,共删除 {deleteCount} 个资源"); - return deleteCount; + return extension.ToLowerInvariant() switch + { + ".bit" => "application/octet-stream", + ".sbit" => "application/octet-stream", + ".bin" => "application/octet-stream", + ".mcs" => "application/octet-stream", + ".hex" => "text/plain", + _ => "application/octet-stream" + }; } } diff --git a/src/APIClient.ts b/src/APIClient.ts index aa103b9..e90c3c8 100644 --- a/src/APIClient.ts +++ b/src/APIClient.ts @@ -2942,277 +2942,6 @@ export class ExamClient { } return Promise.resolve(null as any); } - - /** - * 添加实验资源 - * @param examId 实验ID - * @param resourceType 资源类型 - * @param file (optional) 资源文件 - * @return 添加结果 - */ - addExamResource(examId: string, resourceType: string, file: FileParameter | undefined, cancelToken?: CancelToken): Promise { - let url_ = this.baseUrl + "/api/Exam/{examId}/resources/{resourceType}"; - if (examId === undefined || examId === null) - throw new Error("The parameter 'examId' must be defined."); - url_ = url_.replace("{examId}", encodeURIComponent("" + examId)); - if (resourceType === undefined || resourceType === null) - throw new Error("The parameter 'resourceType' must be defined."); - url_ = url_.replace("{resourceType}", encodeURIComponent("" + resourceType)); - url_ = url_.replace(/[?&]$/, ""); - - const content_ = new FormData(); - if (file === null || file === undefined) - throw new Error("The parameter 'file' cannot be null."); - else - content_.append("file", file.data, file.fileName ? file.fileName : "file"); - - let options_: AxiosRequestConfig = { - data: content_, - method: "POST", - url: url_, - headers: { - "Accept": "application/json" - }, - cancelToken - }; - - return this.instance.request(options_).catch((_error: any) => { - if (isAxiosError(_error) && _error.response) { - return _error.response; - } else { - throw _error; - } - }).then((_response: AxiosResponse) => { - return this.processAddExamResource(_response); - }); - } - - protected processAddExamResource(response: AxiosResponse): Promise { - const status = response.status; - let _headers: any = {}; - if (response.headers && typeof response.headers === "object") { - for (const k in response.headers) { - if (response.headers.hasOwnProperty(k)) { - _headers[k] = response.headers[k]; - } - } - } - if (status === 201) { - const _responseText = response.data; - let result201: any = null; - let resultData201 = _responseText; - result201 = ResourceInfo.fromJS(resultData201); - return Promise.resolve(result201); - - } else if (status === 400) { - const _responseText = response.data; - let result400: any = null; - let resultData400 = _responseText; - result400 = ProblemDetails.fromJS(resultData400); - return throwException("A server side error occurred.", status, _responseText, _headers, result400); - - } else if (status === 401) { - const _responseText = response.data; - let result401: any = null; - let resultData401 = _responseText; - result401 = ProblemDetails.fromJS(resultData401); - return throwException("A server side error occurred.", status, _responseText, _headers, result401); - - } else if (status === 403) { - const _responseText = response.data; - let result403: any = null; - let resultData403 = _responseText; - result403 = ProblemDetails.fromJS(resultData403); - return throwException("A server side error occurred.", status, _responseText, _headers, result403); - - } else if (status === 404) { - const _responseText = response.data; - let result404: any = null; - let resultData404 = _responseText; - result404 = ProblemDetails.fromJS(resultData404); - return throwException("A server side error occurred.", status, _responseText, _headers, result404); - - } else if (status === 409) { - const _responseText = response.data; - let result409: any = null; - let resultData409 = _responseText; - result409 = ProblemDetails.fromJS(resultData409); - return throwException("A server side error occurred.", status, _responseText, _headers, result409); - - } else if (status === 500) { - const _responseText = response.data; - return throwException("A server side error occurred.", status, _responseText, _headers); - - } else if (status !== 200 && status !== 204) { - const _responseText = response.data; - return throwException("An unexpected server error occurred.", status, _responseText, _headers); - } - return Promise.resolve(null as any); - } - - /** - * 获取指定实验ID的指定资源类型的所有资源的ID和名称 - * @param examId 实验ID - * @param resourceType 资源类型 - * @return 资源列表 - */ - getExamResourceList(examId: string, resourceType: string, cancelToken?: CancelToken): Promise { - let url_ = this.baseUrl + "/api/Exam/{examId}/resources/{resourceType}"; - if (examId === undefined || examId === null) - throw new Error("The parameter 'examId' must be defined."); - url_ = url_.replace("{examId}", encodeURIComponent("" + examId)); - if (resourceType === undefined || resourceType === null) - throw new Error("The parameter 'resourceType' must be defined."); - url_ = url_.replace("{resourceType}", encodeURIComponent("" + resourceType)); - url_ = url_.replace(/[?&]$/, ""); - - let options_: AxiosRequestConfig = { - method: "GET", - url: url_, - headers: { - "Accept": "application/json" - }, - cancelToken - }; - - return this.instance.request(options_).catch((_error: any) => { - if (isAxiosError(_error) && _error.response) { - return _error.response; - } else { - throw _error; - } - }).then((_response: AxiosResponse) => { - return this.processGetExamResourceList(_response); - }); - } - - protected processGetExamResourceList(response: AxiosResponse): Promise { - const status = response.status; - let _headers: any = {}; - if (response.headers && typeof response.headers === "object") { - for (const k in response.headers) { - if (response.headers.hasOwnProperty(k)) { - _headers[k] = response.headers[k]; - } - } - } - if (status === 200) { - const _responseText = response.data; - let result200: any = null; - let resultData200 = _responseText; - if (Array.isArray(resultData200)) { - result200 = [] as any; - for (let item of resultData200) - result200!.push(ResourceInfo.fromJS(item)); - } - else { - result200 = null; - } - return Promise.resolve(result200); - - } else if (status === 400) { - const _responseText = response.data; - let result400: any = null; - let resultData400 = _responseText; - result400 = ProblemDetails.fromJS(resultData400); - return throwException("A server side error occurred.", status, _responseText, _headers, result400); - - } else if (status === 401) { - const _responseText = response.data; - let result401: any = null; - let resultData401 = _responseText; - result401 = ProblemDetails.fromJS(resultData401); - return throwException("A server side error occurred.", status, _responseText, _headers, result401); - - } else if (status === 500) { - const _responseText = response.data; - return throwException("A server side error occurred.", status, _responseText, _headers); - - } else if (status !== 200 && status !== 204) { - const _responseText = response.data; - return throwException("An unexpected server error occurred.", status, _responseText, _headers); - } - return Promise.resolve(null as any); - } - - /** - * 根据资源ID下载资源 - * @param resourceId 资源ID - * @return 资源文件 - */ - getExamResourceById(resourceId: number, cancelToken?: CancelToken): Promise { - let url_ = this.baseUrl + "/api/Exam/resources/{resourceId}"; - if (resourceId === undefined || resourceId === null) - throw new Error("The parameter 'resourceId' must be defined."); - url_ = url_.replace("{resourceId}", encodeURIComponent("" + resourceId)); - url_ = url_.replace(/[?&]$/, ""); - - let options_: AxiosRequestConfig = { - responseType: "blob", - method: "GET", - url: url_, - headers: { - "Accept": "application/json" - }, - cancelToken - }; - - return this.instance.request(options_).catch((_error: any) => { - if (isAxiosError(_error) && _error.response) { - return _error.response; - } else { - throw _error; - } - }).then((_response: AxiosResponse) => { - return this.processGetExamResourceById(_response); - }); - } - - protected processGetExamResourceById(response: AxiosResponse): Promise { - const status = response.status; - let _headers: any = {}; - if (response.headers && typeof response.headers === "object") { - for (const k in response.headers) { - if (response.headers.hasOwnProperty(k)) { - _headers[k] = response.headers[k]; - } - } - } - if (status === 200 || status === 206) { - const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined; - let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; - let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; - if (fileName) { - fileName = decodeURIComponent(fileName); - } else { - fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; - fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; - } - return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers }); - } else if (status === 400) { - const _responseText = response.data; - let result400: any = null; - let resultData400 = _responseText; - result400 = ProblemDetails.fromJS(resultData400); - return throwException("A server side error occurred.", status, _responseText, _headers, result400); - - } else if (status === 404) { - const _responseText = response.data; - let result404: any = null; - let resultData404 = _responseText; - result404 = ProblemDetails.fromJS(resultData404); - return throwException("A server side error occurred.", status, _responseText, _headers, result404); - - } else if (status === 500) { - const _responseText = response.data; - return throwException("A server side error occurred.", status, _responseText, _headers); - - } else if (status !== 200 && status !== 204) { - const _responseText = response.data; - return throwException("An unexpected server error occurred.", status, _responseText, _headers); - } - return Promise.resolve(null as any); - } } export class JtagClient { @@ -3427,98 +3156,14 @@ export class JtagClient { return Promise.resolve(null as any); } - /** - * 上传比特流文件到服务器 - * @param address (optional) 目标设备地址 - * @param file (optional) 比特流文件 - * @return 上传结果 - */ - uploadBitstream(address: string | undefined, file: FileParameter | undefined, cancelToken?: CancelToken): Promise { - let url_ = this.baseUrl + "/api/Jtag/UploadBitstream?"; - if (address === null) - throw new Error("The parameter 'address' cannot be null."); - else if (address !== undefined) - url_ += "address=" + encodeURIComponent("" + address) + "&"; - url_ = url_.replace(/[?&]$/, ""); - - const content_ = new FormData(); - if (file === null || file === undefined) - throw new Error("The parameter 'file' cannot be null."); - else - content_.append("file", file.data, file.fileName ? file.fileName : "file"); - - let options_: AxiosRequestConfig = { - data: content_, - method: "POST", - url: url_, - headers: { - "Accept": "application/json" - }, - cancelToken - }; - - return this.instance.request(options_).catch((_error: any) => { - if (isAxiosError(_error) && _error.response) { - return _error.response; - } else { - throw _error; - } - }).then((_response: AxiosResponse) => { - return this.processUploadBitstream(_response); - }); - } - - protected processUploadBitstream(response: AxiosResponse): Promise { - const status = response.status; - let _headers: any = {}; - if (response.headers && typeof response.headers === "object") { - for (const k in response.headers) { - if (response.headers.hasOwnProperty(k)) { - _headers[k] = response.headers[k]; - } - } - } - if (status === 200) { - const _responseText = response.data; - let result200: any = null; - let resultData200 = _responseText; - result200 = resultData200 !== undefined ? resultData200 : null; - - return Promise.resolve(result200); - - } else if (status === 400) { - const _responseText = response.data; - let result400: any = null; - let resultData400 = _responseText; - result400 = resultData400 !== undefined ? resultData400 : null; - - return throwException("A server side error occurred.", status, _responseText, _headers, result400); - - } else if (status === 401) { - const _responseText = response.data; - let result401: any = null; - let resultData401 = _responseText; - result401 = ProblemDetails.fromJS(resultData401); - return throwException("A server side error occurred.", status, _responseText, _headers, result401); - - } else if (status === 500) { - const _responseText = response.data; - return throwException("A server side error occurred.", status, _responseText, _headers); - - } else if (status !== 200 && status !== 204) { - const _responseText = response.data; - return throwException("An unexpected server error occurred.", status, _responseText, _headers); - } - return Promise.resolve(null as any); - } - /** * 通过 JTAG 下载比特流文件到 FPGA 设备 * @param address (optional) JTAG 设备地址 * @param port (optional) JTAG 设备端口 + * @param bitstreamId (optional) 比特流ID * @return 下载结果 */ - downloadBitstream(address: string | undefined, port: number | undefined, cancelToken?: CancelToken): Promise { + downloadBitstream(address: string | undefined, port: number | undefined, bitstreamId: number | undefined, cancelToken?: CancelToken): Promise { let url_ = this.baseUrl + "/api/Jtag/DownloadBitstream?"; if (address === null) throw new Error("The parameter 'address' cannot be null."); @@ -3528,6 +3173,10 @@ export class JtagClient { throw new Error("The parameter 'port' cannot be null."); else if (port !== undefined) url_ += "port=" + encodeURIComponent("" + port) + "&"; + if (bitstreamId === null) + throw new Error("The parameter 'bitstreamId' cannot be null."); + else if (bitstreamId !== undefined) + url_ += "bitstreamId=" + encodeURIComponent("" + bitstreamId) + "&"; url_ = url_.replace(/[?&]$/, ""); let options_: AxiosRequestConfig = { @@ -6565,6 +6214,353 @@ export class RemoteUpdateClient { } } +export class ResourceClient { + protected instance: AxiosInstance; + protected baseUrl: string; + protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined; + + constructor(baseUrl?: string, instance?: AxiosInstance) { + + this.instance = instance || axios.create(); + + this.baseUrl = baseUrl ?? "http://127.0.0.1:5000"; + + } + + /** + * 添加资源(文件上传) + * @param resourceType (optional) 资源类型 + * @param resourcePurpose (optional) 资源用途(template/user) + * @param examID (optional) 所属实验ID(可选) + * @param file (optional) 资源文件 + * @return 添加结果 + */ + addResource(resourceType: string | undefined, resourcePurpose: string | undefined, examID: string | null | undefined, file: FileParameter | undefined, cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/Resource"; + url_ = url_.replace(/[?&]$/, ""); + + const content_ = new FormData(); + if (resourceType === null || resourceType === undefined) + throw new Error("The parameter 'resourceType' cannot be null."); + else + content_.append("ResourceType", resourceType.toString()); + if (resourcePurpose === null || resourcePurpose === undefined) + throw new Error("The parameter 'resourcePurpose' cannot be null."); + else + content_.append("ResourcePurpose", resourcePurpose.toString()); + if (examID !== null && examID !== undefined) + content_.append("ExamID", examID.toString()); + if (file === null || file === undefined) + throw new Error("The parameter 'file' cannot be null."); + else + content_.append("file", file.data, file.fileName ? file.fileName : "file"); + + let options_: AxiosRequestConfig = { + data: content_, + method: "POST", + url: url_, + headers: { + "Accept": "application/json" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processAddResource(_response); + }); + } + + protected processAddResource(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 201) { + const _responseText = response.data; + let result201: any = null; + let resultData201 = _responseText; + result201 = ResourceInfo.fromJS(resultData201); + return Promise.resolve(result201); + + } else if (status === 400) { + const _responseText = response.data; + let result400: any = null; + let resultData400 = _responseText; + result400 = ProblemDetails.fromJS(resultData400); + return throwException("A server side error occurred.", status, _responseText, _headers, result400); + + } else if (status === 401) { + const _responseText = response.data; + let result401: any = null; + let resultData401 = _responseText; + result401 = ProblemDetails.fromJS(resultData401); + return throwException("A server side error occurred.", status, _responseText, _headers, result401); + + } else if (status === 404) { + const _responseText = response.data; + let result404: any = null; + let resultData404 = _responseText; + result404 = ProblemDetails.fromJS(resultData404); + return throwException("A server side error occurred.", status, _responseText, _headers, result404); + + } else if (status === 500) { + const _responseText = response.data; + return throwException("A server side error occurred.", status, _responseText, _headers); + + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + + /** + * 获取资源列表 + * @param examId (optional) 实验ID(可选) + * @param resourceType (optional) 资源类型(可选) + * @param resourcePurpose (optional) 资源用途(可选) + * @return 资源列表 + */ + getResourceList(examId: string | null | undefined, resourceType: string | null | undefined, resourcePurpose: string | null | undefined, cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/Resource?"; + if (examId !== undefined && examId !== null) + url_ += "examId=" + encodeURIComponent("" + examId) + "&"; + if (resourceType !== undefined && resourceType !== null) + url_ += "resourceType=" + encodeURIComponent("" + resourceType) + "&"; + if (resourcePurpose !== undefined && resourcePurpose !== null) + url_ += "resourcePurpose=" + encodeURIComponent("" + resourcePurpose) + "&"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: AxiosRequestConfig = { + method: "GET", + url: url_, + headers: { + "Accept": "application/json" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processGetResourceList(_response); + }); + } + + protected processGetResourceList(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200) { + const _responseText = response.data; + let result200: any = null; + let resultData200 = _responseText; + if (Array.isArray(resultData200)) { + result200 = [] as any; + for (let item of resultData200) + result200!.push(ResourceInfo.fromJS(item)); + } + else { + result200 = null; + } + return Promise.resolve(result200); + + } else if (status === 401) { + const _responseText = response.data; + let result401: any = null; + let resultData401 = _responseText; + result401 = ProblemDetails.fromJS(resultData401); + return throwException("A server side error occurred.", status, _responseText, _headers, result401); + + } else if (status === 500) { + const _responseText = response.data; + return throwException("A server side error occurred.", status, _responseText, _headers); + + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + + /** + * 根据资源ID下载资源 + * @param resourceId 资源ID + * @return 资源文件 + */ + getResourceById(resourceId: number, cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/Resource/{resourceId}"; + if (resourceId === undefined || resourceId === null) + throw new Error("The parameter 'resourceId' must be defined."); + url_ = url_.replace("{resourceId}", encodeURIComponent("" + resourceId)); + url_ = url_.replace(/[?&]$/, ""); + + let options_: AxiosRequestConfig = { + responseType: "blob", + method: "GET", + url: url_, + headers: { + "Accept": "application/json" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processGetResourceById(_response); + }); + } + + protected processGetResourceById(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers }); + } else if (status === 400) { + const _responseText = response.data; + let result400: any = null; + let resultData400 = _responseText; + result400 = ProblemDetails.fromJS(resultData400); + return throwException("A server side error occurred.", status, _responseText, _headers, result400); + + } else if (status === 404) { + const _responseText = response.data; + let result404: any = null; + let resultData404 = _responseText; + result404 = ProblemDetails.fromJS(resultData404); + return throwException("A server side error occurred.", status, _responseText, _headers, result404); + + } else if (status === 500) { + const _responseText = response.data; + return throwException("A server side error occurred.", status, _responseText, _headers); + + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + + /** + * 删除资源 + * @param resourceId 资源ID + * @return 删除结果 + */ + deleteResource(resourceId: number, cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/Resource/{resourceId}"; + if (resourceId === undefined || resourceId === null) + throw new Error("The parameter 'resourceId' must be defined."); + url_ = url_.replace("{resourceId}", encodeURIComponent("" + resourceId)); + url_ = url_.replace(/[?&]$/, ""); + + let options_: AxiosRequestConfig = { + method: "DELETE", + url: url_, + headers: { + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processDeleteResource(_response); + }); + } + + protected processDeleteResource(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 204) { + const _responseText = response.data; + return Promise.resolve(null as any); + + } else if (status === 401) { + const _responseText = response.data; + let result401: any = null; + let resultData401 = _responseText; + result401 = ProblemDetails.fromJS(resultData401); + return throwException("A server side error occurred.", status, _responseText, _headers, result401); + + } else if (status === 403) { + const _responseText = response.data; + let result403: any = null; + let resultData403 = _responseText; + result403 = ProblemDetails.fromJS(resultData403); + return throwException("A server side error occurred.", status, _responseText, _headers, result403); + + } else if (status === 404) { + const _responseText = response.data; + let result404: any = null; + let resultData404 = _responseText; + result404 = ProblemDetails.fromJS(resultData404); + return throwException("A server side error occurred.", status, _responseText, _headers, result404); + + } else if (status === 500) { + const _responseText = response.data; + return throwException("A server side error occurred.", status, _responseText, _headers); + + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } +} + export class TutorialClient { protected instance: AxiosInstance; protected baseUrl: string; @@ -8023,52 +8019,6 @@ export interface ICreateExamRequest { isVisibleToUsers: boolean; } -/** 资源信息类 */ -export class ResourceInfo implements IResourceInfo { - /** 资源ID */ - id!: number; - /** 资源名称 */ - name!: string; - - constructor(data?: IResourceInfo) { - if (data) { - for (var property in data) { - if (data.hasOwnProperty(property)) - (this)[property] = (data)[property]; - } - } - } - - init(_data?: any) { - if (_data) { - this.id = _data["id"]; - this.name = _data["name"]; - } - } - - static fromJS(data: any): ResourceInfo { - data = typeof data === 'object' ? data : {}; - let result = new ResourceInfo(); - result.init(data); - return result; - } - - toJSON(data?: any) { - data = typeof data === 'object' ? data : {}; - data["id"] = this.id; - data["name"] = this.name; - return data; - } -} - -/** 资源信息类 */ -export interface IResourceInfo { - /** 资源ID */ - id: number; - /** 资源名称 */ - name: string; -} - /** 逻辑分析仪运行状态枚举 */ export enum CaptureStatus { None = 0, @@ -8518,6 +8468,82 @@ export interface IOscilloscopeDataResponse { waveformData: string; } +/** 资源信息类 */ +export class ResourceInfo implements IResourceInfo { + /** 资源ID */ + id!: number; + /** 资源名称 */ + name!: string; + /** 资源类型 */ + type!: string; + /** 资源用途(template/user) */ + purpose!: string; + /** 上传时间 */ + uploadTime!: Date; + /** 所属实验ID(可选) */ + examID?: string | undefined; + /** MIME类型 */ + mimeType?: string | undefined; + + constructor(data?: IResourceInfo) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data?: any) { + if (_data) { + this.id = _data["id"]; + this.name = _data["name"]; + this.type = _data["type"]; + this.purpose = _data["purpose"]; + this.uploadTime = _data["uploadTime"] ? new Date(_data["uploadTime"].toString()) : undefined; + this.examID = _data["examID"]; + this.mimeType = _data["mimeType"]; + } + } + + static fromJS(data: any): ResourceInfo { + data = typeof data === 'object' ? data : {}; + let result = new ResourceInfo(); + result.init(data); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["id"] = this.id; + data["name"] = this.name; + data["type"] = this.type; + data["purpose"] = this.purpose; + data["uploadTime"] = this.uploadTime ? this.uploadTime.toISOString() : undefined; + data["examID"] = this.examID; + data["mimeType"] = this.mimeType; + return data; + } +} + +/** 资源信息类 */ +export interface IResourceInfo { + /** 资源ID */ + id: number; + /** 资源名称 */ + name: string; + /** 资源类型 */ + type: string; + /** 资源用途(template/user) */ + purpose: string; + /** 上传时间 */ + uploadTime: Date; + /** 所属实验ID(可选) */ + examID?: string | undefined; + /** MIME类型 */ + mimeType?: string | undefined; +} + /** Package options which to send address to read or write */ export class SendAddrPackOptions implements ISendAddrPackOptions { /** 突发类型 */ diff --git a/src/components/LabCanvas/composable/diagramManager.ts b/src/components/LabCanvas/composable/diagramManager.ts index 42d078e..a03403b 100644 --- a/src/components/LabCanvas/composable/diagramManager.ts +++ b/src/components/LabCanvas/composable/diagramManager.ts @@ -88,17 +88,17 @@ export async function loadDiagramData(examId?: string): Promise { // 如果提供了examId,优先从API加载实验的diagram if (examId) { try { - const examClient = AuthManager.createAuthenticatedExamClient(); + const resourceClient = AuthManager.createAuthenticatedResourceClient(); // 获取diagram类型的资源列表 - const resources = await examClient.getExamResourceList(examId, 'canvas'); + const resources = await resourceClient.getResourceList(examId, 'canvas', 'template'); if (resources && resources.length > 0) { // 获取第一个diagram资源 const diagramResource = resources[0]; // 使用动态API获取资源文件内容 - const response = await examClient.getExamResourceById(diagramResource.id); + const response = await resourceClient.getResourceById(diagramResource.id); if (response && response.data) { const text = await response.data.text(); diff --git a/src/components/MarkdownRenderer.vue b/src/components/MarkdownRenderer.vue index 5f5b9f3..43255b3 100644 --- a/src/components/MarkdownRenderer.vue +++ b/src/components/MarkdownRenderer.vue @@ -6,8 +6,6 @@ import hljs from 'highlight.js'; import 'highlight.js/styles/github.css'; // 亮色主题 // 导入主题存储 import { useThemeStore } from '@/stores/theme'; -// 导入ExamClient用于获取图片资源 -import { ExamClient } from '@/APIClient'; import { AuthManager } from '@/utils/AuthManager'; const props = defineProps({ @@ -36,8 +34,8 @@ const imageResourceCache = ref>(new Map()); // 获取图片资源ID的函数 async function getImageResourceId(examId: string, imagePath: string): Promise { try { - const client = AuthManager.createAuthenticatedExamClient(); - const resources = await client.getExamResourceList(examId, 'images'); + const client = AuthManager.createAuthenticatedResourceClient(); + const resources = await client.getResourceList(examId, 'images', 'template'); // 查找匹配的图片资源 const imageResource = resources.find(r => r.name === imagePath || r.name.endsWith(imagePath)); @@ -52,8 +50,8 @@ async function getImageResourceId(examId: string, imagePath: string): Promise { try { - const client = AuthManager.createAuthenticatedExamClient(); - const response = await client.getExamResourceById(parseInt(resourceId)); + const client = AuthManager.createAuthenticatedResourceClient(); + const response = await client.getResourceById(parseInt(resourceId)); if (response && response.data) { return URL.createObjectURL(response.data); diff --git a/src/components/TutorialCarousel.vue b/src/components/TutorialCarousel.vue index 30fbab7..83511b7 100644 --- a/src/components/TutorialCarousel.vue +++ b/src/components/TutorialCarousel.vue @@ -127,12 +127,13 @@ onMounted(async () => { let thumbnail: string | undefined; try { - // 获取实验的封面资源 - const resourceList = await client.getExamResourceList(exam.id, 'cover'); + // 获取实验的封面资源(模板资源) + const resourceClient = AuthManager.createAuthenticatedResourceClient(); + const resourceList = await resourceClient.getResourceList(exam.id, 'cover', 'template'); if (resourceList && resourceList.length > 0) { // 使用第一个封面资源 const coverResource = resourceList[0]; - const fileResponse = await client.getExamResourceById(coverResource.id); + const fileResponse = await resourceClient.getResourceById(coverResource.id); // 创建Blob URL作为缩略图 thumbnail = URL.createObjectURL(fileResponse.data); } diff --git a/src/components/UploadCard.vue b/src/components/UploadCard.vue index ba7b559..8ad92ce 100644 --- a/src/components/UploadCard.vue +++ b/src/components/UploadCard.vue @@ -75,8 +75,8 @@ import { useDialogStore } from "@/stores/dialog"; import { isNull, isUndefined } from "lodash"; interface Props { - uploadEvent?: (file: File) => Promise; - downloadEvent?: () => Promise; + uploadEvent?: (file: File, examId: string) => Promise; + downloadEvent?: (bitstreamId: number) => Promise; maxMemory?: number; examId?: string; // 新增examId属性 } @@ -127,9 +127,9 @@ async function loadAvailableBitstreams() { } try { - const examClient = AuthManager.createAuthenticatedExamClient(); - // 使用新的API获取比特流资源列表 - const resources = await examClient.getExamResourceList(props.examId, 'bitstream'); + const resourceClient = AuthManager.createAuthenticatedResourceClient(); + // 使用新的ResourceClient API获取比特流模板资源列表 + const resources = await resourceClient.getResourceList(props.examId, 'bitstream', 'template'); availableBitstreams.value = resources.map(r => ({ id: r.id, name: r.name })) || []; } catch (error) { console.error('加载比特流列表失败:', error); @@ -143,10 +143,10 @@ async function downloadExampleBitstream(bitstream: {id: number, name: string}) { isDownloading.value = true; try { - const examClient = AuthManager.createAuthenticatedExamClient(); + const resourceClient = AuthManager.createAuthenticatedResourceClient(); - // 使用动态API获取资源文件 - const response = await examClient.getExamResourceById(bitstream.id); + // 使用新的ResourceClient API获取资源文件 + const response = await resourceClient.getResourceById(bitstream.id); if (response && response.data) { // 创建下载链接 @@ -173,37 +173,21 @@ async function downloadExampleBitstream(bitstream: {id: number, name: string}) { // 直接烧录示例比特流 async function programExampleBitstream(bitstream: {id: number, name: string}) { - if (isProgramming.value || !props.uploadEvent) return; + if (isProgramming.value) return; isProgramming.value = true; try { - const examClient = AuthManager.createAuthenticatedExamClient(); - - // 使用动态API获取比特流文件数据 - const response = await examClient.getExamResourceById(bitstream.id); - - if (!response || !response.data) { - throw new Error('获取比特流文件失败'); - } - - const file = new File([response.data], response.fileName || bitstream.name, { type: response.data.type }); - - // 调用上传事件 - const uploadSuccess = await props.uploadEvent(file); - if (uploadSuccess) { - // 如果有下载事件(烧录),则执行 - if (props.downloadEvent) { - const downloadSuccess = await props.downloadEvent(); - if (downloadSuccess) { - dialog.info("示例比特流烧录成功"); - } else { - dialog.error("烧录失败"); - } + const resourceClient = AuthManager.createAuthenticatedResourceClient(); + + if (props.downloadEvent) { + const downloadSuccess = await props.downloadEvent(bitstream.id); + if (downloadSuccess) { + dialog.info("示例比特流烧录成功"); } else { - dialog.info("示例比特流上传成功"); + dialog.error("烧录失败"); } } else { - dialog.error("上传失败"); + dialog.info("示例比特流props.downloadEvent未定义 无法烧录"); } } catch (error) { console.error('烧录示例比特流失败:', error); @@ -234,6 +218,7 @@ function checkFile(file: File): boolean { } async function handleClick(event: Event): Promise { + console.log("上传按钮被点击"); if (isNull(bitstream.value) || isUndefined(bitstream.value)) { dialog.error(`未选择文件`); return; @@ -246,19 +231,21 @@ async function handleClick(event: Event): Promise { } isUploading.value = true; + let uploadedBitstreamId: number | null = null; try { - const ret = await props.uploadEvent(bitstream.value); + console.log("开始上传比特流文件:", bitstream.value.name); + const bitstreamId = await props.uploadEvent(bitstream.value, props.examId || ''); + console.log("上传结果,ID:", bitstreamId); if (isUndefined(props.downloadEvent)) { - if (ret) { - dialog.info("上传成功"); - emits("finishedUpload", bitstream.value); - } else dialog.error("上传失败"); - return; - } - if (!ret) { + console.log("上传成功,下载未定义"); isUploading.value = false; return; } + if (bitstreamId === null || bitstreamId === undefined) { + isUploading.value = false; + return; + } + uploadedBitstreamId = bitstreamId; } catch (e) { dialog.error("上传失败"); console.error(e); @@ -267,9 +254,14 @@ async function handleClick(event: Event): Promise { // Download try { - const ret = await props.downloadEvent(); - if (ret) dialog.info("下载成功"); - else dialog.error("下载失败"); + console.log("开始下载比特流,ID:", uploadedBitstreamId); + if (uploadedBitstreamId === null || uploadedBitstreamId === undefined) { + dialog.error("uploadedBitstreamId is null or undefined"); + } else { + const ret = await props.downloadEvent(uploadedBitstreamId); + if (ret) dialog.info("下载成功"); + else dialog.error("下载失败"); + } } catch (e) { dialog.error("下载失败"); console.error(e); diff --git a/src/components/equipments/MotherBoardCaps.vue b/src/components/equipments/MotherBoardCaps.vue index 9093ac6..7bcd18a 100644 --- a/src/components/equipments/MotherBoardCaps.vue +++ b/src/components/equipments/MotherBoardCaps.vue @@ -22,8 +22,9 @@
@@ -127,6 +128,11 @@ function handleBitstreamChange(file: File | undefined) { eqps.jtagBitstream = file; } +async function handleDownloadBitstream(bitstreamId: number): Promise { + console.log("开始下载比特流,ID:", bitstreamId); + return await eqps.jtagDownloadBitstream(bitstreamId); +} + function handleSelectJtagSpeed(event: Event) { const target = event.target as HTMLSelectElement; eqps.jtagSetSpeed(target.selectedIndex); diff --git a/src/stores/equipments.ts b/src/stores/equipments.ts index 1d3a19d..bf20d0a 100644 --- a/src/stores/equipments.ts +++ b/src/stores/equipments.ts @@ -11,6 +11,7 @@ import { toFileParameterOrUndefined } from "@/utils/Common"; import { AuthManager } from "@/utils/AuthManager"; import { HubConnectionBuilder } from "@microsoft/signalr"; import { getHubProxyFactory, getReceiverRegister } from "@/TypedSignalR.Client"; +import type { ResourceInfo } from "@/APIClient"; export const useEquipments = defineStore("equipments", () => { // Global Stores @@ -23,6 +24,7 @@ export const useEquipments = defineStore("equipments", () => { // Jtag const jtagBitstream = ref(); const jtagBoundaryScanFreq = ref(100); + const jtagUserBitstreams = ref([]); const jtagClientMutex = withTimeout( new Mutex(), 1000, @@ -96,25 +98,38 @@ export const useEquipments = defineStore("equipments", () => { } } - async function jtagUploadBitstream(bitstream: File): Promise { + async function jtagUploadBitstream(bitstream: File, examId?: string): Promise { try { // 自动开启电源 await powerSetOnOff(true); - const jtagClient = AuthManager.createAuthenticatedJtagClient(); - const resp = await jtagClient.uploadBitstream( - boardAddr.value, + const resourceClient = AuthManager.createAuthenticatedResourceClient(); + const resp = await resourceClient.addResource( + 'bitstream', + 'user', + examId || null, toFileParameterOrUndefined(bitstream), ); - return resp; + + // 如果上传成功,设置为当前选中的比特流 + if (resp && resp.id !== undefined && resp.id !== null) { + return resp.id; + } + + return null; } catch (e) { dialog.error("上传错误"); console.error(e); - return false; + return null; } } - async function jtagDownloadBitstream(): Promise { + async function jtagDownloadBitstream(bitstreamId?: number): Promise { + if (bitstreamId === null || bitstreamId === undefined) { + dialog.error("请先选择要下载的比特流"); + return false; + } + const release = await jtagClientMutex.acquire(); try { // 自动开启电源 @@ -124,10 +139,11 @@ export const useEquipments = defineStore("equipments", () => { const resp = await jtagClient.downloadBitstream( boardAddr.value, boardPort.value, + bitstreamId, ); return resp; } catch (e) { - dialog.error("上传错误"); + dialog.error("下载错误"); console.error(e); return false; } finally { @@ -256,6 +272,7 @@ export const useEquipments = defineStore("equipments", () => { jtagBoundaryScanSetOnOff, jtagBitstream, jtagBoundaryScanFreq, + jtagUserBitstreams, jtagUploadBitstream, jtagDownloadBitstream, jtagGetIDCode, diff --git a/src/utils/AuthManager.ts b/src/utils/AuthManager.ts index 25750ae..a0b9f10 100644 --- a/src/utils/AuthManager.ts +++ b/src/utils/AuthManager.ts @@ -14,6 +14,7 @@ import { OscilloscopeApiClient, DebuggerClient, ExamClient, + ResourceClient, } from "@/APIClient"; import axios, { type AxiosInstance } from "axios"; @@ -33,7 +34,8 @@ type SupportedClient = | NetConfigClient | OscilloscopeApiClient | DebuggerClient - | ExamClient; + | ExamClient + | ResourceClient; export class AuthManager { // 存储token到localStorage @@ -196,6 +198,10 @@ export class AuthManager { return AuthManager.createAuthenticatedClient(ExamClient); } + public static createAuthenticatedResourceClient(): ResourceClient { + return AuthManager.createAuthenticatedClient(ResourceClient); + } + // 登录函数 public static async login( username: string, diff --git a/src/views/ExamView.vue b/src/views/ExamView.vue index 1b349b7..9e56614 100644 --- a/src/views/ExamView.vue +++ b/src/views/ExamView.vue @@ -679,15 +679,15 @@ const downloadResources = async () => { downloadingResources.value = true try { - const client = AuthManager.createAuthenticatedExamClient() + const resourceClient = AuthManager.createAuthenticatedResourceClient() - // 获取资源包列表 - const resourceList = await client.getExamResourceList(selectedExam.value.id, 'resource') + // 获取资源包列表(模板资源) + const resourceList = await resourceClient.getResourceList(selectedExam.value.id, 'resource', 'template') if (resourceList && resourceList.length > 0) { - // 使用动态API获取第一个资源包 + // 使用新的ResourceClient API获取第一个资源包 const resourceId = resourceList[0].id - const fileResponse = await client.getExamResourceById(resourceId) + const fileResponse = await resourceClient.getResourceById(resourceId) // 创建Blob URL const blobUrl = URL.createObjectURL(fileResponse.data) @@ -925,7 +925,7 @@ const submitCreateExam = async () => { // 上传实验资源 const uploadExamResources = async (examId: string) => { - const client = AuthManager.createAuthenticatedExamClient() + const client = AuthManager.createAuthenticatedResourceClient() try { // 上传MD文档 @@ -934,7 +934,7 @@ const uploadExamResources = async (examId: string) => { data: uploadFiles.value.mdFile, fileName: uploadFiles.value.mdFile.name } - await client.addExamResource(examId, 'doc', mdFileParam) + await client.addResource('doc', 'template', examId, mdFileParam) console.log('MD文档上传成功') } @@ -944,7 +944,7 @@ const uploadExamResources = async (examId: string) => { data: imageFile, fileName: imageFile.name } - await client.addExamResource(examId, 'image', imageFileParam) + await client.addResource('image', 'template', examId, imageFileParam) console.log('图片上传成功:', imageFile.name) } @@ -954,7 +954,7 @@ const uploadExamResources = async (examId: string) => { data: bitstreamFile, fileName: bitstreamFile.name } - await client.addExamResource(examId, 'bitstream', bitstreamFileParam) + await client.addResource('bitstream', 'template', examId, bitstreamFileParam) console.log('比特流文件上传成功:', bitstreamFile.name) } @@ -964,7 +964,7 @@ const uploadExamResources = async (examId: string) => { data: canvasFile, fileName: canvasFile.name } - await client.addExamResource(examId, 'canvas', canvasFileParam) + await client.addResource('canvas', 'template', examId, canvasFileParam) console.log('画布模板上传成功:', canvasFile.name) } @@ -974,7 +974,7 @@ const uploadExamResources = async (examId: string) => { data: uploadFiles.value.resourceFile, fileName: uploadFiles.value.resourceFile.name } - await client.addExamResource(examId, 'resource', resourceFileParam) + await client.addResource('resource', 'template', examId, resourceFileParam) console.log('资源包上传成功') } diff --git a/src/views/Project/Index.vue b/src/views/Project/Index.vue index 2e30907..f03b087 100644 --- a/src/views/Project/Index.vue +++ b/src/views/Project/Index.vue @@ -37,7 +37,7 @@ @@ -217,17 +217,17 @@ async function loadDocumentContent() { if (examId) { // 如果有实验ID,从API加载实验文档 console.log('加载实验文档:', examId); - const client = AuthManager.createAuthenticatedExamClient(); + const client = AuthManager.createAuthenticatedResourceClient(); - // 获取markdown类型的资源列表 - const resources = await client.getExamResourceList(examId, 'doc'); + // 获取markdown类型的模板资源列表 + const resources = await client.getResourceList(examId, 'doc', 'template'); if (resources && resources.length > 0) { // 获取第一个markdown资源 const markdownResource = resources[0]; - // 使用动态API获取资源文件内容 - const response = await client.getExamResourceById(markdownResource.id); + // 使用新的ResourceClient API获取资源文件内容 + const response = await client.getResourceById(markdownResource.id); if (!response || !response.data) { throw new Error('获取markdown文件失败');