add: 添加实验列表界面,实验增删完全依赖数据库实现
This commit is contained in:
parent
d27b5d7737
commit
c564844673
|
@ -168,6 +168,17 @@ try
|
||||||
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "log")),
|
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "log")),
|
||||||
RequestPath = "/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");
|
app.MapFallbackToFile("index.html");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,19 +205,6 @@ try
|
||||||
// Setup Program
|
// Setup Program
|
||||||
MsgBus.Init();
|
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
|
// Generate API Client
|
||||||
app.MapGet("GetAPIClientCode", async (HttpContext context) =>
|
app.MapGet("GetAPIClientCode", async (HttpContext context) =>
|
||||||
{
|
{
|
||||||
|
|
|
@ -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 开发工具进行仿真
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
- 确保输入信号的电平正确
|
|
||||||
- 注意时序的约束
|
|
||||||
- 验证结果时要仔细对比真值表
|
|
|
@ -1,35 +0,0 @@
|
||||||
# 实验002:组合逻辑电路设计
|
|
||||||
|
|
||||||
## 实验目的
|
|
||||||
|
|
||||||
本实验旨在让学生学习如何设计和实现复杂的组合逻辑电路,掌握多个逻辑门的组合使用。
|
|
||||||
|
|
||||||
## 实验内容
|
|
||||||
|
|
||||||
### 1. 半加器设计
|
|
||||||
设计一个半加器电路,实现两个一位二进制数的加法运算。
|
|
||||||
|
|
||||||
### 2. 全加器设计
|
|
||||||
在半加器的基础上,设计全加器电路,考虑进位输入。
|
|
||||||
|
|
||||||
### 3. 编码器和译码器
|
|
||||||
实现简单的编码器和译码器电路。
|
|
||||||
|
|
||||||
## 实验要求
|
|
||||||
|
|
||||||
1. 使用 Verilog HDL 编写代码
|
|
||||||
2. 绘制逻辑电路图
|
|
||||||
3. 编写测试用例验证功能
|
|
||||||
4. 分析电路的延时特性
|
|
||||||
|
|
||||||
## 评估标准
|
|
||||||
|
|
||||||
- 电路功能正确性 (40%)
|
|
||||||
- 代码质量和规范性 (30%)
|
|
||||||
- 测试覆盖率 (20%)
|
|
||||||
- 实验报告 (10%)
|
|
||||||
|
|
||||||
## 参考资料
|
|
||||||
|
|
||||||
- 数字逻辑设计教材第3-4章
|
|
||||||
- Verilog HDL 语法参考手册
|
|
|
@ -25,9 +25,14 @@ public class ExamController : ControllerBase
|
||||||
public required string ID { get; set; }
|
public required string ID { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 实验文档内容(Markdown格式)
|
/// 实验名称
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required string DocContent { get; set; }
|
public required string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实验描述
|
||||||
|
/// </summary>
|
||||||
|
public required string Description { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 实验创建时间
|
/// 实验创建时间
|
||||||
|
@ -38,6 +43,21 @@ public class ExamController : ControllerBase
|
||||||
/// 实验最后更新时间
|
/// 实验最后更新时间
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime UpdatedTime { get; set; }
|
public DateTime UpdatedTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实验标签
|
||||||
|
/// </summary>
|
||||||
|
public string[] Tags { get; set; } = Array.Empty<string>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实验难度(1-5)
|
||||||
|
/// </summary>
|
||||||
|
public int Difficulty { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 普通用户是否可见
|
||||||
|
/// </summary>
|
||||||
|
public bool IsVisibleToUsers { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -50,6 +70,11 @@ public class ExamController : ControllerBase
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required string ID { get; set; }
|
public required string ID { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实验名称
|
||||||
|
/// </summary>
|
||||||
|
public required string Name { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 实验创建时间
|
/// 实验创建时间
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -61,25 +86,71 @@ public class ExamController : ControllerBase
|
||||||
public DateTime UpdatedTime { get; set; }
|
public DateTime UpdatedTime { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 实验标题(从文档内容中提取)
|
/// 实验标签
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Title { get; set; } = "";
|
public string[] Tags { get; set; } = Array.Empty<string>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实验难度(1-5)
|
||||||
|
/// </summary>
|
||||||
|
public int Difficulty { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 普通用户是否可见
|
||||||
|
/// </summary>
|
||||||
|
public bool IsVisibleToUsers { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 扫描结果类
|
/// 资源信息类
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ScanResult
|
public class ResourceInfo
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 结果消息
|
/// 资源ID
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required string Message { get; set; }
|
public int ID { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 更新的实验数量
|
/// 资源名称
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int UpdateCount { get; set; }
|
public required string Name { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建实验请求类
|
||||||
|
/// </summary>
|
||||||
|
public class CreateExamRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 实验ID
|
||||||
|
/// </summary>
|
||||||
|
public required string ID { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实验名称
|
||||||
|
/// </summary>
|
||||||
|
public required string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实验描述
|
||||||
|
/// </summary>
|
||||||
|
public required string Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实验标签
|
||||||
|
/// </summary>
|
||||||
|
public string[] Tags { get; set; } = Array.Empty<string>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实验难度(1-5)
|
||||||
|
/// </summary>
|
||||||
|
public int Difficulty { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 普通用户是否可见
|
||||||
|
/// </summary>
|
||||||
|
public bool IsVisibleToUsers { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -102,9 +173,12 @@ public class ExamController : ControllerBase
|
||||||
var examSummaries = exams.Select(exam => new ExamSummary
|
var examSummaries = exams.Select(exam => new ExamSummary
|
||||||
{
|
{
|
||||||
ID = exam.ID,
|
ID = exam.ID,
|
||||||
|
Name = exam.Name,
|
||||||
CreatedTime = exam.CreatedTime,
|
CreatedTime = exam.CreatedTime,
|
||||||
UpdatedTime = exam.UpdatedTime,
|
UpdatedTime = exam.UpdatedTime,
|
||||||
Title = ExtractTitleFromMarkdown(exam.DocContent)
|
Tags = exam.GetTagsList(),
|
||||||
|
Difficulty = exam.Difficulty,
|
||||||
|
IsVisibleToUsers = exam.IsVisibleToUsers
|
||||||
}).ToArray();
|
}).ToArray();
|
||||||
|
|
||||||
logger.Info($"成功获取实验列表,共 {examSummaries.Length} 个实验");
|
logger.Info($"成功获取实验列表,共 {examSummaries.Length} 个实验");
|
||||||
|
@ -156,9 +230,13 @@ public class ExamController : ControllerBase
|
||||||
var examInfo = new ExamInfo
|
var examInfo = new ExamInfo
|
||||||
{
|
{
|
||||||
ID = exam.ID,
|
ID = exam.ID,
|
||||||
DocContent = exam.DocContent,
|
Name = exam.Name,
|
||||||
|
Description = exam.Description,
|
||||||
CreatedTime = exam.CreatedTime,
|
CreatedTime = exam.CreatedTime,
|
||||||
UpdatedTime = exam.UpdatedTime
|
UpdatedTime = exam.UpdatedTime,
|
||||||
|
Tags = exam.GetTagsList(),
|
||||||
|
Difficulty = exam.Difficulty,
|
||||||
|
IsVisibleToUsers = exam.IsVisibleToUsers
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.Info($"成功获取实验信息: {examId}");
|
logger.Info($"成功获取实验信息: {examId}");
|
||||||
|
@ -172,60 +250,205 @@ public class ExamController : ControllerBase
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 重新扫描实验文件夹并更新数据库
|
/// 创建新实验
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>更新结果</returns>
|
/// <param name="request">创建实验请求</param>
|
||||||
|
/// <returns>创建结果</returns>
|
||||||
[Authorize("Admin")]
|
[Authorize("Admin")]
|
||||||
[HttpPost("scan")]
|
[HttpPost]
|
||||||
[EnableCors("Users")]
|
[EnableCors("Users")]
|
||||||
[ProducesResponseType(typeof(ScanResult), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(ExamInfo), StatusCodes.Status201Created)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[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
|
try
|
||||||
{
|
{
|
||||||
using var db = new Database.AppDataConnection();
|
using var db = new Database.AppDataConnection();
|
||||||
var examFolderPath = Path.Combine(Directory.GetCurrentDirectory(), "exam");
|
var result = db.CreateExam(request.ID, request.Name, request.Description, request.Tags, request.Difficulty, request.IsVisibleToUsers);
|
||||||
var updateCount = db.ScanAndUpdateExams(examFolderPath);
|
|
||||||
|
|
||||||
var result = new ScanResult
|
if (!result.IsSuccessful)
|
||||||
{
|
{
|
||||||
Message = $"扫描完成,更新了 {updateCount} 个实验",
|
if (result.Error.Message.Contains("已存在"))
|
||||||
UpdateCount = updateCount
|
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} 个实验");
|
logger.Info($"成功创建实验: {request.ID}");
|
||||||
return Ok(result);
|
return CreatedAtAction(nameof(GetExam), new { examId = request.ID }, examInfo);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.Error($"扫描实验时出错: {ex.Message}");
|
logger.Error($"创建实验 {request.ID} 时出错: {ex.Message}");
|
||||||
return StatusCode(StatusCodes.Status500InternalServerError, $"扫描实验失败: {ex.Message}");
|
return StatusCode(StatusCodes.Status500InternalServerError, $"创建实验失败: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 从 Markdown 内容中提取标题
|
/// 添加实验资源
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="markdownContent">Markdown 内容</param>
|
/// <param name="examId">实验ID</param>
|
||||||
/// <returns>提取的标题</returns>
|
/// <param name="resourceType">资源类型</param>
|
||||||
private static string ExtractTitleFromMarkdown(string markdownContent)
|
/// <param name="file">资源文件</param>
|
||||||
|
/// <returns>添加结果</returns>
|
||||||
|
[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<IActionResult> AddExamResource(string examId, string resourceType, IFormFile file)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(markdownContent))
|
if (string.IsNullOrWhiteSpace(examId) || string.IsNullOrWhiteSpace(resourceType) || file == null)
|
||||||
return "";
|
return BadRequest("实验ID、资源类型和文件不能为空");
|
||||||
|
|
||||||
var lines = markdownContent.Split('\n');
|
try
|
||||||
foreach (var line in lines)
|
|
||||||
{
|
{
|
||||||
var trimmedLine = line.Trim();
|
using var db = new Database.AppDataConnection();
|
||||||
if (trimmedLine.StartsWith("# "))
|
|
||||||
{
|
|
||||||
return trimmedLine.Substring(2).Trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
// 读取文件数据
|
||||||
|
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取指定实验ID的指定资源类型的所有资源的ID和名称
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="examId">实验ID</param>
|
||||||
|
/// <param name="resourceType">资源类型</param>
|
||||||
|
/// <returns>资源列表</returns>
|
||||||
|
[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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据资源ID下载资源
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="resourceId">资源ID</param>
|
||||||
|
/// <returns>资源文件</returns>
|
||||||
|
[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}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -162,10 +162,16 @@ public class Exam
|
||||||
public required string ID { get; set; }
|
public required string ID { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 实验文档内容(Markdown格式)
|
/// 实验名称
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[NotNull]
|
[NotNull]
|
||||||
public required string DocContent { get; set; }
|
public required string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实验描述
|
||||||
|
/// </summary>
|
||||||
|
[NotNull]
|
||||||
|
public required string Description { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 实验创建时间
|
/// 实验创建时间
|
||||||
|
@ -178,6 +184,127 @@ public class Exam
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[NotNull]
|
[NotNull]
|
||||||
public DateTime UpdatedTime { get; set; } = DateTime.Now;
|
public DateTime UpdatedTime { get; set; } = DateTime.Now;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实验标签(以逗号分隔的字符串)
|
||||||
|
/// </summary>
|
||||||
|
[NotNull]
|
||||||
|
public string Tags { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实验难度(1-5,1为最简单)
|
||||||
|
/// </summary>
|
||||||
|
[NotNull]
|
||||||
|
public int Difficulty { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 普通用户是否可见
|
||||||
|
/// </summary>
|
||||||
|
[NotNull]
|
||||||
|
public bool IsVisibleToUsers { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取标签列表
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>标签数组</returns>
|
||||||
|
public string[] GetTagsList()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(Tags))
|
||||||
|
return Array.Empty<string>();
|
||||||
|
|
||||||
|
return Tags.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Select(tag => tag.Trim())
|
||||||
|
.Where(tag => !string.IsNullOrEmpty(tag))
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置标签列表
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tags">标签数组</param>
|
||||||
|
public void SetTagsList(string[] tags)
|
||||||
|
{
|
||||||
|
Tags = string.Join(",", tags.Where(tag => !string.IsNullOrWhiteSpace(tag)).Select(tag => tag.Trim()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实验资源表(图片等)
|
||||||
|
/// </summary>
|
||||||
|
public class ExamResource
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 资源的唯一标识符
|
||||||
|
/// </summary>
|
||||||
|
[PrimaryKey, Identity]
|
||||||
|
public int ID { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所属实验ID
|
||||||
|
/// </summary>
|
||||||
|
[NotNull]
|
||||||
|
public required string ExamID { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 资源类型(images, markdown, bitstream, diagram, project)
|
||||||
|
/// </summary>
|
||||||
|
[NotNull]
|
||||||
|
public required string ResourceType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 资源名称(包含文件扩展名)
|
||||||
|
/// </summary>
|
||||||
|
[NotNull]
|
||||||
|
public required string ResourceName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 资源的二进制数据
|
||||||
|
/// </summary>
|
||||||
|
[NotNull]
|
||||||
|
public required byte[] Data { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 资源创建时间
|
||||||
|
/// </summary>
|
||||||
|
[NotNull]
|
||||||
|
public DateTime CreatedTime { get; set; } = DateTime.Now;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 资源的MIME类型
|
||||||
|
/// </summary>
|
||||||
|
[NotNull]
|
||||||
|
public string MimeType { get; set; } = "application/octet-stream";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 资源类型枚举
|
||||||
|
/// </summary>
|
||||||
|
public static class ResourceTypes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 图片资源类型
|
||||||
|
/// </summary>
|
||||||
|
public const string Images = "images";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Markdown文档资源类型
|
||||||
|
/// </summary>
|
||||||
|
public const string Markdown = "markdown";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 比特流文件资源类型
|
||||||
|
/// </summary>
|
||||||
|
public const string Bitstream = "bitstream";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 原理图资源类型
|
||||||
|
/// </summary>
|
||||||
|
public const string Diagram = "diagram";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 项目文件资源类型
|
||||||
|
/// </summary>
|
||||||
|
public const string Project = "project";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -228,6 +355,7 @@ public class AppDataConnection : DataConnection
|
||||||
this.CreateTable<User>();
|
this.CreateTable<User>();
|
||||||
this.CreateTable<Board>();
|
this.CreateTable<Board>();
|
||||||
this.CreateTable<Exam>();
|
this.CreateTable<Exam>();
|
||||||
|
this.CreateTable<ExamResource>();
|
||||||
logger.Info("数据库表创建完成");
|
logger.Info("数据库表创建完成");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,6 +368,7 @@ public class AppDataConnection : DataConnection
|
||||||
this.DropTable<User>();
|
this.DropTable<User>();
|
||||||
this.DropTable<Board>();
|
this.DropTable<Board>();
|
||||||
this.DropTable<Exam>();
|
this.DropTable<Exam>();
|
||||||
|
this.DropTable<ExamResource>();
|
||||||
logger.Warn("所有数据库表已删除");
|
logger.Warn("所有数据库表已删除");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -674,75 +803,271 @@ public class AppDataConnection : DataConnection
|
||||||
public ITable<Exam> ExamTable => this.GetTable<Exam>();
|
public ITable<Exam> ExamTable => this.GetTable<Exam>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 扫描 exam 文件夹并更新实验数据库
|
/// 实验资源表
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="examFolderPath">exam 文件夹的路径</param>
|
public ITable<ExamResource> ExamResourceTable => this.GetTable<ExamResource>();
|
||||||
/// <returns>更新的实验数量</returns>
|
|
||||||
public int ScanAndUpdateExams(string examFolderPath)
|
/// <summary>
|
||||||
|
/// 创建新实验
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">实验ID</param>
|
||||||
|
/// <param name="name">实验名称</param>
|
||||||
|
/// <param name="description">实验描述</param>
|
||||||
|
/// <param name="tags">实验标签</param>
|
||||||
|
/// <param name="difficulty">实验难度</param>
|
||||||
|
/// <param name="isVisibleToUsers">普通用户是否可见</param>
|
||||||
|
/// <returns>创建的实验</returns>
|
||||||
|
public Result<Exam> CreateExam(string id, string name, string description, string[]? tags = null, int difficulty = 1, bool isVisibleToUsers = true)
|
||||||
{
|
{
|
||||||
if (!Directory.Exists(examFolderPath))
|
try
|
||||||
{
|
{
|
||||||
logger.Warn($"实验文件夹不存在: {examFolderPath}");
|
// 检查实验ID是否已存在
|
||||||
return 0;
|
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);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Error($"创建实验时出错: {ex.Message}");
|
||||||
|
return new(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新实验信息
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">实验ID</param>
|
||||||
|
/// <param name="name">实验名称</param>
|
||||||
|
/// <param name="description">实验描述</param>
|
||||||
|
/// <param name="tags">实验标签</param>
|
||||||
|
/// <param name="difficulty">实验难度</param>
|
||||||
|
/// <param name="isVisibleToUsers">普通用户是否可见</param>
|
||||||
|
/// <returns>更新的记录数</returns>
|
||||||
|
public Result<int> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 添加实验资源
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="examId">所属实验ID</param>
|
||||||
|
/// <param name="resourceType">资源类型</param>
|
||||||
|
/// <param name="resourceName">资源名称</param>
|
||||||
|
/// <param name="data">资源二进制数据</param>
|
||||||
|
/// <param name="mimeType">MIME类型(可选,将根据文件扩展名自动确定)</param>
|
||||||
|
/// <returns>创建的资源</returns>
|
||||||
|
public Result<ExamResource> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取指定实验ID的指定资源类型的所有资源的ID和名称
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="examId">实验ID</param>
|
||||||
|
/// <param name="resourceType">资源类型</param>
|
||||||
|
/// <returns>资源信息列表</returns>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据资源ID获取资源
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="resourceId">资源ID</param>
|
||||||
|
/// <returns>资源数据</returns>
|
||||||
|
public Result<Optional<ExamResource>> GetExamResourceById(int resourceId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var resource = this.ExamResourceTable.Where(r => r.ID == resourceId).FirstOrDefault();
|
||||||
|
|
||||||
|
if (resource == null)
|
||||||
|
{
|
||||||
|
logger.Info($"未找到资源: {resourceId}");
|
||||||
|
return new(Optional<ExamResource>.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info($"成功获取资源: {resourceId} ({resource.ResourceName})");
|
||||||
|
return new(resource);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Error($"获取资源时出错: {ex.Message}");
|
||||||
|
return new(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除实验资源
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="resourceId">资源ID</param>
|
||||||
|
/// <returns>删除的记录数</returns>
|
||||||
|
public Result<int> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据文件扩展名获取MIME类型
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="extension">文件扩展名</param>
|
||||||
|
/// <param name="fileName">文件名(可选,用于特殊文件判断)</param>
|
||||||
|
/// <returns>MIME类型</returns>
|
||||||
|
private string GetMimeTypeFromExtension(string extension, string fileName = "")
|
||||||
|
{
|
||||||
|
// 特殊文件名处理
|
||||||
|
if (!string.IsNullOrEmpty(fileName) && fileName.Equals("diagram.json", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return "application/json";
|
||||||
}
|
}
|
||||||
|
|
||||||
int updateCount = 0;
|
return extension.ToLowerInvariant() switch
|
||||||
var subdirectories = Directory.GetDirectories(examFolderPath);
|
|
||||||
|
|
||||||
foreach (var examDir in subdirectories)
|
|
||||||
{
|
{
|
||||||
var examId = Path.GetFileName(examDir);
|
".png" => "image/png",
|
||||||
var docPath = Path.Combine(examDir, "doc.md");
|
".jpg" or ".jpeg" => "image/jpeg",
|
||||||
|
".gif" => "image/gif",
|
||||||
if (!File.Exists(docPath))
|
".bmp" => "image/bmp",
|
||||||
{
|
".svg" => "image/svg+xml",
|
||||||
logger.Warn($"实验 {examId} 缺少 doc.md 文件");
|
".sbit" => "application/octet-stream",
|
||||||
continue;
|
".bit" => "application/octet-stream",
|
||||||
}
|
".bin" => "application/octet-stream",
|
||||||
|
".json" => "application/json",
|
||||||
try
|
".zip" => "application/zip",
|
||||||
{
|
".md" => "text/markdown",
|
||||||
var docContent = File.ReadAllText(docPath);
|
_ => "application/octet-stream"
|
||||||
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.Info($"实验扫描完成,共更新 {updateCount} 个实验");
|
|
||||||
return updateCount;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -780,4 +1105,31 @@ public class AppDataConnection : DataConnection
|
||||||
logger.Debug($"成功获取实验信息: {examId}");
|
logger.Debug($"成功获取实验信息: {examId}");
|
||||||
return new(exams[0]);
|
return new(exams[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除所有实验
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>删除的实验数量</returns>
|
||||||
|
public int DeleteAllExams()
|
||||||
|
{
|
||||||
|
// 先删除所有实验资源
|
||||||
|
var resourceDeleteCount = this.DeleteAllExamResources();
|
||||||
|
logger.Info($"已删除所有实验资源,共删除 {resourceDeleteCount} 个资源");
|
||||||
|
|
||||||
|
// 再删除所有实验
|
||||||
|
var examDeleteCount = this.ExamTable.Delete();
|
||||||
|
logger.Info($"已删除所有实验,共删除 {examDeleteCount} 个实验");
|
||||||
|
return examDeleteCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除所有实验资源
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>删除的资源数量</returns>
|
||||||
|
public int DeleteAllExamResources()
|
||||||
|
{
|
||||||
|
var deleteCount = this.ExamResourceTable.Delete();
|
||||||
|
logger.Info($"已删除所有实验资源,共删除 {deleteCount} 个资源");
|
||||||
|
return deleteCount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
599
src/APIClient.ts
599
src/APIClient.ts
|
@ -2811,17 +2811,22 @@ export class ExamClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重新扫描实验文件夹并更新数据库
|
* 创建新实验
|
||||||
* @return 更新结果
|
* @param request 创建实验请求
|
||||||
|
* @return 创建结果
|
||||||
*/
|
*/
|
||||||
scanExams( cancelToken?: CancelToken): Promise<ScanResult> {
|
createExam(request: CreateExamRequest, cancelToken?: CancelToken): Promise<ExamInfo> {
|
||||||
let url_ = this.baseUrl + "/api/Exam/scan";
|
let url_ = this.baseUrl + "/api/Exam";
|
||||||
url_ = url_.replace(/[?&]$/, "");
|
url_ = url_.replace(/[?&]$/, "");
|
||||||
|
|
||||||
|
const content_ = JSON.stringify(request);
|
||||||
|
|
||||||
let options_: AxiosRequestConfig = {
|
let options_: AxiosRequestConfig = {
|
||||||
|
data: content_,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: url_,
|
url: url_,
|
||||||
headers: {
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
"Accept": "application/json"
|
"Accept": "application/json"
|
||||||
},
|
},
|
||||||
cancelToken
|
cancelToken
|
||||||
|
@ -2834,11 +2839,11 @@ export class ExamClient {
|
||||||
throw _error;
|
throw _error;
|
||||||
}
|
}
|
||||||
}).then((_response: AxiosResponse) => {
|
}).then((_response: AxiosResponse) => {
|
||||||
return this.processScanExams(_response);
|
return this.processCreateExam(_response);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected processScanExams(response: AxiosResponse): Promise<ScanResult> {
|
protected processCreateExam(response: AxiosResponse): Promise<ExamInfo> {
|
||||||
const status = response.status;
|
const status = response.status;
|
||||||
let _headers: any = {};
|
let _headers: any = {};
|
||||||
if (response.headers && typeof response.headers === "object") {
|
if (response.headers && typeof response.headers === "object") {
|
||||||
|
@ -2848,12 +2853,19 @@ export class ExamClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (status === 200) {
|
if (status === 201) {
|
||||||
const _responseText = response.data;
|
const _responseText = response.data;
|
||||||
let result200: any = null;
|
let result201: any = null;
|
||||||
let resultData200 = _responseText;
|
let resultData201 = _responseText;
|
||||||
result200 = ScanResult.fromJS(resultData200);
|
result201 = ExamInfo.fromJS(resultData201);
|
||||||
return Promise.resolve<ScanResult>(result200);
|
return Promise.resolve<ExamInfo>(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) {
|
} else if (status === 401) {
|
||||||
const _responseText = response.data;
|
const _responseText = response.data;
|
||||||
|
@ -2869,6 +2881,13 @@ export class ExamClient {
|
||||||
result403 = ProblemDetails.fromJS(resultData403);
|
result403 = ProblemDetails.fromJS(resultData403);
|
||||||
return throwException("A server side error occurred.", status, _responseText, _headers, result403);
|
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) {
|
} else if (status === 500) {
|
||||||
const _responseText = response.data;
|
const _responseText = response.data;
|
||||||
return throwException("A server side error occurred.", status, _responseText, _headers);
|
return throwException("A server side error occurred.", status, _responseText, _headers);
|
||||||
|
@ -2877,7 +2896,278 @@ export class ExamClient {
|
||||||
const _responseText = response.data;
|
const _responseText = response.data;
|
||||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||||
}
|
}
|
||||||
return Promise.resolve<ScanResult>(null as any);
|
return Promise.resolve<ExamInfo>(null as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加实验资源
|
||||||
|
* @param examId 实验ID
|
||||||
|
* @param resourceType 资源类型
|
||||||
|
* @param file (optional) 资源文件
|
||||||
|
* @return 添加结果
|
||||||
|
*/
|
||||||
|
addExamResource(examId: string, resourceType: string, file: FileParameter | undefined, cancelToken?: CancelToken): Promise<ResourceInfo> {
|
||||||
|
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<ResourceInfo> {
|
||||||
|
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<ResourceInfo>(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<ResourceInfo>(null as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定实验ID的指定资源类型的所有资源的ID和名称
|
||||||
|
* @param examId 实验ID
|
||||||
|
* @param resourceType 资源类型
|
||||||
|
* @return 资源列表
|
||||||
|
*/
|
||||||
|
getExamResourceList(examId: string, resourceType: string, cancelToken?: CancelToken): Promise<ResourceInfo[]> {
|
||||||
|
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<ResourceInfo[]> {
|
||||||
|
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 = <any>null;
|
||||||
|
}
|
||||||
|
return Promise.resolve<ResourceInfo[]>(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<ResourceInfo[]>(null as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据资源ID下载资源
|
||||||
|
* @param resourceId 资源ID
|
||||||
|
* @return 资源文件
|
||||||
|
*/
|
||||||
|
getExamResourceById(resourceId: number, cancelToken?: CancelToken): Promise<FileResponse> {
|
||||||
|
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<FileResponse> {
|
||||||
|
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<FileResponse>(null as any);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7239,20 +7529,15 @@ export enum CaptureMode {
|
||||||
|
|
||||||
/** 调试器整体配置信息 */
|
/** 调试器整体配置信息 */
|
||||||
export class DebuggerConfig implements IDebuggerConfig {
|
export class DebuggerConfig implements IDebuggerConfig {
|
||||||
/** 时钟频率
|
/** 时钟频率 */
|
||||||
*/
|
|
||||||
clkFreq!: number;
|
clkFreq!: number;
|
||||||
/** 总端口数量
|
/** 总端口数量 */
|
||||||
*/
|
|
||||||
totalPortNum!: number;
|
totalPortNum!: number;
|
||||||
/** 捕获深度(采样点数)
|
/** 捕获深度(采样点数) */
|
||||||
*/
|
|
||||||
captureDepth!: number;
|
captureDepth!: number;
|
||||||
/** 触发器数量
|
/** 触发器数量 */
|
||||||
*/
|
|
||||||
triggerNum!: number;
|
triggerNum!: number;
|
||||||
/** 所有信号通道的配置信息
|
/** 所有信号通道的配置信息 */
|
||||||
*/
|
|
||||||
channelConfigs!: ChannelConfig[];
|
channelConfigs!: ChannelConfig[];
|
||||||
|
|
||||||
constructor(data?: IDebuggerConfig) {
|
constructor(data?: IDebuggerConfig) {
|
||||||
|
@ -7305,42 +7590,31 @@ export class DebuggerConfig implements IDebuggerConfig {
|
||||||
|
|
||||||
/** 调试器整体配置信息 */
|
/** 调试器整体配置信息 */
|
||||||
export interface IDebuggerConfig {
|
export interface IDebuggerConfig {
|
||||||
/** 时钟频率
|
/** 时钟频率 */
|
||||||
*/
|
|
||||||
clkFreq: number;
|
clkFreq: number;
|
||||||
/** 总端口数量
|
/** 总端口数量 */
|
||||||
*/
|
|
||||||
totalPortNum: number;
|
totalPortNum: number;
|
||||||
/** 捕获深度(采样点数)
|
/** 捕获深度(采样点数) */
|
||||||
*/
|
|
||||||
captureDepth: number;
|
captureDepth: number;
|
||||||
/** 触发器数量
|
/** 触发器数量 */
|
||||||
*/
|
|
||||||
triggerNum: number;
|
triggerNum: number;
|
||||||
/** 所有信号通道的配置信息
|
/** 所有信号通道的配置信息 */
|
||||||
*/
|
|
||||||
channelConfigs: ChannelConfig[];
|
channelConfigs: ChannelConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 表示单个信号通道的配置信息 */
|
/** 表示单个信号通道的配置信息 */
|
||||||
export class ChannelConfig implements IChannelConfig {
|
export class ChannelConfig implements IChannelConfig {
|
||||||
/** 通道名称
|
/** 通道名称 */
|
||||||
*/
|
|
||||||
name!: string;
|
name!: string;
|
||||||
/** 通道显示颜色(如前端波形显示用)
|
/** 通道显示颜色(如前端波形显示用) */
|
||||||
*/
|
|
||||||
color!: string;
|
color!: string;
|
||||||
/** 通道信号线宽度(位数)
|
/** 通道信号线宽度(位数) */
|
||||||
*/
|
|
||||||
wireWidth!: number;
|
wireWidth!: number;
|
||||||
/** 信号线在父端口中的起始索引(bit)
|
/** 信号线在父端口中的起始索引(bit) */
|
||||||
*/
|
|
||||||
wireStartIndex!: number;
|
wireStartIndex!: number;
|
||||||
/** 父端口编号
|
/** 父端口编号 */
|
||||||
*/
|
|
||||||
parentPort!: number;
|
parentPort!: number;
|
||||||
/** 捕获模式(如上升沿、下降沿等)
|
/** 捕获模式(如上升沿、下降沿等) */
|
||||||
*/
|
|
||||||
mode!: CaptureMode;
|
mode!: CaptureMode;
|
||||||
|
|
||||||
constructor(data?: IChannelConfig) {
|
constructor(data?: IChannelConfig) {
|
||||||
|
@ -7384,33 +7658,25 @@ export class ChannelConfig implements IChannelConfig {
|
||||||
|
|
||||||
/** 表示单个信号通道的配置信息 */
|
/** 表示单个信号通道的配置信息 */
|
||||||
export interface IChannelConfig {
|
export interface IChannelConfig {
|
||||||
/** 通道名称
|
/** 通道名称 */
|
||||||
*/
|
|
||||||
name: string;
|
name: string;
|
||||||
/** 通道显示颜色(如前端波形显示用)
|
/** 通道显示颜色(如前端波形显示用) */
|
||||||
*/
|
|
||||||
color: string;
|
color: string;
|
||||||
/** 通道信号线宽度(位数)
|
/** 通道信号线宽度(位数) */
|
||||||
*/
|
|
||||||
wireWidth: number;
|
wireWidth: number;
|
||||||
/** 信号线在父端口中的起始索引(bit)
|
/** 信号线在父端口中的起始索引(bit) */
|
||||||
*/
|
|
||||||
wireStartIndex: number;
|
wireStartIndex: number;
|
||||||
/** 父端口编号
|
/** 父端口编号 */
|
||||||
*/
|
|
||||||
parentPort: number;
|
parentPort: number;
|
||||||
/** 捕获模式(如上升沿、下降沿等)
|
/** 捕获模式(如上升沿、下降沿等) */
|
||||||
*/
|
|
||||||
mode: CaptureMode;
|
mode: CaptureMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 单个通道的捕获数据 */
|
/** 单个通道的捕获数据 */
|
||||||
export class ChannelCaptureData implements IChannelCaptureData {
|
export class ChannelCaptureData implements IChannelCaptureData {
|
||||||
/** 通道名称
|
/** 通道名称 */
|
||||||
*/
|
|
||||||
name!: string;
|
name!: string;
|
||||||
/** 通道捕获到的数据(Base64编码的UInt32数组)
|
/** 通道捕获到的数据(Base64编码的UInt32数组) */
|
||||||
*/
|
|
||||||
data!: string;
|
data!: string;
|
||||||
|
|
||||||
constructor(data?: IChannelCaptureData) {
|
constructor(data?: IChannelCaptureData) {
|
||||||
|
@ -7446,11 +7712,9 @@ export class ChannelCaptureData implements IChannelCaptureData {
|
||||||
|
|
||||||
/** 单个通道的捕获数据 */
|
/** 单个通道的捕获数据 */
|
||||||
export interface IChannelCaptureData {
|
export interface IChannelCaptureData {
|
||||||
/** 通道名称
|
/** 通道名称 */
|
||||||
*/
|
|
||||||
name: string;
|
name: string;
|
||||||
/** 通道捕获到的数据(Base64编码的UInt32数组)
|
/** 通道捕获到的数据(Base64编码的UInt32数组) */
|
||||||
*/
|
|
||||||
data: string;
|
data: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7458,12 +7722,18 @@ export interface IChannelCaptureData {
|
||||||
export class ExamSummary implements IExamSummary {
|
export class ExamSummary implements IExamSummary {
|
||||||
/** 实验的唯一标识符 */
|
/** 实验的唯一标识符 */
|
||||||
id!: string;
|
id!: string;
|
||||||
|
/** 实验名称 */
|
||||||
|
name!: string;
|
||||||
/** 实验创建时间 */
|
/** 实验创建时间 */
|
||||||
createdTime!: Date;
|
createdTime!: Date;
|
||||||
/** 实验最后更新时间 */
|
/** 实验最后更新时间 */
|
||||||
updatedTime!: Date;
|
updatedTime!: Date;
|
||||||
/** 实验标题(从文档内容中提取) */
|
/** 实验标签 */
|
||||||
title!: string;
|
tags!: string[];
|
||||||
|
/** 实验难度(1-5) */
|
||||||
|
difficulty!: number;
|
||||||
|
/** 普通用户是否可见 */
|
||||||
|
isVisibleToUsers!: boolean;
|
||||||
|
|
||||||
constructor(data?: IExamSummary) {
|
constructor(data?: IExamSummary) {
|
||||||
if (data) {
|
if (data) {
|
||||||
|
@ -7472,14 +7742,24 @@ export class ExamSummary implements IExamSummary {
|
||||||
(<any>this)[property] = (<any>data)[property];
|
(<any>this)[property] = (<any>data)[property];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!data) {
|
||||||
|
this.tags = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_data?: any) {
|
init(_data?: any) {
|
||||||
if (_data) {
|
if (_data) {
|
||||||
this.id = _data["id"];
|
this.id = _data["id"];
|
||||||
|
this.name = _data["name"];
|
||||||
this.createdTime = _data["createdTime"] ? new Date(_data["createdTime"].toString()) : <any>undefined;
|
this.createdTime = _data["createdTime"] ? new Date(_data["createdTime"].toString()) : <any>undefined;
|
||||||
this.updatedTime = _data["updatedTime"] ? new Date(_data["updatedTime"].toString()) : <any>undefined;
|
this.updatedTime = _data["updatedTime"] ? new Date(_data["updatedTime"].toString()) : <any>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) {
|
toJSON(data?: any) {
|
||||||
data = typeof data === 'object' ? data : {};
|
data = typeof data === 'object' ? data : {};
|
||||||
data["id"] = this.id;
|
data["id"] = this.id;
|
||||||
|
data["name"] = this.name;
|
||||||
data["createdTime"] = this.createdTime ? this.createdTime.toISOString() : <any>undefined;
|
data["createdTime"] = this.createdTime ? this.createdTime.toISOString() : <any>undefined;
|
||||||
data["updatedTime"] = this.updatedTime ? this.updatedTime.toISOString() : <any>undefined;
|
data["updatedTime"] = this.updatedTime ? this.updatedTime.toISOString() : <any>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;
|
return data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7504,24 +7791,38 @@ export class ExamSummary implements IExamSummary {
|
||||||
export interface IExamSummary {
|
export interface IExamSummary {
|
||||||
/** 实验的唯一标识符 */
|
/** 实验的唯一标识符 */
|
||||||
id: string;
|
id: string;
|
||||||
|
/** 实验名称 */
|
||||||
|
name: string;
|
||||||
/** 实验创建时间 */
|
/** 实验创建时间 */
|
||||||
createdTime: Date;
|
createdTime: Date;
|
||||||
/** 实验最后更新时间 */
|
/** 实验最后更新时间 */
|
||||||
updatedTime: Date;
|
updatedTime: Date;
|
||||||
/** 实验标题(从文档内容中提取) */
|
/** 实验标签 */
|
||||||
title: string;
|
tags: string[];
|
||||||
|
/** 实验难度(1-5) */
|
||||||
|
difficulty: number;
|
||||||
|
/** 普通用户是否可见 */
|
||||||
|
isVisibleToUsers: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 实验信息类 */
|
/** 实验信息类 */
|
||||||
export class ExamInfo implements IExamInfo {
|
export class ExamInfo implements IExamInfo {
|
||||||
/** 实验的唯一标识符 */
|
/** 实验的唯一标识符 */
|
||||||
id!: string;
|
id!: string;
|
||||||
/** 实验文档内容(Markdown格式) */
|
/** 实验名称 */
|
||||||
docContent!: string;
|
name!: string;
|
||||||
|
/** 实验描述 */
|
||||||
|
description!: string;
|
||||||
/** 实验创建时间 */
|
/** 实验创建时间 */
|
||||||
createdTime!: Date;
|
createdTime!: Date;
|
||||||
/** 实验最后更新时间 */
|
/** 实验最后更新时间 */
|
||||||
updatedTime!: Date;
|
updatedTime!: Date;
|
||||||
|
/** 实验标签 */
|
||||||
|
tags!: string[];
|
||||||
|
/** 实验难度(1-5) */
|
||||||
|
difficulty!: number;
|
||||||
|
/** 普通用户是否可见 */
|
||||||
|
isVisibleToUsers!: boolean;
|
||||||
|
|
||||||
constructor(data?: IExamInfo) {
|
constructor(data?: IExamInfo) {
|
||||||
if (data) {
|
if (data) {
|
||||||
|
@ -7530,14 +7831,25 @@ export class ExamInfo implements IExamInfo {
|
||||||
(<any>this)[property] = (<any>data)[property];
|
(<any>this)[property] = (<any>data)[property];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!data) {
|
||||||
|
this.tags = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_data?: any) {
|
init(_data?: any) {
|
||||||
if (_data) {
|
if (_data) {
|
||||||
this.id = _data["id"];
|
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()) : <any>undefined;
|
this.createdTime = _data["createdTime"] ? new Date(_data["createdTime"].toString()) : <any>undefined;
|
||||||
this.updatedTime = _data["updatedTime"] ? new Date(_data["updatedTime"].toString()) : <any>undefined;
|
this.updatedTime = _data["updatedTime"] ? new Date(_data["updatedTime"].toString()) : <any>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) {
|
toJSON(data?: any) {
|
||||||
data = typeof data === 'object' ? data : {};
|
data = typeof data === 'object' ? data : {};
|
||||||
data["id"] = this.id;
|
data["id"] = this.id;
|
||||||
data["docContent"] = this.docContent;
|
data["name"] = this.name;
|
||||||
|
data["description"] = this.description;
|
||||||
data["createdTime"] = this.createdTime ? this.createdTime.toISOString() : <any>undefined;
|
data["createdTime"] = this.createdTime ? this.createdTime.toISOString() : <any>undefined;
|
||||||
data["updatedTime"] = this.updatedTime ? this.updatedTime.toISOString() : <any>undefined;
|
data["updatedTime"] = this.updatedTime ? this.updatedTime.toISOString() : <any>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;
|
return data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7562,22 +7882,111 @@ export class ExamInfo implements IExamInfo {
|
||||||
export interface IExamInfo {
|
export interface IExamInfo {
|
||||||
/** 实验的唯一标识符 */
|
/** 实验的唯一标识符 */
|
||||||
id: string;
|
id: string;
|
||||||
/** 实验文档内容(Markdown格式) */
|
/** 实验名称 */
|
||||||
docContent: string;
|
name: string;
|
||||||
|
/** 实验描述 */
|
||||||
|
description: string;
|
||||||
/** 实验创建时间 */
|
/** 实验创建时间 */
|
||||||
createdTime: Date;
|
createdTime: Date;
|
||||||
/** 实验最后更新时间 */
|
/** 实验最后更新时间 */
|
||||||
updatedTime: Date;
|
updatedTime: Date;
|
||||||
|
/** 实验标签 */
|
||||||
|
tags: string[];
|
||||||
|
/** 实验难度(1-5) */
|
||||||
|
difficulty: number;
|
||||||
|
/** 普通用户是否可见 */
|
||||||
|
isVisibleToUsers: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 扫描结果类 */
|
/** 创建实验请求类 */
|
||||||
export class ScanResult implements IScanResult {
|
export class CreateExamRequest implements ICreateExamRequest {
|
||||||
/** 结果消息 */
|
/** 实验ID */
|
||||||
declare message: string;
|
id!: string;
|
||||||
/** 更新的实验数量 */
|
/** 实验名称 */
|
||||||
updateCount!: number;
|
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))
|
||||||
|
(<any>this)[property] = (<any>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) {
|
if (data) {
|
||||||
for (var property in data) {
|
for (var property in data) {
|
||||||
if (data.hasOwnProperty(property))
|
if (data.hasOwnProperty(property))
|
||||||
|
@ -7588,32 +7997,32 @@ export class ScanResult implements IScanResult {
|
||||||
|
|
||||||
init(_data?: any) {
|
init(_data?: any) {
|
||||||
if (_data) {
|
if (_data) {
|
||||||
this.message = _data["message"];
|
this.id = _data["id"];
|
||||||
this.updateCount = _data["updateCount"];
|
this.name = _data["name"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJS(data: any): ScanResult {
|
static fromJS(data: any): ResourceInfo {
|
||||||
data = typeof data === 'object' ? data : {};
|
data = typeof data === 'object' ? data : {};
|
||||||
let result = new ScanResult();
|
let result = new ResourceInfo();
|
||||||
result.init(data);
|
result.init(data);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON(data?: any) {
|
toJSON(data?: any) {
|
||||||
data = typeof data === 'object' ? data : {};
|
data = typeof data === 'object' ? data : {};
|
||||||
data["message"] = this.message;
|
data["id"] = this.id;
|
||||||
data["updateCount"] = this.updateCount;
|
data["name"] = this.name;
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 扫描结果类 */
|
/** 资源信息类 */
|
||||||
export interface IScanResult {
|
export interface IResourceInfo {
|
||||||
/** 结果消息 */
|
/** 资源ID */
|
||||||
message: string;
|
id: number;
|
||||||
/** 更新的实验数量 */
|
/** 资源名称 */
|
||||||
updateCount: number;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 逻辑分析仪运行状态枚举 */
|
/** 逻辑分析仪运行状态枚举 */
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="fixed left-1/2 top-30 z-999 -translate-x-1/2">
|
<div class="fixed left-1/2 top-30 z-[9999] -translate-x-1/2">
|
||||||
<transition
|
<transition
|
||||||
name="alert"
|
name="alert"
|
||||||
enter-active-class="alert-enter-active"
|
enter-active-class="alert-enter-active"
|
||||||
|
|
|
@ -119,6 +119,7 @@
|
||||||
componentManager.prepareComponentProps(
|
componentManager.prepareComponentProps(
|
||||||
component.attrs || {},
|
component.attrs || {},
|
||||||
component.id,
|
component.id,
|
||||||
|
props.examId,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
@update:bindKey="
|
@update:bindKey="
|
||||||
|
@ -175,9 +176,7 @@ import {
|
||||||
ref,
|
ref,
|
||||||
reactive,
|
reactive,
|
||||||
onMounted,
|
onMounted,
|
||||||
onUnmounted,
|
|
||||||
computed,
|
computed,
|
||||||
watch,
|
|
||||||
provide,
|
provide,
|
||||||
} from "vue";
|
} from "vue";
|
||||||
import { useEventListener } from "@vueuse/core";
|
import { useEventListener } from "@vueuse/core";
|
||||||
|
@ -217,6 +216,7 @@ const emit = defineEmits(["toggle-doc-panel", "open-components"]);
|
||||||
// 定义组件接受的属性
|
// 定义组件接受的属性
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
showDocPanel?: boolean; // 添加属性接收文档面板的显示状态
|
showDocPanel?: boolean; // 添加属性接收文档面板的显示状态
|
||||||
|
examId?: string; // 新增examId属性
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// 获取componentManager实例
|
// 获取componentManager实例
|
||||||
|
@ -977,7 +977,8 @@ function exportDiagram() {
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 加载图表数据
|
// 加载图表数据
|
||||||
try {
|
try {
|
||||||
diagramData.value = await loadDiagramData();
|
// 传入examId参数,让diagramManager处理动态加载
|
||||||
|
diagramData.value = await loadDiagramData(props.examId);
|
||||||
|
|
||||||
// 预加载所有组件模块
|
// 预加载所有组件模块
|
||||||
const componentTypes = new Set<string>();
|
const componentTypes = new Set<string>();
|
||||||
|
|
|
@ -763,11 +763,15 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||||
function prepareComponentProps(
|
function prepareComponentProps(
|
||||||
attrs: Record<string, any>,
|
attrs: Record<string, any>,
|
||||||
componentId?: string,
|
componentId?: string,
|
||||||
|
examId?: string,
|
||||||
): Record<string, any> {
|
): Record<string, any> {
|
||||||
const result: Record<string, any> = { ...attrs };
|
const result: Record<string, any> = { ...attrs };
|
||||||
if (componentId) {
|
if (componentId) {
|
||||||
result.componentId = componentId;
|
result.componentId = componentId;
|
||||||
}
|
}
|
||||||
|
if (examId) {
|
||||||
|
result.examId = examId;
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,8 @@ export interface DiagramPart {
|
||||||
// 连接类型定义 - 使用元组类型表示四元素数组
|
// 连接类型定义 - 使用元组类型表示四元素数组
|
||||||
export type ConnectionArray = [string, string, number, string[]];
|
export type ConnectionArray = [string, string, number, string[]];
|
||||||
|
|
||||||
|
import { AuthManager } from '@/utils/AuthManager';
|
||||||
|
|
||||||
// 解析连接字符串为组件ID和引脚ID
|
// 解析连接字符串为组件ID和引脚ID
|
||||||
export function parseConnectionPin(connectionPin: string): { componentId: string; pinId: string } {
|
export function parseConnectionPin(connectionPin: string): { componentId: string; pinId: string } {
|
||||||
const [componentId, pinId] = connectionPin.split(':');
|
const [componentId, pinId] = connectionPin.split(':');
|
||||||
|
@ -80,22 +82,72 @@ export interface WireItem {
|
||||||
showLabel: boolean;
|
showLabel: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从本地存储加载图表数据
|
// 从本地存储或动态API加载图表数据
|
||||||
export async function loadDiagramData(): Promise<DiagramData> {
|
export async function loadDiagramData(examId?: string): Promise<DiagramData> {
|
||||||
try {
|
try {
|
||||||
// 先尝试从本地存储加载
|
// 如果提供了examId,优先从API加载实验的diagram
|
||||||
const savedData = localStorage.getItem('diagramData');
|
if (examId) {
|
||||||
if (savedData) {
|
try {
|
||||||
return JSON.parse(savedData);
|
const examClient = AuthManager.createAuthenticatedExamClient();
|
||||||
|
|
||||||
|
// 获取diagram类型的资源列表
|
||||||
|
const resources = await examClient.getExamResourceList(examId, 'canvas');
|
||||||
|
|
||||||
|
if (resources && resources.length > 0) {
|
||||||
|
// 获取第一个diagram资源
|
||||||
|
const diagramResource = resources[0];
|
||||||
|
|
||||||
|
// 使用动态API获取资源文件内容
|
||||||
|
const response = await examClient.getExamResourceById(diagramResource.id);
|
||||||
|
|
||||||
|
if (response && response.data) {
|
||||||
|
const text = await response.data.text();
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
|
||||||
|
// 验证数据格式
|
||||||
|
const validation = validateDiagramData(data);
|
||||||
|
if (validation.isValid) {
|
||||||
|
console.log('成功从API加载实验diagram:', examId);
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
console.warn('API返回的diagram数据格式无效:', validation.errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('未找到实验diagram资源,使用默认加载方式');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('从API加载实验diagram失败,使用默认加载方式:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果本地存储没有,从文件加载
|
// 如果没有examId或API加载失败,尝试从本地存储加载
|
||||||
|
const savedData = localStorage.getItem('diagramData');
|
||||||
|
if (savedData) {
|
||||||
|
const data = JSON.parse(savedData);
|
||||||
|
const validation = validateDiagramData(data);
|
||||||
|
if (validation.isValid) {
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
console.warn('本地存储的diagram数据格式无效:', validation.errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果本地存储也没有,从静态文件加载(作为最后的备选)
|
||||||
const response = await fetch('/src/components/diagram.json');
|
const response = await fetch('/src/components/diagram.json');
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to load diagram.json: ${response.statusText}`);
|
throw new Error(`Failed to load diagram.json: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
|
||||||
|
// 验证静态文件数据
|
||||||
|
const validation = validateDiagramData(data);
|
||||||
|
if (validation.isValid) {
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
console.warn('静态diagram文件数据格式无效:', validation.errors);
|
||||||
|
throw new Error('所有diagram数据源都无效');
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading diagram data:', error);
|
console.error('Error loading diagram data:', error);
|
||||||
// 返回空的默认数据结构
|
// 返回空的默认数据结构
|
||||||
|
|
|
@ -588,9 +588,9 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
||||||
};
|
};
|
||||||
|
|
||||||
const forceCapture = async () => {
|
const forceCapture = async () => {
|
||||||
// 检查是否有其他操作正在进行
|
// 检查是否正在捕获
|
||||||
if (operationMutex.isLocked()) {
|
if (!isCapturing.value) {
|
||||||
alert.warn("有其他操作正在进行中,请稍后再试", 3000);
|
alert.warn("当前没有正在进行的捕获操作", 2000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,9 @@ import hljs from 'highlight.js';
|
||||||
import 'highlight.js/styles/github.css'; // 亮色主题
|
import 'highlight.js/styles/github.css'; // 亮色主题
|
||||||
// 导入主题存储
|
// 导入主题存储
|
||||||
import { useThemeStore } from '@/stores/theme';
|
import { useThemeStore } from '@/stores/theme';
|
||||||
|
// 导入ExamClient用于获取图片资源
|
||||||
|
import { ExamClient } from '@/APIClient';
|
||||||
|
import { AuthManager } from '@/utils/AuthManager';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
content: {
|
content: {
|
||||||
|
@ -15,6 +18,10 @@ const props = defineProps({
|
||||||
removeFirstH1: {
|
removeFirstH1: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
examId: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -23,6 +30,42 @@ const themeStore = useThemeStore();
|
||||||
// 使用 isDarkTheme 函数来检查当前是否为暗色主题
|
// 使用 isDarkTheme 函数来检查当前是否为暗色主题
|
||||||
const isDarkMode = computed(() => themeStore.isDarkTheme());
|
const isDarkMode = computed(() => themeStore.isDarkTheme());
|
||||||
|
|
||||||
|
// 图片资源缓存
|
||||||
|
const imageResourceCache = ref<Map<string, string>>(new Map());
|
||||||
|
|
||||||
|
// 获取图片资源ID的函数
|
||||||
|
async function getImageResourceId(examId: string, imagePath: string): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const client = AuthManager.createAuthenticatedExamClient();
|
||||||
|
const resources = await client.getExamResourceList(examId, 'images');
|
||||||
|
|
||||||
|
// 查找匹配的图片资源
|
||||||
|
const imageResource = resources.find(r => r.name === imagePath || r.name.endsWith(imagePath));
|
||||||
|
|
||||||
|
return imageResource ? imageResource.id.toString() : null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取图片资源ID失败:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过资源ID获取图片数据URL
|
||||||
|
async function getImageDataUrl(resourceId: string): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const client = AuthManager.createAuthenticatedExamClient();
|
||||||
|
const response = await client.getExamResourceById(parseInt(resourceId));
|
||||||
|
|
||||||
|
if (response && response.data) {
|
||||||
|
return URL.createObjectURL(response.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取图片数据失败:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 监听主题变化
|
// 监听主题变化
|
||||||
watch(() => themeStore.currentTheme, () => {
|
watch(() => themeStore.currentTheme, () => {
|
||||||
// 主题变化时更新代码高亮样式
|
// 主题变化时更新代码高亮样式
|
||||||
|
@ -54,6 +97,27 @@ const renderedContent = computed(() => {
|
||||||
// 创建自定义渲染器
|
// 创建自定义渲染器
|
||||||
const renderer = new marked.Renderer();
|
const renderer = new marked.Renderer();
|
||||||
|
|
||||||
|
// 重写图片渲染方法,处理相对路径
|
||||||
|
renderer.image = (href, title, text) => {
|
||||||
|
let src = href;
|
||||||
|
|
||||||
|
console.log(`原始图片路径: ${href}, examId: ${props.examId}`);
|
||||||
|
|
||||||
|
// 如果是相对路径且有实验ID,需要通过动态API获取
|
||||||
|
if (props.examId && href && href.startsWith('./')) {
|
||||||
|
// 对于相对路径的图片,我们需要先获取图片资源ID,然后通过动态API获取
|
||||||
|
// 暂时保留原始路径,在后处理中进行替换
|
||||||
|
src = href;
|
||||||
|
console.log(`保留原始路径用于后处理: ${src}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const titleAttr = title ? ` title="${title}"` : '';
|
||||||
|
const altAttr = text ? ` alt="${text}"` : '';
|
||||||
|
const dataOriginal = href && href.startsWith('./') ? ` data-original-src="${href}"` : '';
|
||||||
|
console.log(`最终渲染的HTML: <img src="${src}"${titleAttr}${altAttr}${dataOriginal} />`);
|
||||||
|
return `<img src="${src}"${titleAttr}${altAttr}${dataOriginal} />`;
|
||||||
|
};
|
||||||
|
|
||||||
// 重写代码块渲染方法,添加语言信息
|
// 重写代码块渲染方法,添加语言信息
|
||||||
renderer.code = (code, incomingLanguage) => {
|
renderer.code = (code, incomingLanguage) => {
|
||||||
// 确保语言参数是字符串
|
// 确保语言参数是字符串
|
||||||
|
@ -67,14 +131,60 @@ const renderedContent = computed(() => {
|
||||||
return `<pre class="hljs" data-language="${validLanguage}"><code class="language-${validLanguage}">${highlightedCode}</code></pre>`;
|
return `<pre class="hljs" data-language="${validLanguage}"><code class="language-${validLanguage}">${highlightedCode}</code></pre>`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 设置 marked 选项
|
// 设置 marked 选项并解析内容
|
||||||
marked.use({
|
let html = marked.parse(processedContent, {
|
||||||
renderer: renderer,
|
renderer: renderer,
|
||||||
gfm: true,
|
gfm: true,
|
||||||
breaks: true
|
breaks: true
|
||||||
});
|
}) as string;
|
||||||
|
|
||||||
return marked(processedContent);
|
// 后处理HTML,异步处理图片
|
||||||
|
if (props.examId) {
|
||||||
|
// 查找所有需要处理的图片
|
||||||
|
const imgMatches = Array.from(html.matchAll(/(<img[^>]+data-original-src=["'])\.\/([^"']+)(["'][^>]*>)/g));
|
||||||
|
|
||||||
|
// 异步处理每个图片
|
||||||
|
imgMatches.forEach(async (match) => {
|
||||||
|
const [fullMatch, prefix, path, suffix] = match;
|
||||||
|
const imagePath = path.replace('images/', '');
|
||||||
|
|
||||||
|
// 检查缓存
|
||||||
|
if (imageResourceCache.value.has(imagePath)) {
|
||||||
|
const cachedUrl = imageResourceCache.value.get(imagePath)!;
|
||||||
|
html = html.replace(fullMatch, `${prefix}${cachedUrl}${suffix.replace(' data-original-src="./'+path+'"', '')}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取图片资源ID
|
||||||
|
const resourceId = await getImageResourceId(props.examId, imagePath);
|
||||||
|
if (resourceId) {
|
||||||
|
// 获取图片数据URL
|
||||||
|
const dataUrl = await getImageDataUrl(resourceId);
|
||||||
|
if (dataUrl) {
|
||||||
|
// 缓存URL
|
||||||
|
imageResourceCache.value.set(imagePath, dataUrl);
|
||||||
|
|
||||||
|
// 更新HTML中的图片src
|
||||||
|
const updatedHtml = html.replace(fullMatch, `${prefix}${dataUrl}${suffix.replace(' data-original-src="./'+path+'"', '')}`);
|
||||||
|
|
||||||
|
// 触发重新渲染
|
||||||
|
setTimeout(() => {
|
||||||
|
const imgElements = document.querySelectorAll(`img[data-original-src="./${path}"]`);
|
||||||
|
imgElements.forEach(img => {
|
||||||
|
(img as HTMLImageElement).src = dataUrl;
|
||||||
|
img.removeAttribute('data-original-src');
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`处理图片 ${imagePath} 失败:`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 页面挂载后,确保应用正确的主题样式
|
// 页面挂载后,确保应用正确的主题样式
|
||||||
|
|
|
@ -31,8 +31,20 @@
|
||||||
|
|
||||||
<!-- 标题覆盖层 -->
|
<!-- 标题覆盖层 -->
|
||||||
<div class="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-base-300 to-transparent">
|
<div class="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-base-300 to-transparent">
|
||||||
<h3 class="text-lg font-bold text-base-content">{{ tutorial.title }}</h3>
|
<div class="flex flex-col gap-2">
|
||||||
<p class="text-sm opacity-80 truncate">{{ tutorial.description }}</p>
|
<h3 class="text-lg font-bold text-base-content">{{ tutorial.title }}</h3>
|
||||||
|
<p class="text-sm opacity-80 truncate">{{ tutorial.description }}</p>
|
||||||
|
<!-- 标签显示 -->
|
||||||
|
<div v-if="tutorial.tags && tutorial.tags.length > 0" class="flex flex-wrap gap-1">
|
||||||
|
<span
|
||||||
|
v-for="tag in tutorial.tags.slice(0, 3)"
|
||||||
|
:key="tag"
|
||||||
|
class="badge badge-outline badge-xs text-xs"
|
||||||
|
>
|
||||||
|
{{ tag }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -54,6 +66,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted } from 'vue';
|
import { ref, onMounted, onUnmounted } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
import { AuthManager } from '@/utils/AuthManager';
|
||||||
|
import type { ExamSummary } from '@/APIClient';
|
||||||
|
|
||||||
// 接口定义
|
// 接口定义
|
||||||
interface Tutorial {
|
interface Tutorial {
|
||||||
|
@ -61,7 +75,7 @@ interface Tutorial {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
thumbnail?: string;
|
thumbnail?: string;
|
||||||
docPath: string;
|
tags: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
|
@ -81,87 +95,92 @@ let autoRotationTimer: number | null = null;
|
||||||
// 处理卡片点击
|
// 处理卡片点击
|
||||||
const handleCardClick = (index: number, tutorialId: string) => {
|
const handleCardClick = (index: number, tutorialId: string) => {
|
||||||
if (index === currentIndex.value) {
|
if (index === currentIndex.value) {
|
||||||
goToTutorial(tutorialId);
|
goToExam(tutorialId);
|
||||||
} else {
|
} else {
|
||||||
setActiveCard(index);
|
setActiveCard(index);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 从 public/doc 目录加载例程信息
|
// 从数据库加载实验数据
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
// 尝试从API获取教程目录
|
console.log('正在从数据库加载实验数据...');
|
||||||
let tutorialIds: string[] = [];
|
|
||||||
try {
|
// 创建认证客户端
|
||||||
const response = await fetch('/api/tutorial');
|
const client = AuthManager.createAuthenticatedExamClient();
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
// 获取实验列表
|
||||||
tutorialIds = data.tutorials || [];
|
const examList: ExamSummary[] = await client.getExamList();
|
||||||
}
|
|
||||||
} catch (error) {
|
// 筛选可见的实验并转换为Tutorial格式
|
||||||
console.warn('无法从API获取教程目录,使用默认值:', error);
|
const visibleExams = examList
|
||||||
|
.filter(exam => exam.isVisibleToUsers)
|
||||||
|
.slice(0, 6); // 限制轮播显示最多6个实验
|
||||||
|
|
||||||
|
if (visibleExams.length === 0) {
|
||||||
|
console.warn('没有找到可见的实验');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果API调用失败或返回空列表,使用默认值
|
// 转换数据格式并获取封面图片
|
||||||
if (tutorialIds.length === 0) {
|
const tutorialPromises = visibleExams.map(async (exam) => {
|
||||||
console.log('使用默认教程列表');
|
let thumbnail: string | undefined;
|
||||||
tutorialIds = ['01', '02', '03', '04', '05', '06', '11', '12', '13']; // 默认例程
|
|
||||||
} else {
|
|
||||||
console.log('使用API获取的教程列表:', tutorialIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 为每个例程创建对象并尝试获取文档标题
|
|
||||||
const tutorialPromises = tutorialIds.map(async (id) => {
|
|
||||||
// 尝试读取doc.md获取标题
|
|
||||||
let title = `例程 ${id}`;
|
|
||||||
let description = "点击加载此例程";
|
|
||||||
let thumbnail = `/doc/${id}/cover.png`; // 默认使用第一张图片作为缩略图
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 尝试读取文档内容获取标题
|
// 获取实验的封面资源
|
||||||
const response = await fetch(`/doc/${id}/doc.md`);
|
const resourceList = await client.getExamResourceList(exam.id, 'cover');
|
||||||
if (response.ok) {
|
if (resourceList && resourceList.length > 0) {
|
||||||
const text = await response.text();
|
// 使用第一个封面资源
|
||||||
// 从Markdown提取标题
|
const coverResource = resourceList[0];
|
||||||
const titleMatch = text.match(/^#\s+(.+)$/m);
|
const fileResponse = await client.getExamResourceById(coverResource.id);
|
||||||
if (titleMatch && titleMatch[1]) {
|
// 创建Blob URL作为缩略图
|
||||||
title = titleMatch[1].trim();
|
thumbnail = URL.createObjectURL(fileResponse.data);
|
||||||
}
|
|
||||||
|
|
||||||
// 提取第一段作为描述
|
|
||||||
const descMatch = text.match(/\n\n([^#\n][^\n]+)/);
|
|
||||||
if (descMatch && descMatch[1]) {
|
|
||||||
description = descMatch[1].substring(0, 100).trim();
|
|
||||||
if (description.length === 100) description += '...';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`无法读取例程${id}的文档内容:`, error);
|
console.warn(`无法获取实验${exam.id}的封面图片:`, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id: exam.id,
|
||||||
title,
|
title: exam.name,
|
||||||
description,
|
description: '点击查看实验详情',
|
||||||
thumbnail,
|
thumbnail,
|
||||||
docPath: `/doc/${id}/doc.md`
|
tags: exam.tags || []
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
tutorials.value = await Promise.all(tutorialPromises);
|
tutorials.value = await Promise.all(tutorialPromises);
|
||||||
|
|
||||||
|
console.log('成功加载实验数据:', tutorials.value.length, '个实验');
|
||||||
|
|
||||||
// 启动自动旋转
|
// 启动自动旋转
|
||||||
startAutoRotation();
|
startAutoRotation();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载例程失败:', error);
|
console.error('加载实验数据失败:', error);
|
||||||
|
|
||||||
|
// 如果加载失败,显示默认的占位内容
|
||||||
|
tutorials.value = [{
|
||||||
|
id: 'placeholder',
|
||||||
|
title: '实验数据加载中...',
|
||||||
|
description: '请稍后或刷新页面重试',
|
||||||
|
thumbnail: undefined,
|
||||||
|
tags: []
|
||||||
|
}];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 在组件销毁时清除计时器
|
// 在组件销毁时清除计时器和Blob URLs
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (autoRotationTimer) {
|
if (autoRotationTimer) {
|
||||||
clearInterval(autoRotationTimer);
|
clearInterval(autoRotationTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清理创建的Blob URLs
|
||||||
|
tutorials.value.forEach(tutorial => {
|
||||||
|
if (tutorial.thumbnail && tutorial.thumbnail.startsWith('blob:')) {
|
||||||
|
URL.revokeObjectURL(tutorial.thumbnail);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 鼠标滚轮处理
|
// 鼠标滚轮处理
|
||||||
|
@ -210,12 +229,12 @@ const resumeAutoRotation = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 前往例程
|
// 前往实验
|
||||||
const goToTutorial = (tutorialId: string) => {
|
const goToExam = (examId: string) => {
|
||||||
// 跳转到工程页面,并通过 query 参数传递文档路径
|
// 跳转到实验列表页面并传递examId参数,页面将自动打开对应的实验详情模态框
|
||||||
router.push({
|
router.push({
|
||||||
path: '/project',
|
path: '/exam',
|
||||||
query: { tutorial: tutorialId }
|
query: { examId: examId }
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,64 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col bg-base-100 justify-center items-center">
|
<div class="flex flex-col bg-base-100 justify-center items-center gap-4">
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<h1 class="font-bold text-2xl">上传比特流文件</h1>
|
<h1 class="font-bold text-2xl">比特流文件</h1>
|
||||||
|
|
||||||
|
<!-- 示例比特流下载区域 (仅在有examId时显示) -->
|
||||||
|
<div v-if="examId && availableBitstreams.length > 0" class="w-full">
|
||||||
|
<fieldset class="fieldset w-full">
|
||||||
|
<legend class="fieldset-legend text-sm">示例比特流文件</legend>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div v-for="bitstream in availableBitstreams" :key="bitstream.id" class="flex items-center justify-between p-2 bg-base-200 rounded">
|
||||||
|
<span class="text-sm">{{ bitstream.name }}</span>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
@click="downloadExampleBitstream(bitstream)"
|
||||||
|
class="btn btn-sm btn-secondary"
|
||||||
|
:disabled="isDownloading || isProgramming"
|
||||||
|
>
|
||||||
|
<div v-if="isDownloading">
|
||||||
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
|
下载中...
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
下载示例
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="programExampleBitstream(bitstream)"
|
||||||
|
class="btn btn-sm btn-primary"
|
||||||
|
:disabled="isDownloading || isProgramming || !uploadEvent"
|
||||||
|
>
|
||||||
|
<div v-if="isProgramming">
|
||||||
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
|
烧录中...
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
直接烧录
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分割线 -->
|
||||||
|
<div v-if="examId && availableBitstreams.length > 0" class="divider">或</div>
|
||||||
|
|
||||||
<!-- Input File -->
|
<!-- Input File -->
|
||||||
<fieldset class="fieldset w-full">
|
<fieldset class="fieldset w-full">
|
||||||
<legend class="fieldset-legend text-sm">选择或拖拽上传文件</legend>
|
<legend class="fieldset-legend text-sm">上传自定义比特流文件</legend>
|
||||||
<input type="file" ref="fileInput" class="file-input w-full" @change="handleFileChange" />
|
<input type="file" ref="fileInput" class="file-input w-full" @change="handleFileChange" />
|
||||||
<label class="fieldset-label">文件最大容量: {{ maxMemory }}MB</label>
|
<label class="fieldset-label">文件最大容量: {{ maxMemory }}MB</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<!-- Upload Button -->
|
<!-- Upload Button -->
|
||||||
<div class="card-actions w-full">
|
<div class="card-actions w-full">
|
||||||
<button @click="handleClick" class="btn btn-primary grow" :disabled="isUploading">
|
<button @click="handleClick" class="btn btn-primary grow" :disabled="isUploading || isProgramming">
|
||||||
<div v-if="isUploading">
|
<div v-if="isUploading">
|
||||||
<span class="loading loading-spinner"></span>
|
<span class="loading loading-spinner"></span>
|
||||||
下载中...
|
上传中...
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
{{ buttonText }}
|
{{ buttonText }}
|
||||||
|
@ -27,6 +70,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref, useTemplateRef, onMounted } from "vue";
|
import { computed, ref, useTemplateRef, onMounted } from "vue";
|
||||||
|
import { AuthManager } from "@/utils/AuthManager"; // Adjust the path based on your project structure
|
||||||
import { useDialogStore } from "@/stores/dialog";
|
import { useDialogStore } from "@/stores/dialog";
|
||||||
import { isNull, isUndefined } from "lodash";
|
import { isNull, isUndefined } from "lodash";
|
||||||
|
|
||||||
|
@ -34,10 +78,12 @@ interface Props {
|
||||||
uploadEvent?: (file: File) => Promise<boolean>;
|
uploadEvent?: (file: File) => Promise<boolean>;
|
||||||
downloadEvent?: () => Promise<boolean>;
|
downloadEvent?: () => Promise<boolean>;
|
||||||
maxMemory?: number;
|
maxMemory?: number;
|
||||||
|
examId?: string; // 新增examId属性
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
maxMemory: 4,
|
maxMemory: 4,
|
||||||
|
examId: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const emits = defineEmits<{
|
const emits = defineEmits<{
|
||||||
|
@ -47,6 +93,10 @@ const emits = defineEmits<{
|
||||||
const dialog = useDialogStore();
|
const dialog = useDialogStore();
|
||||||
|
|
||||||
const isUploading = ref(false);
|
const isUploading = ref(false);
|
||||||
|
const isDownloading = ref(false);
|
||||||
|
const isProgramming = ref(false);
|
||||||
|
const availableBitstreams = ref<{id: number, name: string}[]>([]);
|
||||||
|
|
||||||
const buttonText = computed(() => {
|
const buttonText = computed(() => {
|
||||||
return isUndefined(props.downloadEvent) ? "上传" : "上传并下载";
|
return isUndefined(props.downloadEvent) ? "上传" : "上传并下载";
|
||||||
});
|
});
|
||||||
|
@ -56,14 +106,113 @@ const bitstream = defineModel("bitstreamFile", {
|
||||||
type: File,
|
type: File,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
});
|
});
|
||||||
onMounted(() => {
|
|
||||||
|
// 初始化时加载示例比特流
|
||||||
|
onMounted(async () => {
|
||||||
if (!isUndefined(bitstream.value) && !isNull(fileInput.value)) {
|
if (!isUndefined(bitstream.value) && !isNull(fileInput.value)) {
|
||||||
let fileList = new DataTransfer();
|
let fileList = new DataTransfer();
|
||||||
fileList.items.add(bitstream.value);
|
fileList.items.add(bitstream.value);
|
||||||
fileInput.value.files = fileList.files;
|
fileInput.value.files = fileList.files;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await loadAvailableBitstreams();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 加载可用的比特流文件列表
|
||||||
|
async function loadAvailableBitstreams() {
|
||||||
|
console.log('加载可用比特流文件,examId:', props.examId);
|
||||||
|
if (!props.examId) {
|
||||||
|
availableBitstreams.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const examClient = AuthManager.createAuthenticatedExamClient();
|
||||||
|
// 使用新的API获取比特流资源列表
|
||||||
|
const resources = await examClient.getExamResourceList(props.examId, 'bitstream');
|
||||||
|
availableBitstreams.value = resources.map(r => ({ id: r.id, name: r.name })) || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载比特流列表失败:', error);
|
||||||
|
availableBitstreams.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载示例比特流
|
||||||
|
async function downloadExampleBitstream(bitstream: {id: number, name: string}) {
|
||||||
|
if (isDownloading.value) return;
|
||||||
|
|
||||||
|
isDownloading.value = true;
|
||||||
|
try {
|
||||||
|
const examClient = AuthManager.createAuthenticatedExamClient();
|
||||||
|
|
||||||
|
// 使用动态API获取资源文件
|
||||||
|
const response = await examClient.getExamResourceById(bitstream.id);
|
||||||
|
|
||||||
|
if (response && response.data) {
|
||||||
|
// 创建下载链接
|
||||||
|
const url = URL.createObjectURL(response.data);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = response.fileName || bitstream.name;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
dialog.info("示例比特流下载成功");
|
||||||
|
} else {
|
||||||
|
dialog.error("下载失败:响应数据为空");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('下载示例比特流失败:', error);
|
||||||
|
dialog.error("下载示例比特流失败");
|
||||||
|
} finally {
|
||||||
|
isDownloading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接烧录示例比特流
|
||||||
|
async function programExampleBitstream(bitstream: {id: number, name: string}) {
|
||||||
|
if (isProgramming.value || !props.uploadEvent) 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("烧录失败");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dialog.info("示例比特流上传成功");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dialog.error("上传失败");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('烧录示例比特流失败:', error);
|
||||||
|
dialog.error("烧录示例比特流失败");
|
||||||
|
} finally {
|
||||||
|
isProgramming.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleFileChange(event: Event): void {
|
function handleFileChange(event: Event): void {
|
||||||
const target = event.target as HTMLInputElement;
|
const target = event.target as HTMLInputElement;
|
||||||
const file = target.files?.[0]; // 获取选中的第一个文件
|
const file = target.files?.[0]; // 获取选中的第一个文件
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
to="#ComponentCapabilities"
|
to="#ComponentCapabilities"
|
||||||
v-if="selectecComponentID === props.componentId"
|
v-if="selectecComponentID === props.componentId"
|
||||||
>
|
>
|
||||||
<MotherBoardCaps :jtagFreq="jtagFreq" @change-jtag-freq="changeJtagFreq" />
|
<MotherBoardCaps :jtagFreq="jtagFreq" :exam-id="examId" @change-jtag-freq="changeJtagFreq" />
|
||||||
</Teleport>
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -41,6 +41,7 @@ import { toNumber } from "lodash";
|
||||||
export interface MotherBoardProps {
|
export interface MotherBoardProps {
|
||||||
size: number;
|
size: number;
|
||||||
componentId?: string;
|
componentId?: string;
|
||||||
|
examId?: string; // 新增examId属性
|
||||||
}
|
}
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<UploadCard class="bg-base-200" :upload-event="eqps.jtagUploadBitstream"
|
<UploadCard class="bg-base-200" :upload-event="eqps.jtagUploadBitstream"
|
||||||
:download-event="eqps.jtagDownloadBitstream" :bitstream-file="eqps.jtagBitstream"
|
:download-event="eqps.jtagDownloadBitstream" :bitstream-file="eqps.jtagBitstream"
|
||||||
|
:exam-id="examId"
|
||||||
@update:bitstream-file="handleBitstreamChange">
|
@update:bitstream-file="handleBitstreamChange">
|
||||||
</UploadCard>
|
</UploadCard>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
@ -61,6 +62,7 @@ import { RefreshCcwIcon } from "lucide-vue-next";
|
||||||
|
|
||||||
interface CapsProps {
|
interface CapsProps {
|
||||||
jtagFreq?: string;
|
jtagFreq?: string;
|
||||||
|
examId?: string; // 新增examId属性
|
||||||
}
|
}
|
||||||
|
|
||||||
const emits = defineEmits<{
|
const emits = defineEmits<{
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -25,26 +25,37 @@
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p class="py-6 text-lg opacity-80 leading-relaxed">
|
<p class="py-6 text-lg opacity-80 leading-relaxed">
|
||||||
Prototype and simulate electronic circuits in your browser with our
|
在浏览器中进行FPGA原型设计和电路仿真,使用现代直观的界面。创建、测试和分享您的FPGA设计,体验从基础学习到高级项目的完整开发流程。
|
||||||
modern, intuitive interface. Create, test, and share your FPGA
|
|
||||||
designs seamlessly.
|
|
||||||
</p>
|
</p>
|
||||||
<div class="flex flex-wrap gap-4 actions-container">
|
<div class="flex flex-col sm:flex-row gap-4 actions-container">
|
||||||
<router-link
|
<router-link
|
||||||
to="/project"
|
to="/project"
|
||||||
class="btn btn-primary text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1"
|
class="btn btn-primary text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1 flex-1 sm:flex-none"
|
||||||
>
|
>
|
||||||
<BookOpen class="h-5 w-5 mr-2" />
|
<BookOpen class="h-5 w-5 mr-2" />
|
||||||
进入工程界面
|
进入工程界面
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
to="/exam"
|
||||||
|
class="btn btn-secondary text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1 flex-1 sm:flex-none"
|
||||||
|
>
|
||||||
|
<GraduationCap class="h-5 w-5 mr-2" />
|
||||||
|
实验列表
|
||||||
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="mt-8 p-4 bg-base-300 rounded-lg shadow-inner opacity-80 transition-all duration-300 hover:opacity-100 hover:shadow-md"
|
class="mt-8 p-4 bg-base-300 rounded-lg shadow-inner opacity-80 transition-all duration-300 hover:opacity-100 hover:shadow-md"
|
||||||
>
|
>
|
||||||
<p class="text-sm">
|
<div class="space-y-2">
|
||||||
<span class="font-semibold text-primary">提示:</span>
|
<p class="text-sm">
|
||||||
您可以在工程界面中创建、编辑和测试您的FPGA项目,使用我们简洁直观的界面轻松进行硬件设计。
|
<span class="font-semibold text-primary">工程界面:</span>
|
||||||
</p>
|
自由创建和编辑FPGA项目,使用可视化画布进行电路设计和仿真测试。
|
||||||
|
</p>
|
||||||
|
<p class="text-sm">
|
||||||
|
<span class="font-semibold text-secondary">实验列表:</span>
|
||||||
|
浏览结构化的学习实验,从基础概念到高级应用的系统性学习路径。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -55,7 +66,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import "@/router";
|
import "@/router";
|
||||||
import TutorialCarousel from "@/components/TutorialCarousel.vue";
|
import TutorialCarousel from "@/components/TutorialCarousel.vue";
|
||||||
import { BookOpen } from "lucide-vue-next";
|
import { BookOpen, GraduationCap } from "lucide-vue-next";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="postcss">
|
<style scoped lang="postcss">
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
<DiagramCanvas
|
<DiagramCanvas
|
||||||
ref="diagramCanvas"
|
ref="diagramCanvas"
|
||||||
:showDocPanel="showDocPanel"
|
:showDocPanel="showDocPanel"
|
||||||
|
:exam-id="(route.query.examId as string) || ''"
|
||||||
@open-components="openComponentsMenu"
|
@open-components="openComponentsMenu"
|
||||||
@toggle-doc-panel="toggleDocPanel"
|
@toggle-doc-panel="toggleDocPanel"
|
||||||
/>
|
/>
|
||||||
|
@ -59,7 +60,10 @@
|
||||||
v-show="showDocPanel"
|
v-show="showDocPanel"
|
||||||
class="doc-panel overflow-y-auto h-full"
|
class="doc-panel overflow-y-auto h-full"
|
||||||
>
|
>
|
||||||
<MarkdownRenderer :content="documentContent" />
|
<MarkdownRenderer
|
||||||
|
:content="documentContent"
|
||||||
|
:examId="(route.query.examId as string) || ''"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SplitterPanel>
|
</SplitterPanel>
|
||||||
|
@ -115,7 +119,6 @@ import MarkdownRenderer from "@/components/MarkdownRenderer.vue";
|
||||||
import BottomBar from "@/views/Project/BottomBar.vue";
|
import BottomBar from "@/views/Project/BottomBar.vue";
|
||||||
import RequestBoardDialog from "@/views/Project/RequestBoardDialog.vue";
|
import RequestBoardDialog from "@/views/Project/RequestBoardDialog.vue";
|
||||||
import { useProvideComponentManager } from "@/components/LabCanvas";
|
import { useProvideComponentManager } from "@/components/LabCanvas";
|
||||||
import type { DiagramData } from "@/components/LabCanvas";
|
|
||||||
import { useAlertStore } from "@/components/Alert";
|
import { useAlertStore } from "@/components/Alert";
|
||||||
import { AuthManager } from "@/utils/AuthManager";
|
import { AuthManager } from "@/utils/AuthManager";
|
||||||
import { useEquipments } from "@/stores/equipments";
|
import { useEquipments } from "@/stores/equipments";
|
||||||
|
@ -182,29 +185,37 @@ async function toggleDocPanel() {
|
||||||
// 加载文档内容
|
// 加载文档内容
|
||||||
async function loadDocumentContent() {
|
async function loadDocumentContent() {
|
||||||
try {
|
try {
|
||||||
// 从路由参数中获取教程ID
|
// 检查是否有实验ID参数
|
||||||
const tutorialId = (route.query.tutorial as string) || "02"; // 默认加载02例程
|
const examId = route.query.examId as string;
|
||||||
|
if (examId) {
|
||||||
|
// 如果有实验ID,从API加载实验文档
|
||||||
|
console.log('加载实验文档:', examId);
|
||||||
|
const client = AuthManager.createAuthenticatedExamClient();
|
||||||
|
|
||||||
// 构建文档路径
|
// 获取markdown类型的资源列表
|
||||||
let docPath = `/doc/${tutorialId}/doc.md`;
|
const resources = await client.getExamResourceList(examId, 'doc');
|
||||||
|
|
||||||
// 检查当前路径是否包含下划线(例如 02_key 格式)
|
if (resources && resources.length > 0) {
|
||||||
// 如果不包含,那么使用更新的命名格式
|
// 获取第一个markdown资源
|
||||||
if (!tutorialId.includes("_")) {
|
const markdownResource = resources[0];
|
||||||
docPath = `/doc/${tutorialId}/doc.md`;
|
|
||||||
|
// 使用动态API获取资源文件内容
|
||||||
|
const response = await client.getExamResourceById(markdownResource.id);
|
||||||
|
|
||||||
|
if (!response || !response.data) {
|
||||||
|
throw new Error('获取markdown文件失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = await response.data.text();
|
||||||
|
|
||||||
|
// 更新文档内容,暂时不处理图片路径,由MarkdownRenderer处理
|
||||||
|
documentContent.value = content;
|
||||||
|
} else {
|
||||||
|
documentContent.value = "# 暂无实验文档\n\n该实验尚未提供文档内容。";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
documentContent.value = "# 无文档";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取文档内容
|
|
||||||
const response = await fetch(docPath);
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to load document: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新文档内容,并替换图片路径
|
|
||||||
documentContent.value = (await response.text()).replace(
|
|
||||||
/.\/images/gi,
|
|
||||||
`/doc/${tutorialId}/images`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("加载文档失败:", error);
|
console.error("加载文档失败:", error);
|
||||||
documentContent.value = "# 文档加载失败\n\n无法加载请求的文档。";
|
documentContent.value = "# 文档加载失败\n\n无法加载请求的文档。";
|
||||||
|
@ -312,8 +323,8 @@ onMounted(async () => {
|
||||||
// 检查并初始化用户实验板
|
// 检查并初始化用户实验板
|
||||||
await checkAndInitializeBoard();
|
await checkAndInitializeBoard();
|
||||||
|
|
||||||
// 检查是否有例程参数,如果有则自动打开文档面板
|
// 检查是否有例程参数或实验ID参数,如果有则自动打开文档面板
|
||||||
if (route.query.tutorial) {
|
if (route.query.tutorial || route.query.examId) {
|
||||||
showDocPanel.value = true;
|
showDocPanel.value = true;
|
||||||
await loadDocumentContent();
|
await loadDocumentContent();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue