From c56484467315d267e8f5454c88cfb040d8995b4c Mon Sep 17 00:00:00 2001 From: alivender <13898766233@163.com> Date: Fri, 1 Aug 2025 12:57:30 +0800 Subject: [PATCH] =?UTF-8?q?add:=20=E6=B7=BB=E5=8A=A0=E5=AE=9E=E9=AA=8C?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E7=95=8C=E9=9D=A2=EF=BC=8C=E5=AE=9E=E9=AA=8C?= =?UTF-8?q?=E5=A2=9E=E5=88=A0=E5=AE=8C=E5=85=A8=E4=BE=9D=E8=B5=96=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/Program.cs | 24 +- server/exam/EXP001/doc.md | 37 - server/exam/EXP002/doc.md | 35 - server/src/Controllers/ExamController.cs | 307 +++- server/src/Database.cs | 480 +++++- src/APIClient.ts | 599 +++++-- src/components/Alert/Alert.vue | 2 +- src/components/LabCanvas/DiagramCanvas.vue | 7 +- .../LabCanvas/composable/componentManager.ts | 4 + .../LabCanvas/composable/diagramManager.ts | 68 +- .../LogicAnalyzer/LogicAnalyzerManager.ts | 6 +- src/components/MarkdownRenderer.vue | 1412 +++++++++-------- src/components/TutorialCarousel.vue | 629 ++++---- src/components/UploadCard.vue | 161 +- src/components/equipments/MotherBoard.vue | 3 +- src/components/equipments/MotherBoardCaps.vue | 2 + src/views/ExamView.vue | 1329 +++++++++++----- src/views/HomeView.vue | 31 +- src/views/Project/Index.vue | 63 +- 19 files changed, 3511 insertions(+), 1688 deletions(-) delete mode 100644 server/exam/EXP001/doc.md delete mode 100644 server/exam/EXP002/doc.md diff --git a/server/Program.cs b/server/Program.cs index ef87db2..642519f 100644 --- a/server/Program.cs +++ b/server/Program.cs @@ -168,6 +168,17 @@ try FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "log")), RequestPath = "/log" }); + + // Exam Files (实验静态资源) + if (Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), "exam"))) + { + app.UseStaticFiles(new StaticFileOptions + { + FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "exam")), + RequestPath = "/exam" + }); + } + app.MapFallbackToFile("index.html"); } @@ -194,19 +205,6 @@ try // Setup Program MsgBus.Init(); - // 扫描并更新实验数据库 - try - { - using var db = new Database.AppDataConnection(); - var examFolderPath = Path.Combine(Directory.GetCurrentDirectory(), "exam"); - var updateCount = db.ScanAndUpdateExams(examFolderPath); - logger.Info($"实验数据库扫描完成,更新了 {updateCount} 个实验"); - } - catch (Exception ex) - { - logger.Error($"扫描实验文件夹时出错: {ex.Message}"); - } - // Generate API Client app.MapGet("GetAPIClientCode", async (HttpContext context) => { diff --git a/server/exam/EXP001/doc.md b/server/exam/EXP001/doc.md deleted file mode 100644 index ee33842..0000000 --- a/server/exam/EXP001/doc.md +++ /dev/null @@ -1,37 +0,0 @@ -# 实验001:基础逻辑门电路 - -## 实验目的 - -本实验旨在帮助学生理解基础逻辑门的工作原理,包括与门、或门、非门等基本逻辑运算。 - -## 实验内容 - -### 1. 与门(AND Gate) -与门是一个基本的逻辑门,当所有输入都为高电平(1)时,输出才为高电平(1)。 - -### 2. 或门(OR Gate) -或门是另一个基本的逻辑门,当任意一个输入为高电平(1)时,输出就为高电平(1)。 - -### 3. 非门(NOT Gate) -非门是一个反相器,输入为高电平时输出为低电平,反之亦然。 - -## 实验步骤 - -1. 打开 FPGA 开发环境 -2. 创建新的项目文件 -3. 编写 Verilog 代码实现各种逻辑门 -4. 进行仿真验证 -5. 下载到 FPGA 板进行硬件验证 - -## 预期结果 - -通过本实验,学生应该能够: -- 理解基本逻辑门的真值表 -- 掌握 Verilog 代码的基本语法 -- 学会使用 FPGA 开发工具进行仿真 - -## 注意事项 - -- 确保输入信号的电平正确 -- 注意时序的约束 -- 验证结果时要仔细对比真值表 diff --git a/server/exam/EXP002/doc.md b/server/exam/EXP002/doc.md deleted file mode 100644 index f318158..0000000 --- a/server/exam/EXP002/doc.md +++ /dev/null @@ -1,35 +0,0 @@ -# 实验002:组合逻辑电路设计 - -## 实验目的 - -本实验旨在让学生学习如何设计和实现复杂的组合逻辑电路,掌握多个逻辑门的组合使用。 - -## 实验内容 - -### 1. 半加器设计 -设计一个半加器电路,实现两个一位二进制数的加法运算。 - -### 2. 全加器设计 -在半加器的基础上,设计全加器电路,考虑进位输入。 - -### 3. 编码器和译码器 -实现简单的编码器和译码器电路。 - -## 实验要求 - -1. 使用 Verilog HDL 编写代码 -2. 绘制逻辑电路图 -3. 编写测试用例验证功能 -4. 分析电路的延时特性 - -## 评估标准 - -- 电路功能正确性 (40%) -- 代码质量和规范性 (30%) -- 测试覆盖率 (20%) -- 实验报告 (10%) - -## 参考资料 - -- 数字逻辑设计教材第3-4章 -- Verilog HDL 语法参考手册 diff --git a/server/src/Controllers/ExamController.cs b/server/src/Controllers/ExamController.cs index 5a434c7..cc3c540 100644 --- a/server/src/Controllers/ExamController.cs +++ b/server/src/Controllers/ExamController.cs @@ -25,9 +25,14 @@ public class ExamController : ControllerBase public required string ID { get; set; } /// - /// 实验文档内容(Markdown格式) + /// 实验名称 /// - public required string DocContent { get; set; } + public required string Name { get; set; } + + /// + /// 实验描述 + /// + public required string Description { get; set; } /// /// 实验创建时间 @@ -38,6 +43,21 @@ public class ExamController : ControllerBase /// 实验最后更新时间 /// public DateTime UpdatedTime { get; set; } + + /// + /// 实验标签 + /// + public string[] Tags { get; set; } = Array.Empty(); + + /// + /// 实验难度(1-5) + /// + public int Difficulty { get; set; } = 1; + + /// + /// 普通用户是否可见 + /// + public bool IsVisibleToUsers { get; set; } = true; } /// @@ -50,6 +70,11 @@ public class ExamController : ControllerBase /// public required string ID { get; set; } + /// + /// 实验名称 + /// + public required string Name { get; set; } + /// /// 实验创建时间 /// @@ -61,25 +86,71 @@ public class ExamController : ControllerBase public DateTime UpdatedTime { get; set; } /// - /// 实验标题(从文档内容中提取) + /// 实验标签 /// - public string Title { get; set; } = ""; + public string[] Tags { get; set; } = Array.Empty(); + + /// + /// 实验难度(1-5) + /// + public int Difficulty { get; set; } = 1; + + /// + /// 普通用户是否可见 + /// + public bool IsVisibleToUsers { get; set; } = true; } /// - /// 扫描结果类 + /// 资源信息类 /// - public class ScanResult + public class ResourceInfo { /// - /// 结果消息 + /// 资源ID /// - public required string Message { get; set; } + public int ID { get; set; } /// - /// 更新的实验数量 + /// 资源名称 /// - public int UpdateCount { get; set; } + public required string Name { get; set; } + } + + /// + /// 创建实验请求类 + /// + public class CreateExamRequest + { + /// + /// 实验ID + /// + public required string ID { get; set; } + + /// + /// 实验名称 + /// + public required string Name { get; set; } + + /// + /// 实验描述 + /// + public required string Description { get; set; } + + /// + /// 实验标签 + /// + public string[] Tags { get; set; } = Array.Empty(); + + /// + /// 实验难度(1-5) + /// + public int Difficulty { get; set; } = 1; + + /// + /// 普通用户是否可见 + /// + public bool IsVisibleToUsers { get; set; } = true; } /// @@ -102,9 +173,12 @@ public class ExamController : ControllerBase var examSummaries = exams.Select(exam => new ExamSummary { ID = exam.ID, + Name = exam.Name, CreatedTime = exam.CreatedTime, UpdatedTime = exam.UpdatedTime, - Title = ExtractTitleFromMarkdown(exam.DocContent) + Tags = exam.GetTagsList(), + Difficulty = exam.Difficulty, + IsVisibleToUsers = exam.IsVisibleToUsers }).ToArray(); logger.Info($"成功获取实验列表,共 {examSummaries.Length} 个实验"); @@ -156,9 +230,13 @@ public class ExamController : ControllerBase var examInfo = new ExamInfo { ID = exam.ID, - DocContent = exam.DocContent, + Name = exam.Name, + Description = exam.Description, CreatedTime = exam.CreatedTime, - UpdatedTime = exam.UpdatedTime + UpdatedTime = exam.UpdatedTime, + Tags = exam.GetTagsList(), + Difficulty = exam.Difficulty, + IsVisibleToUsers = exam.IsVisibleToUsers }; logger.Info($"成功获取实验信息: {examId}"); @@ -172,60 +250,205 @@ public class ExamController : ControllerBase } /// - /// 重新扫描实验文件夹并更新数据库 + /// 创建新实验 /// - /// 更新结果 + /// 创建实验请求 + /// 创建结果 [Authorize("Admin")] - [HttpPost("scan")] + [HttpPost] [EnableCors("Users")] - [ProducesResponseType(typeof(ScanResult), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ExamInfo), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status409Conflict)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public IActionResult ScanExams() + public IActionResult CreateExam([FromBody] CreateExamRequest request) { + if (string.IsNullOrWhiteSpace(request.ID) || string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.Description)) + return BadRequest("实验ID、名称和描述不能为空"); + try { using var db = new Database.AppDataConnection(); - var examFolderPath = Path.Combine(Directory.GetCurrentDirectory(), "exam"); - var updateCount = db.ScanAndUpdateExams(examFolderPath); + var result = db.CreateExam(request.ID, request.Name, request.Description, request.Tags, request.Difficulty, request.IsVisibleToUsers); - var result = new ScanResult + if (!result.IsSuccessful) { - Message = $"扫描完成,更新了 {updateCount} 个实验", - UpdateCount = updateCount + if (result.Error.Message.Contains("已存在")) + return Conflict(result.Error.Message); + + logger.Error($"创建实验时出错: {result.Error.Message}"); + return StatusCode(StatusCodes.Status500InternalServerError, $"创建实验失败: {result.Error.Message}"); + } + + var exam = result.Value; + var examInfo = new ExamInfo + { + ID = exam.ID, + Name = exam.Name, + Description = exam.Description, + CreatedTime = exam.CreatedTime, + UpdatedTime = exam.UpdatedTime, + Tags = exam.GetTagsList(), + Difficulty = exam.Difficulty, + IsVisibleToUsers = exam.IsVisibleToUsers }; - logger.Info($"手动扫描实验完成,更新了 {updateCount} 个实验"); - return Ok(result); + logger.Info($"成功创建实验: {request.ID}"); + return CreatedAtAction(nameof(GetExam), new { examId = request.ID }, examInfo); } catch (Exception ex) { - logger.Error($"扫描实验时出错: {ex.Message}"); - return StatusCode(StatusCodes.Status500InternalServerError, $"扫描实验失败: {ex.Message}"); + logger.Error($"创建实验 {request.ID} 时出错: {ex.Message}"); + return StatusCode(StatusCodes.Status500InternalServerError, $"创建实验失败: {ex.Message}"); } } /// - /// 从 Markdown 内容中提取标题 + /// 添加实验资源 /// - /// Markdown 内容 - /// 提取的标题 - private static string ExtractTitleFromMarkdown(string markdownContent) + /// 实验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.IsNullOrEmpty(markdownContent)) - return ""; + if (string.IsNullOrWhiteSpace(examId) || string.IsNullOrWhiteSpace(resourceType) || file == null) + return BadRequest("实验ID、资源类型和文件不能为空"); - var lines = markdownContent.Split('\n'); - foreach (var line in lines) + try { - var trimmedLine = line.Trim(); - if (trimmedLine.StartsWith("# ")) - { - return trimmedLine.Substring(2).Trim(); - } - } + using var db = new Database.AppDataConnection(); + + // 读取文件数据 + using var memoryStream = new MemoryStream(); + await file.CopyToAsync(memoryStream); + var fileData = memoryStream.ToArray(); - return ""; + 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/Database.cs b/server/src/Database.cs index da31649..8f6c4fd 100644 --- a/server/src/Database.cs +++ b/server/src/Database.cs @@ -162,10 +162,16 @@ public class Exam public required string ID { get; set; } /// - /// 实验文档内容(Markdown格式) + /// 实验名称 /// [NotNull] - public required string DocContent { get; set; } + public required string Name { get; set; } + + /// + /// 实验描述 + /// + [NotNull] + public required string Description { get; set; } /// /// 实验创建时间 @@ -178,6 +184,127 @@ public class Exam /// [NotNull] public DateTime UpdatedTime { get; set; } = DateTime.Now; + + /// + /// 实验标签(以逗号分隔的字符串) + /// + [NotNull] + public string Tags { get; set; } = ""; + + /// + /// 实验难度(1-5,1为最简单) + /// + [NotNull] + public int Difficulty { get; set; } = 1; + + /// + /// 普通用户是否可见 + /// + [NotNull] + public bool IsVisibleToUsers { get; set; } = true; + + /// + /// 获取标签列表 + /// + /// 标签数组 + public string[] GetTagsList() + { + if (string.IsNullOrWhiteSpace(Tags)) + return Array.Empty(); + + return Tags.Split(',', StringSplitOptions.RemoveEmptyEntries) + .Select(tag => tag.Trim()) + .Where(tag => !string.IsNullOrEmpty(tag)) + .ToArray(); + } + + /// + /// 设置标签列表 + /// + /// 标签数组 + public void SetTagsList(string[] tags) + { + Tags = string.Join(",", tags.Where(tag => !string.IsNullOrWhiteSpace(tag)).Select(tag => tag.Trim())); + } +} + +/// +/// 实验资源表(图片等) +/// +public class ExamResource +{ + /// + /// 资源的唯一标识符 + /// + [PrimaryKey, Identity] + public int ID { get; set; } + + /// + /// 所属实验ID + /// + [NotNull] + public required string ExamID { get; set; } + + /// + /// 资源类型(images, markdown, bitstream, diagram, project) + /// + [NotNull] + public required string ResourceType { get; set; } + + /// + /// 资源名称(包含文件扩展名) + /// + [NotNull] + public required string ResourceName { get; set; } + + /// + /// 资源的二进制数据 + /// + [NotNull] + public required byte[] Data { get; set; } + + /// + /// 资源创建时间 + /// + [NotNull] + public DateTime CreatedTime { get; set; } = DateTime.Now; + + /// + /// 资源的MIME类型 + /// + [NotNull] + public string MimeType { get; set; } = "application/octet-stream"; + + /// + /// 资源类型枚举 + /// + public static class ResourceTypes + { + /// + /// 图片资源类型 + /// + public const string Images = "images"; + + /// + /// Markdown文档资源类型 + /// + public const string Markdown = "markdown"; + + /// + /// 比特流文件资源类型 + /// + public const string Bitstream = "bitstream"; + + /// + /// 原理图资源类型 + /// + public const string Diagram = "diagram"; + + /// + /// 项目文件资源类型 + /// + public const string Project = "project"; + } } /// @@ -228,6 +355,7 @@ public class AppDataConnection : DataConnection this.CreateTable(); this.CreateTable(); this.CreateTable(); + this.CreateTable(); logger.Info("数据库表创建完成"); } @@ -240,6 +368,7 @@ public class AppDataConnection : DataConnection this.DropTable(); this.DropTable(); this.DropTable(); + this.DropTable(); logger.Warn("所有数据库表已删除"); } @@ -674,75 +803,271 @@ public class AppDataConnection : DataConnection public ITable ExamTable => this.GetTable(); /// - /// 扫描 exam 文件夹并更新实验数据库 + /// 实验资源表 /// - /// exam 文件夹的路径 - /// 更新的实验数量 - public int ScanAndUpdateExams(string examFolderPath) + public ITable ExamResourceTable => this.GetTable(); + + /// + /// 创建新实验 + /// + /// 实验ID + /// 实验名称 + /// 实验描述 + /// 实验标签 + /// 实验难度 + /// 普通用户是否可见 + /// 创建的实验 + public Result CreateExam(string id, string name, string description, string[]? tags = null, int difficulty = 1, bool isVisibleToUsers = true) { - if (!Directory.Exists(examFolderPath)) + try { - logger.Warn($"实验文件夹不存在: {examFolderPath}"); - return 0; + // 检查实验ID是否已存在 + var existingExam = this.ExamTable.Where(e => e.ID == id).FirstOrDefault(); + if (existingExam != null) + { + logger.Error($"实验ID已存在: {id}"); + return new(new Exception($"实验ID已存在: {id}")); + } + + var exam = new Exam + { + ID = id, + Name = name, + Description = description, + Difficulty = Math.Max(1, Math.Min(5, difficulty)), + IsVisibleToUsers = isVisibleToUsers, + CreatedTime = DateTime.Now, + UpdatedTime = DateTime.Now + }; + + if (tags != null) + { + exam.SetTagsList(tags); + } + + this.Insert(exam); + logger.Info($"新实验已创建: {id} ({name})"); + return new(exam); } - - int updateCount = 0; - var subdirectories = Directory.GetDirectories(examFolderPath); - - foreach (var examDir in subdirectories) + catch (Exception ex) { - var examId = Path.GetFileName(examDir); - var docPath = Path.Combine(examDir, "doc.md"); - - if (!File.Exists(docPath)) - { - logger.Warn($"实验 {examId} 缺少 doc.md 文件"); - continue; - } - - try - { - var docContent = File.ReadAllText(docPath); - var existingExam = this.ExamTable.Where(e => e.ID == examId).FirstOrDefault(); - - if (existingExam == null) - { - // 创建新实验 - var newExam = new Exam - { - ID = examId, - DocContent = docContent, - CreatedTime = DateTime.Now, - UpdatedTime = DateTime.Now - }; - this.Insert(newExam); - logger.Info($"新实验已添加: {examId}"); - updateCount++; - } - else - { - // 更新现有实验 - var fileLastWrite = File.GetLastWriteTime(docPath); - if (fileLastWrite > existingExam.UpdatedTime) - { - this.ExamTable - .Where(e => e.ID == examId) - .Set(e => e.DocContent, docContent) - .Set(e => e.UpdatedTime, DateTime.Now) - .Update(); - logger.Info($"实验已更新: {examId}"); - updateCount++; - } - } - } - catch (Exception ex) - { - logger.Error($"处理实验 {examId} 时出错: {ex.Message}"); - } + logger.Error($"创建实验时出错: {ex.Message}"); + return new(ex); } + } - logger.Info($"实验扫描完成,共更新 {updateCount} 个实验"); - return updateCount; + /// + /// 更新实验信息 + /// + /// 实验ID + /// 实验名称 + /// 实验描述 + /// 实验标签 + /// 实验难度 + /// 普通用户是否可见 + /// 更新的记录数 + public Result UpdateExam(string id, string? name = null, string? description = null, string[]? tags = null, int? difficulty = null, bool? isVisibleToUsers = null) + { + try + { + int result = 0; + + if (name != null) + { + result += this.ExamTable.Where(e => e.ID == id).Set(e => e.Name, name).Update(); + } + if (description != null) + { + result += this.ExamTable.Where(e => e.ID == id).Set(e => e.Description, description).Update(); + } + if (tags != null) + { + var tagsString = string.Join(",", tags.Where(tag => !string.IsNullOrWhiteSpace(tag)).Select(tag => tag.Trim())); + result += this.ExamTable.Where(e => e.ID == id).Set(e => e.Tags, tagsString).Update(); + } + if (difficulty.HasValue) + { + result += this.ExamTable.Where(e => e.ID == id).Set(e => e.Difficulty, Math.Max(1, Math.Min(5, difficulty.Value))).Update(); + } + if (isVisibleToUsers.HasValue) + { + result += this.ExamTable.Where(e => e.ID == id).Set(e => e.IsVisibleToUsers, isVisibleToUsers.Value).Update(); + } + + // 更新时间 + this.ExamTable.Where(e => e.ID == id).Set(e => e.UpdatedTime, DateTime.Now).Update(); + + logger.Info($"实验已更新: {id},更新记录数: {result}"); + return new(result); + } + catch (Exception ex) + { + logger.Error($"更新实验时出错: {ex.Message}"); + return new(ex); + } + } + + /// + /// 添加实验资源 + /// + /// 所属实验ID + /// 资源类型 + /// 资源名称 + /// 资源二进制数据 + /// MIME类型(可选,将根据文件扩展名自动确定) + /// 创建的资源 + public Result AddExamResource(string examId, string resourceType, string resourceName, byte[] data, string? mimeType = null) + { + try + { + // 验证实验是否存在 + var exam = this.ExamTable.Where(e => e.ID == examId).FirstOrDefault(); + if (exam == null) + { + logger.Error($"实验不存在: {examId}"); + return new(new Exception($"实验不存在: {examId}")); + } + + // 检查资源是否已存在 + var existingResource = this.ExamResourceTable + .Where(r => r.ExamID == examId && r.ResourceType == resourceType && r.ResourceName == resourceName) + .FirstOrDefault(); + if (existingResource != null) + { + logger.Error($"资源已存在: {examId}/{resourceType}/{resourceName}"); + return new(new Exception($"资源已存在: {examId}/{resourceType}/{resourceName}")); + } + + // 如果未指定MIME类型,根据文件扩展名自动确定 + if (string.IsNullOrEmpty(mimeType)) + { + var extension = Path.GetExtension(resourceName).ToLowerInvariant(); + mimeType = GetMimeTypeFromExtension(extension, resourceName); + } + + var resource = new ExamResource + { + ExamID = examId, + ResourceType = resourceType, + ResourceName = resourceName, + Data = data, + MimeType = mimeType, + CreatedTime = DateTime.Now + }; + + this.Insert(resource); + logger.Info($"新资源已添加: {examId}/{resourceType}/{resourceName} ({data.Length} bytes)"); + return new(resource); + } + catch (Exception ex) + { + logger.Error($"添加实验资源时出错: {ex.Message}"); + return new(ex); + } + } + + /// + /// 获取指定实验ID的指定资源类型的所有资源的ID和名称 + /// + /// 实验ID + /// 资源类型 + /// 资源信息列表 + public Result<(int ID, string Name)[]> GetExamResourceList(string examId, string resourceType) + { + try + { + var resources = this.ExamResourceTable + .Where(r => r.ExamID == examId && r.ResourceType == resourceType) + .Select(r => new { r.ID, r.ResourceName }) + .ToArray(); + + var result = resources.Select(r => (r.ID, r.ResourceName)).ToArray(); + logger.Info($"获取实验资源列表: {examId}/{resourceType},共 {result.Length} 个资源"); + return new(result); + } + catch (Exception ex) + { + logger.Error($"获取实验资源列表时出错: {ex.Message}"); + return new(ex); + } + } + + /// + /// 根据资源ID获取资源 + /// + /// 资源ID + /// 资源数据 + public Result> GetExamResourceById(int resourceId) + { + try + { + var resource = this.ExamResourceTable.Where(r => r.ID == resourceId).FirstOrDefault(); + + if (resource == null) + { + logger.Info($"未找到资源: {resourceId}"); + return new(Optional.None); + } + + logger.Info($"成功获取资源: {resourceId} ({resource.ResourceName})"); + return new(resource); + } + catch (Exception ex) + { + logger.Error($"获取资源时出错: {ex.Message}"); + return new(ex); + } + } + + /// + /// 删除实验资源 + /// + /// 资源ID + /// 删除的记录数 + public Result DeleteExamResource(int resourceId) + { + try + { + var result = this.ExamResourceTable.Where(r => r.ID == resourceId).Delete(); + logger.Info($"资源已删除: {resourceId},删除记录数: {result}"); + return new(result); + } + catch (Exception ex) + { + logger.Error($"删除资源时出错: {ex.Message}"); + return new(ex); + } + } + + /// + /// 根据文件扩展名获取MIME类型 + /// + /// 文件扩展名 + /// 文件名(可选,用于特殊文件判断) + /// MIME类型 + private string GetMimeTypeFromExtension(string extension, string fileName = "") + { + // 特殊文件名处理 + if (!string.IsNullOrEmpty(fileName) && fileName.Equals("diagram.json", StringComparison.OrdinalIgnoreCase)) + { + return "application/json"; + } + + return extension.ToLowerInvariant() switch + { + ".png" => "image/png", + ".jpg" or ".jpeg" => "image/jpeg", + ".gif" => "image/gif", + ".bmp" => "image/bmp", + ".svg" => "image/svg+xml", + ".sbit" => "application/octet-stream", + ".bit" => "application/octet-stream", + ".bin" => "application/octet-stream", + ".json" => "application/json", + ".zip" => "application/zip", + ".md" => "text/markdown", + _ => "application/octet-stream" + }; } /// @@ -780,4 +1105,31 @@ public class AppDataConnection : DataConnection logger.Debug($"成功获取实验信息: {examId}"); return new(exams[0]); } + + /// + /// 删除所有实验 + /// + /// 删除的实验数量 + public int DeleteAllExams() + { + // 先删除所有实验资源 + 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; + } } diff --git a/src/APIClient.ts b/src/APIClient.ts index 324260b..9825ea0 100644 --- a/src/APIClient.ts +++ b/src/APIClient.ts @@ -2811,17 +2811,22 @@ export class ExamClient { } /** - * 重新扫描实验文件夹并更新数据库 - * @return 更新结果 + * 创建新实验 + * @param request 创建实验请求 + * @return 创建结果 */ - scanExams( cancelToken?: CancelToken): Promise { - let url_ = this.baseUrl + "/api/Exam/scan"; + createExam(request: CreateExamRequest, cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/Exam"; url_ = url_.replace(/[?&]$/, ""); + const content_ = JSON.stringify(request); + let options_: AxiosRequestConfig = { + data: content_, method: "POST", url: url_, headers: { + "Content-Type": "application/json", "Accept": "application/json" }, cancelToken @@ -2834,11 +2839,11 @@ export class ExamClient { throw _error; } }).then((_response: AxiosResponse) => { - return this.processScanExams(_response); + return this.processCreateExam(_response); }); } - protected processScanExams(response: AxiosResponse): Promise { + protected processCreateExam(response: AxiosResponse): Promise { const status = response.status; let _headers: any = {}; if (response.headers && typeof response.headers === "object") { @@ -2848,12 +2853,19 @@ export class ExamClient { } } } - if (status === 200) { + if (status === 201) { const _responseText = response.data; - let result200: any = null; - let resultData200 = _responseText; - result200 = ScanResult.fromJS(resultData200); - return Promise.resolve(result200); + let result201: any = null; + let resultData201 = _responseText; + result201 = ExamInfo.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; @@ -2869,6 +2881,13 @@ export class ExamClient { result403 = ProblemDetails.fromJS(resultData403); return throwException("A server side error occurred.", status, _responseText, _headers, result403); + } 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); @@ -2877,7 +2896,278 @@ export class ExamClient { const _responseText = response.data; return throwException("An unexpected server error occurred.", status, _responseText, _headers); } - return Promise.resolve(null as any); + 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); } } @@ -7239,20 +7529,15 @@ export enum CaptureMode { /** 调试器整体配置信息 */ export class DebuggerConfig implements IDebuggerConfig { - /** 时钟频率 - */ + /** 时钟频率 */ clkFreq!: number; - /** 总端口数量 - */ + /** 总端口数量 */ totalPortNum!: number; - /** 捕获深度(采样点数) - */ + /** 捕获深度(采样点数) */ captureDepth!: number; - /** 触发器数量 - */ + /** 触发器数量 */ triggerNum!: number; - /** 所有信号通道的配置信息 - */ + /** 所有信号通道的配置信息 */ channelConfigs!: ChannelConfig[]; constructor(data?: IDebuggerConfig) { @@ -7305,42 +7590,31 @@ export class DebuggerConfig implements IDebuggerConfig { /** 调试器整体配置信息 */ export interface IDebuggerConfig { - /** 时钟频率 - */ + /** 时钟频率 */ clkFreq: number; - /** 总端口数量 - */ + /** 总端口数量 */ totalPortNum: number; - /** 捕获深度(采样点数) - */ + /** 捕获深度(采样点数) */ captureDepth: number; - /** 触发器数量 - */ + /** 触发器数量 */ triggerNum: number; - /** 所有信号通道的配置信息 - */ + /** 所有信号通道的配置信息 */ channelConfigs: ChannelConfig[]; } /** 表示单个信号通道的配置信息 */ export class ChannelConfig implements IChannelConfig { - /** 通道名称 - */ + /** 通道名称 */ name!: string; - /** 通道显示颜色(如前端波形显示用) - */ + /** 通道显示颜色(如前端波形显示用) */ color!: string; - /** 通道信号线宽度(位数) - */ + /** 通道信号线宽度(位数) */ wireWidth!: number; - /** 信号线在父端口中的起始索引(bit) - */ + /** 信号线在父端口中的起始索引(bit) */ wireStartIndex!: number; - /** 父端口编号 - */ + /** 父端口编号 */ parentPort!: number; - /** 捕获模式(如上升沿、下降沿等) - */ + /** 捕获模式(如上升沿、下降沿等) */ mode!: CaptureMode; constructor(data?: IChannelConfig) { @@ -7384,33 +7658,25 @@ export class ChannelConfig implements IChannelConfig { /** 表示单个信号通道的配置信息 */ export interface IChannelConfig { - /** 通道名称 - */ + /** 通道名称 */ name: string; - /** 通道显示颜色(如前端波形显示用) - */ + /** 通道显示颜色(如前端波形显示用) */ color: string; - /** 通道信号线宽度(位数) - */ + /** 通道信号线宽度(位数) */ wireWidth: number; - /** 信号线在父端口中的起始索引(bit) - */ + /** 信号线在父端口中的起始索引(bit) */ wireStartIndex: number; - /** 父端口编号 - */ + /** 父端口编号 */ parentPort: number; - /** 捕获模式(如上升沿、下降沿等) - */ + /** 捕获模式(如上升沿、下降沿等) */ mode: CaptureMode; } /** 单个通道的捕获数据 */ export class ChannelCaptureData implements IChannelCaptureData { - /** 通道名称 - */ + /** 通道名称 */ name!: string; - /** 通道捕获到的数据(Base64编码的UInt32数组) - */ + /** 通道捕获到的数据(Base64编码的UInt32数组) */ data!: string; constructor(data?: IChannelCaptureData) { @@ -7446,11 +7712,9 @@ export class ChannelCaptureData implements IChannelCaptureData { /** 单个通道的捕获数据 */ export interface IChannelCaptureData { - /** 通道名称 - */ + /** 通道名称 */ name: string; - /** 通道捕获到的数据(Base64编码的UInt32数组) - */ + /** 通道捕获到的数据(Base64编码的UInt32数组) */ data: string; } @@ -7458,12 +7722,18 @@ export interface IChannelCaptureData { export class ExamSummary implements IExamSummary { /** 实验的唯一标识符 */ id!: string; + /** 实验名称 */ + name!: string; /** 实验创建时间 */ createdTime!: Date; /** 实验最后更新时间 */ updatedTime!: Date; - /** 实验标题(从文档内容中提取) */ - title!: string; + /** 实验标签 */ + tags!: string[]; + /** 实验难度(1-5) */ + difficulty!: number; + /** 普通用户是否可见 */ + isVisibleToUsers!: boolean; constructor(data?: IExamSummary) { if (data) { @@ -7472,14 +7742,24 @@ export class ExamSummary implements IExamSummary { (this)[property] = (data)[property]; } } + if (!data) { + this.tags = []; + } } init(_data?: any) { if (_data) { this.id = _data["id"]; + this.name = _data["name"]; this.createdTime = _data["createdTime"] ? new Date(_data["createdTime"].toString()) : undefined; this.updatedTime = _data["updatedTime"] ? new Date(_data["updatedTime"].toString()) : undefined; - this.title = _data["title"]; + if (Array.isArray(_data["tags"])) { + this.tags = [] as any; + for (let item of _data["tags"]) + this.tags!.push(item); + } + this.difficulty = _data["difficulty"]; + this.isVisibleToUsers = _data["isVisibleToUsers"]; } } @@ -7493,9 +7773,16 @@ export class ExamSummary implements IExamSummary { toJSON(data?: any) { data = typeof data === 'object' ? data : {}; data["id"] = this.id; + data["name"] = this.name; data["createdTime"] = this.createdTime ? this.createdTime.toISOString() : undefined; data["updatedTime"] = this.updatedTime ? this.updatedTime.toISOString() : undefined; - data["title"] = this.title; + if (Array.isArray(this.tags)) { + data["tags"] = []; + for (let item of this.tags) + data["tags"].push(item); + } + data["difficulty"] = this.difficulty; + data["isVisibleToUsers"] = this.isVisibleToUsers; return data; } } @@ -7504,24 +7791,38 @@ export class ExamSummary implements IExamSummary { export interface IExamSummary { /** 实验的唯一标识符 */ id: string; + /** 实验名称 */ + name: string; /** 实验创建时间 */ createdTime: Date; /** 实验最后更新时间 */ updatedTime: Date; - /** 实验标题(从文档内容中提取) */ - title: string; + /** 实验标签 */ + tags: string[]; + /** 实验难度(1-5) */ + difficulty: number; + /** 普通用户是否可见 */ + isVisibleToUsers: boolean; } /** 实验信息类 */ export class ExamInfo implements IExamInfo { /** 实验的唯一标识符 */ id!: string; - /** 实验文档内容(Markdown格式) */ - docContent!: string; + /** 实验名称 */ + name!: string; + /** 实验描述 */ + description!: string; /** 实验创建时间 */ createdTime!: Date; /** 实验最后更新时间 */ updatedTime!: Date; + /** 实验标签 */ + tags!: string[]; + /** 实验难度(1-5) */ + difficulty!: number; + /** 普通用户是否可见 */ + isVisibleToUsers!: boolean; constructor(data?: IExamInfo) { if (data) { @@ -7530,14 +7831,25 @@ export class ExamInfo implements IExamInfo { (this)[property] = (data)[property]; } } + if (!data) { + this.tags = []; + } } init(_data?: any) { if (_data) { this.id = _data["id"]; - this.docContent = _data["docContent"]; + this.name = _data["name"]; + this.description = _data["description"]; this.createdTime = _data["createdTime"] ? new Date(_data["createdTime"].toString()) : undefined; this.updatedTime = _data["updatedTime"] ? new Date(_data["updatedTime"].toString()) : undefined; + if (Array.isArray(_data["tags"])) { + this.tags = [] as any; + for (let item of _data["tags"]) + this.tags!.push(item); + } + this.difficulty = _data["difficulty"]; + this.isVisibleToUsers = _data["isVisibleToUsers"]; } } @@ -7551,9 +7863,17 @@ export class ExamInfo implements IExamInfo { toJSON(data?: any) { data = typeof data === 'object' ? data : {}; data["id"] = this.id; - data["docContent"] = this.docContent; + data["name"] = this.name; + data["description"] = this.description; data["createdTime"] = this.createdTime ? this.createdTime.toISOString() : undefined; data["updatedTime"] = this.updatedTime ? this.updatedTime.toISOString() : undefined; + if (Array.isArray(this.tags)) { + data["tags"] = []; + for (let item of this.tags) + data["tags"].push(item); + } + data["difficulty"] = this.difficulty; + data["isVisibleToUsers"] = this.isVisibleToUsers; return data; } } @@ -7562,22 +7882,111 @@ export class ExamInfo implements IExamInfo { export interface IExamInfo { /** 实验的唯一标识符 */ id: string; - /** 实验文档内容(Markdown格式) */ - docContent: string; + /** 实验名称 */ + name: string; + /** 实验描述 */ + description: string; /** 实验创建时间 */ createdTime: Date; /** 实验最后更新时间 */ updatedTime: Date; + /** 实验标签 */ + tags: string[]; + /** 实验难度(1-5) */ + difficulty: number; + /** 普通用户是否可见 */ + isVisibleToUsers: boolean; } -/** 扫描结果类 */ -export class ScanResult implements IScanResult { - /** 结果消息 */ - declare message: string; - /** 更新的实验数量 */ - updateCount!: number; +/** 创建实验请求类 */ +export class CreateExamRequest implements ICreateExamRequest { + /** 实验ID */ + id!: string; + /** 实验名称 */ + name!: string; + /** 实验描述 */ + description!: string; + /** 实验标签 */ + tags!: string[]; + /** 实验难度(1-5) */ + difficulty!: number; + /** 普通用户是否可见 */ + isVisibleToUsers!: boolean; - constructor(data?: IScanResult) { + constructor(data?: ICreateExamRequest) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + if (!data) { + this.tags = []; + } + } + + init(_data?: any) { + if (_data) { + this.id = _data["id"]; + this.name = _data["name"]; + this.description = _data["description"]; + if (Array.isArray(_data["tags"])) { + this.tags = [] as any; + for (let item of _data["tags"]) + this.tags!.push(item); + } + this.difficulty = _data["difficulty"]; + this.isVisibleToUsers = _data["isVisibleToUsers"]; + } + } + + static fromJS(data: any): CreateExamRequest { + data = typeof data === 'object' ? data : {}; + let result = new CreateExamRequest(); + result.init(data); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["id"] = this.id; + data["name"] = this.name; + data["description"] = this.description; + if (Array.isArray(this.tags)) { + data["tags"] = []; + for (let item of this.tags) + data["tags"].push(item); + } + data["difficulty"] = this.difficulty; + data["isVisibleToUsers"] = this.isVisibleToUsers; + return data; + } +} + +/** 创建实验请求类 */ +export interface ICreateExamRequest { + /** 实验ID */ + id: string; + /** 实验名称 */ + name: string; + /** 实验描述 */ + description: string; + /** 实验标签 */ + tags: string[]; + /** 实验难度(1-5) */ + difficulty: number; + /** 普通用户是否可见 */ + 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)) @@ -7588,32 +7997,32 @@ export class ScanResult implements IScanResult { init(_data?: any) { if (_data) { - this.message = _data["message"]; - this.updateCount = _data["updateCount"]; + this.id = _data["id"]; + this.name = _data["name"]; } } - static fromJS(data: any): ScanResult { + static fromJS(data: any): ResourceInfo { data = typeof data === 'object' ? data : {}; - let result = new ScanResult(); + let result = new ResourceInfo(); result.init(data); return result; } toJSON(data?: any) { data = typeof data === 'object' ? data : {}; - data["message"] = this.message; - data["updateCount"] = this.updateCount; + data["id"] = this.id; + data["name"] = this.name; return data; } } -/** 扫描结果类 */ -export interface IScanResult { - /** 结果消息 */ - message: string; - /** 更新的实验数量 */ - updateCount: number; +/** 资源信息类 */ +export interface IResourceInfo { + /** 资源ID */ + id: number; + /** 资源名称 */ + name: string; } /** 逻辑分析仪运行状态枚举 */ diff --git a/src/components/Alert/Alert.vue b/src/components/Alert/Alert.vue index 74f0cf6..0677cfe 100644 --- a/src/components/Alert/Alert.vue +++ b/src/components/Alert/Alert.vue @@ -1,5 +1,5 @@