12 Commits

35 changed files with 4432 additions and 1955 deletions

View File

@@ -303,8 +303,11 @@ async function generateApiClient(): Promise<void> {
async function generateSignalRClient(): Promise<void> { async function generateSignalRClient(): Promise<void> {
console.log("Generating SignalR TypeScript client..."); console.log("Generating SignalR TypeScript client...");
try { try {
// TypedSignalR.Client.TypeScript.Analyzer 会在编译时自动生成客户端
// 我们只需要确保服务器项目构建一次即可生成 TypeScript 客户端
const { stdout, stderr } = await execAsync( const { stdout, stderr } = await execAsync(
"dotnet tsrts --project ./server/server.csproj --output ./src", "dotnet build --configuration Release",
{ cwd: "./server" }
); );
if (stdout) console.log(stdout); if (stdout) console.log(stdout);
if (stderr) console.error(stderr); if (stderr) console.error(stderr);

View File

@@ -95,6 +95,12 @@ try
.AllowAnyMethod() .AllowAnyMethod()
.AllowAnyHeader() .AllowAnyHeader()
); );
options.AddPolicy("SignalR", policy => policy
.WithOrigins("http://localhost:5173")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
);
}); });
// Use SignalR // Use SignalR
@@ -171,6 +177,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");
} }
@@ -190,29 +207,19 @@ try
}; };
}); });
app.UseSwaggerUi(); app.UseSwaggerUi();
// SignalR
app.UseWebSockets();
app.UseSignalRHubSpecification(); app.UseSignalRHubSpecification();
app.UseSignalRHubDevelopmentUI(); app.UseSignalRHubDevelopmentUI();
// Router // Router
app.MapControllers(); app.MapControllers();
app.MapHub<server.Hubs.JtagHub.JtagHub>("hubs/JtagHub").RequireCors("Users"); app.MapHub<server.Hubs.JtagHub.JtagHub>("hubs/JtagHub");
// 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) =>
{ {

View File

@@ -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 开发工具进行仿真
## 注意事项
- 确保输入信号的电平正确
- 注意时序的约束
- 验证结果时要仔细对比真值表

View File

@@ -1,35 +0,0 @@
# 实验002组合逻辑电路设计
## 实验目的
本实验旨在让学生学习如何设计和实现复杂的组合逻辑电路,掌握多个逻辑门的组合使用。
## 实验内容
### 1. 半加器设计
设计一个半加器电路,实现两个一位二进制数的加法运算。
### 2. 全加器设计
在半加器的基础上,设计全加器电路,考虑进位输入。
### 3. 编码器和译码器
实现简单的编码器和译码器电路。
## 实验要求
1. 使用 Verilog HDL 编写代码
2. 绘制逻辑电路图
3. 编写测试用例验证功能
4. 分析电路的延时特性
## 评估标准
- 电路功能正确性 (40%)
- 代码质量和规范性 (30%)
- 测试覆盖率 (20%)
- 实验报告 (10%)
## 参考资料
- 数字逻辑设计教材第3-4章
- Verilog HDL 语法参考手册

View File

@@ -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,55 @@ 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 CreateExamRequest
{ {
/// <summary> /// <summary>
/// 结果消息 /// 实验ID
/// </summary> /// </summary>
public required string Message { get; set; } public required string ID { get; set; }
/// <summary> /// <summary>
/// 更新的实验数量 /// 实验名称
/// </summary> /// </summary>
public int UpdateCount { get; set; } 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 +157,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 +214,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 +234,58 @@ 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>
/// 从 Markdown 内容中提取标题
/// </summary>
/// <param name="markdownContent">Markdown 内容</param>
/// <returns>提取的标题</returns>
private static string ExtractTitleFromMarkdown(string markdownContent)
{
if (string.IsNullOrEmpty(markdownContent))
return "";
var lines = markdownContent.Split('\n');
foreach (var line in lines)
{
var trimmedLine = line.Trim();
if (trimmedLine.StartsWith("# "))
{
return trimmedLine.Substring(2).Trim();
}
}
return "";
}
} }

View File

@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Database;
namespace server.Controllers; namespace server.Controllers;
@@ -14,8 +15,6 @@ public class JtagController : ControllerBase
{ {
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private const string BITSTREAM_PATH = "bitstream/Jtag";
/// <summary> /// <summary>
/// 控制器首页信息 /// 控制器首页信息
/// </summary> /// </summary>
@@ -112,64 +111,12 @@ public class JtagController : ControllerBase
} }
} }
/// <summary>
/// 上传比特流文件到服务器
/// </summary>
/// <param name="address">目标设备地址</param>
/// <param name="file">比特流文件</param>
/// <returns>上传结果</returns>
[HttpPost("UploadBitstream")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> UploadBitstream(string address, IFormFile file)
{
logger.Info($"User {User.Identity?.Name} uploading bitstream for device {address}");
if (file == null || file.Length == 0)
{
logger.Warn($"User {User.Identity?.Name} attempted to upload empty file for device {address}");
return TypedResults.BadRequest("未选择文件");
}
try
{
// 生成安全的文件名(避免路径遍历攻击)
var fileName = Path.GetRandomFileName();
var uploadsFolder = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
// 如果存在文件,则删除原文件再上传
if (Directory.Exists(uploadsFolder))
{
Directory.Delete(uploadsFolder, true);
logger.Info($"User {User.Identity?.Name} removed existing bitstream folder for device {address}");
}
Directory.CreateDirectory(uploadsFolder);
var filePath = Path.Combine(uploadsFolder, fileName);
using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
logger.Info($"User {User.Identity?.Name} successfully uploaded bitstream for device {address}, file size: {file.Length} bytes");
return TypedResults.Ok(true);
}
catch (Exception ex)
{
logger.Error(ex, $"User {User.Identity?.Name} failed to upload bitstream for device {address}");
return TypedResults.InternalServerError(ex);
}
}
/// <summary> /// <summary>
/// 通过 JTAG 下载比特流文件到 FPGA 设备 /// 通过 JTAG 下载比特流文件到 FPGA 设备
/// </summary> /// </summary>
/// <param name="address">JTAG 设备地址</param> /// <param name="address">JTAG 设备地址</param>
/// <param name="port">JTAG 设备端口</param> /// <param name="port">JTAG 设备端口</param>
/// <param name="bitstreamId">比特流ID</param>
/// <returns>下载结果</returns> /// <returns>下载结果</returns>
[HttpPost("DownloadBitstream")] [HttpPost("DownloadBitstream")]
[EnableCors("Users")] [EnableCors("Users")]
@@ -177,87 +124,111 @@ public class JtagController : ControllerBase
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async ValueTask<IResult> DownloadBitstream(string address, int port) public async ValueTask<IResult> DownloadBitstream(string address, int port, int bitstreamId)
{ {
logger.Info($"User {User.Identity?.Name} initiating bitstream download to device {address}:{port}"); logger.Info($"User {User.Identity?.Name} initiating bitstream download to device {address}:{port} using bitstream ID: {bitstreamId}");
// 检查文件
var fileDir = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
if (!Directory.Exists(fileDir))
{
logger.Warn($"User {User.Identity?.Name} attempted to download non-existent bitstream for device {address}");
return TypedResults.BadRequest("Empty bitstream, Please upload it first");
}
try try
{ {
// 读取文件 // 获取当前用户名
var filePath = Directory.GetFiles(fileDir)[0]; var username = User.Identity?.Name;
logger.Info($"User {User.Identity?.Name} reading bitstream file: {filePath}"); if (string.IsNullOrEmpty(username))
using (var fileStream = System.IO.File.Open(filePath, System.IO.FileMode.Open))
{ {
if (fileStream is null || fileStream.Length <= 0) logger.Warn("Anonymous user attempted to download bitstream");
return TypedResults.Unauthorized();
}
// 从数据库获取用户信息
using var db = new Database.AppDataConnection();
var userResult = db.GetUserByName(username);
if (!userResult.IsSuccessful || !userResult.Value.HasValue)
{
logger.Error($"User {username} not found in database");
return TypedResults.BadRequest("用户不存在");
}
var user = userResult.Value.Value;
// 从数据库获取比特流
var bitstreamResult = db.GetResourceById(bitstreamId);
if (!bitstreamResult.IsSuccessful)
{
logger.Error($"User {username} failed to get bitstream from database: {bitstreamResult.Error}");
return TypedResults.InternalServerError($"数据库查询失败: {bitstreamResult.Error?.Message}");
}
if (!bitstreamResult.Value.HasValue)
{
logger.Warn($"User {username} attempted to download non-existent bitstream ID: {bitstreamId}");
return TypedResults.BadRequest("比特流不存在");
}
var bitstream = bitstreamResult.Value.Value;
// 处理比特流数据
var fileBytes = bitstream.Data;
if (fileBytes == null || fileBytes.Length == 0)
{
logger.Warn($"User {username} found empty bitstream data for ID: {bitstreamId}");
return TypedResults.BadRequest("比特流数据为空,请重新上传");
}
logger.Info($"User {username} processing bitstream file of size: {fileBytes.Length} bytes");
// 定义缓冲区大小: 32KB
byte[] buffer = new byte[32 * 1024];
byte[] revBuffer = new byte[32 * 1024];
long totalBytesProcessed = 0;
// 使用内存流处理文件
using (var inputStream = new MemoryStream(fileBytes))
using (var outputStream = new MemoryStream())
{
int bytesRead;
while ((bytesRead = await inputStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{ {
logger.Warn($"User {User.Identity?.Name} found invalid bitstream file for device {address}"); // 反转 32bits
return TypedResults.BadRequest("Wrong bitstream, Please upload it again"); var retBuffer = Common.Number.ReverseBytes(buffer, 4);
if (!retBuffer.IsSuccessful)
{
logger.Error($"User {username} failed to reverse bytes: {retBuffer.Error}");
return TypedResults.InternalServerError(retBuffer.Error);
}
revBuffer = retBuffer.Value;
for (int i = 0; i < revBuffer.Length; i++)
{
revBuffer[i] = Common.Number.ReverseBits(revBuffer[i]);
}
await outputStream.WriteAsync(revBuffer, 0, bytesRead);
totalBytesProcessed += bytesRead;
} }
logger.Info($"User {User.Identity?.Name} processing bitstream file of size: {fileStream.Length} bytes"); // 获取处理后的数据
var processedBytes = outputStream.ToArray();
logger.Info($"User {username} processed {totalBytesProcessed} bytes for device {address}");
// 定义缓冲区大小: 32KB // 下载比特流
byte[] buffer = new byte[32 * 1024]; var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
byte[] revBuffer = new byte[32 * 1024]; var ret = await jtagCtrl.DownloadBitstream(processedBytes);
long totalBytesRead = 0;
// 使用异步流读取文件 if (ret.IsSuccessful)
using (var memoryStream = new MemoryStream())
{ {
int bytesRead; logger.Info($"User {username} successfully downloaded bitstream '{bitstream.ResourceName}' to device {address}");
while ((bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length)) > 0) return TypedResults.Ok(ret.Value);
{ }
// 反转 32bits else
var retBuffer = Common.Number.ReverseBytes(buffer, 4); {
if (!retBuffer.IsSuccessful) logger.Error($"User {username} failed to download bitstream to device {address}: {ret.Error}");
{ return TypedResults.InternalServerError(ret.Error);
logger.Error($"User {User.Identity?.Name} failed to reverse bytes: {retBuffer.Error}");
return TypedResults.InternalServerError(retBuffer.Error);
}
revBuffer = retBuffer.Value;
for (int i = 0; i < revBuffer.Length; i++)
{
revBuffer[i] = Common.Number.ReverseBits(revBuffer[i]);
}
await memoryStream.WriteAsync(revBuffer, 0, bytesRead);
totalBytesRead += bytesRead;
}
// 将所有数据转换为字节数组(注意:如果文件非常大,可能不适合完全加载到内存)
var fileBytes = memoryStream.ToArray();
logger.Info($"User {User.Identity?.Name} processed {totalBytesRead} bytes for device {address}");
// 下载比特流
var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
var ret = await jtagCtrl.DownloadBitstream(fileBytes);
if (ret.IsSuccessful)
{
logger.Info($"User {User.Identity?.Name} successfully downloaded bitstream to device {address}");
return TypedResults.Ok(ret.Value);
}
else
{
logger.Error($"User {User.Identity?.Name} failed to download bitstream to device {address}: {ret.Error}");
return TypedResults.InternalServerError(ret.Error);
}
} }
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.Error(ex, $"User {User.Identity?.Name} encountered exception while downloading bitstream to device {address}"); logger.Error(ex, $"User encountered exception while downloading bitstream to device {address}");
return TypedResults.InternalServerError(ex); return TypedResults.InternalServerError(ex);
} }
} }

View File

@@ -0,0 +1,377 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using DotNext;
using Database;
namespace server.Controllers;
/// <summary>
/// 资源控制器 - 提供统一的资源管理API
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class ResourceController : ControllerBase
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
/// <summary>
/// 资源信息类
/// </summary>
public class ResourceInfo
{
/// <summary>
/// 资源ID
/// </summary>
public int ID { get; set; }
/// <summary>
/// 资源名称
/// </summary>
public required string Name { get; set; }
/// <summary>
/// 资源类型
/// </summary>
public required string Type { get; set; }
/// <summary>
/// 资源用途template/user
/// </summary>
public required string Purpose { get; set; }
/// <summary>
/// 上传时间
/// </summary>
public DateTime UploadTime { get; set; }
/// <summary>
/// 所属实验ID可选
/// </summary>
public string? ExamID { get; set; }
/// <summary>
/// MIME类型
/// </summary>
public string? MimeType { get; set; }
}
/// <summary>
/// 添加资源请求类
/// </summary>
public class AddResourceRequest
{
/// <summary>
/// 资源类型
/// </summary>
public required string ResourceType { get; set; }
/// <summary>
/// 资源用途template/user
/// </summary>
public required string ResourcePurpose { get; set; }
/// <summary>
/// 所属实验ID可选
/// </summary>
public string? ExamID { get; set; }
}
/// <summary>
/// 添加资源(文件上传)
/// </summary>
/// <param name="request">添加资源请求</param>
/// <param name="file">资源文件</param>
/// <returns>添加结果</returns>
[Authorize]
[HttpPost]
[EnableCors("Users")]
[ProducesResponseType(typeof(ResourceInfo), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> AddResource([FromForm] AddResourceRequest request, IFormFile file)
{
if (string.IsNullOrWhiteSpace(request.ResourceType) || string.IsNullOrWhiteSpace(request.ResourcePurpose) || file == null)
return BadRequest("资源类型、资源用途和文件不能为空");
// 验证资源用途
if (request.ResourcePurpose != Resource.ResourcePurposes.Template && request.ResourcePurpose != Resource.ResourcePurposes.User)
return BadRequest($"无效的资源用途: {request.ResourcePurpose}");
// 模板资源需要管理员权限
if (request.ResourcePurpose == Resource.ResourcePurposes.Template && !User.IsInRole("Admin"))
return Forbid("只有管理员可以添加模板资源");
try
{
using var db = new Database.AppDataConnection();
// 获取当前用户ID
var userName = User.Identity?.Name;
if (string.IsNullOrEmpty(userName))
return Unauthorized("无法获取用户信息");
var userResult = db.GetUserByName(userName);
if (!userResult.IsSuccessful || !userResult.Value.HasValue)
return Unauthorized("用户不存在");
var user = userResult.Value.Value;
// 读取文件数据
using var memoryStream = new MemoryStream();
await file.CopyToAsync(memoryStream);
var fileData = memoryStream.ToArray();
var result = db.AddResource(user.ID, request.ResourceType, request.ResourcePurpose, file.FileName, fileData, request.ExamID);
if (!result.IsSuccessful)
{
if (result.Error.Message.Contains("不存在"))
return NotFound(result.Error.Message);
logger.Error($"添加资源时出错: {result.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"添加资源失败: {result.Error.Message}");
}
var resource = result.Value;
var resourceInfo = new ResourceInfo
{
ID = resource.ID,
Name = resource.ResourceName,
Type = resource.ResourceType,
Purpose = resource.ResourcePurpose,
UploadTime = resource.UploadTime,
ExamID = resource.ExamID,
MimeType = resource.MimeType
};
logger.Info($"成功添加资源: {request.ResourceType}/{request.ResourcePurpose}/{file.FileName}");
return CreatedAtAction(nameof(GetResourceById), new { resourceId = resource.ID }, resourceInfo);
}
catch (Exception ex)
{
logger.Error($"添加资源 {request.ResourceType}/{request.ResourcePurpose}/{file.FileName} 时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"添加资源失败: {ex.Message}");
}
}
/// <summary>
/// 获取资源列表
/// </summary>
/// <param name="examId">实验ID可选</param>
/// <param name="resourceType">资源类型(可选)</param>
/// <param name="resourcePurpose">资源用途(可选)</param>
/// <returns>资源列表</returns>
[Authorize]
[HttpGet]
[EnableCors("Users")]
[ProducesResponseType(typeof(ResourceInfo[]), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult GetResourceList([FromQuery] string? examId = null, [FromQuery] string? resourceType = null, [FromQuery] string? resourcePurpose = null)
{
try
{
using var db = new Database.AppDataConnection();
// 获取当前用户ID
var userName = User.Identity?.Name;
if (string.IsNullOrEmpty(userName))
return Unauthorized("无法获取用户信息");
var userResult = db.GetUserByName(userName);
if (!userResult.IsSuccessful || !userResult.Value.HasValue)
return Unauthorized("用户不存在");
var user = userResult.Value.Value;
// 普通用户只能查看自己的资源和模板资源
Guid? userId = null;
if (!User.IsInRole("Admin"))
{
// 如果指定了用户资源用途,则只查看自己的资源
if (resourcePurpose == Resource.ResourcePurposes.User)
{
userId = user.ID;
}
// 如果指定了模板资源用途则不限制用户ID
else if (resourcePurpose == Resource.ResourcePurposes.Template)
{
userId = null;
}
// 如果没有指定用途,则查看自己的用户资源和所有模板资源
else
{
// 这种情况下需要分别查询并合并结果
var userResourcesResult = db.GetFullResourceList(examId, resourceType, Resource.ResourcePurposes.User, user.ID);
var templateResourcesResult = db.GetFullResourceList(examId, resourceType, Resource.ResourcePurposes.Template, null);
if (!userResourcesResult.IsSuccessful || !templateResourcesResult.IsSuccessful)
{
logger.Error($"获取资源列表时出错");
return StatusCode(StatusCodes.Status500InternalServerError, "获取资源列表失败");
}
var allResources = userResourcesResult.Value.Concat(templateResourcesResult.Value)
.OrderByDescending(r => r.UploadTime);
var mergedResourceInfos = allResources.Select(r => new ResourceInfo
{
ID = r.ID,
Name = r.ResourceName,
Type = r.ResourceType,
Purpose = r.ResourcePurpose,
UploadTime = r.UploadTime,
ExamID = r.ExamID,
MimeType = r.MimeType
}).ToArray();
logger.Info($"成功获取资源列表,共 {mergedResourceInfos.Length} 个资源");
return Ok(mergedResourceInfos);
}
}
var result = db.GetFullResourceList(examId, resourceType, resourcePurpose, userId);
if (!result.IsSuccessful)
{
logger.Error($"获取资源列表时出错: {result.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源列表失败: {result.Error.Message}");
}
var resources = result.Value.Select(r => new ResourceInfo
{
ID = r.ID,
Name = r.ResourceName,
Type = r.ResourceType,
Purpose = r.ResourcePurpose,
UploadTime = r.UploadTime,
ExamID = r.ExamID,
MimeType = r.MimeType
}).ToArray();
logger.Info($"成功获取资源列表,共 {resources.Length} 个资源");
return Ok(resources);
}
catch (Exception ex)
{
logger.Error($"获取资源列表时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源列表失败: {ex.Message}");
}
}
/// <summary>
/// 根据资源ID下载资源
/// </summary>
/// <param name="resourceId">资源ID</param>
/// <returns>资源文件</returns>
[HttpGet("{resourceId}")]
[EnableCors("Users")]
[ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult GetResourceById(int resourceId)
{
try
{
using var db = new Database.AppDataConnection();
var result = db.GetResourceById(resourceId);
if (!result.IsSuccessful)
{
logger.Error($"获取资源时出错: {result.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {result.Error.Message}");
}
if (!result.Value.HasValue)
{
logger.Warn($"资源不存在: {resourceId}");
return NotFound($"资源 {resourceId} 不存在");
}
var resource = result.Value.Value;
logger.Info($"成功获取资源: {resourceId} ({resource.ResourceName})");
return File(resource.Data, resource.MimeType ?? "application/octet-stream", resource.ResourceName);
}
catch (Exception ex)
{
logger.Error($"获取资源 {resourceId} 时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {ex.Message}");
}
}
/// <summary>
/// 删除资源
/// </summary>
/// <param name="resourceId">资源ID</param>
/// <returns>删除结果</returns>
[Authorize]
[HttpDelete("{resourceId}")]
[EnableCors("Users")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult DeleteResource(int resourceId)
{
try
{
using var db = new Database.AppDataConnection();
// 获取当前用户信息
var userName = User.Identity?.Name;
if (string.IsNullOrEmpty(userName))
return Unauthorized("无法获取用户信息");
var userResult = db.GetUserByName(userName);
if (!userResult.IsSuccessful || !userResult.Value.HasValue)
return Unauthorized("用户不存在");
var user = userResult.Value.Value;
// 先获取资源信息以验证权限
var resourceResult = db.GetResourceById(resourceId);
if (!resourceResult.IsSuccessful)
{
logger.Error($"获取资源时出错: {resourceResult.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {resourceResult.Error.Message}");
}
if (!resourceResult.Value.HasValue)
{
logger.Warn($"资源不存在: {resourceId}");
return NotFound($"资源 {resourceId} 不存在");
}
var resource = resourceResult.Value.Value;
// 权限检查:管理员可以删除所有资源,普通用户只能删除自己的用户资源
if (!User.IsInRole("Admin"))
{
if (resource.ResourcePurpose == Resource.ResourcePurposes.Template)
return Forbid("普通用户不能删除模板资源");
if (resource.UserID != user.ID)
return Forbid("只能删除自己的资源");
}
var deleteResult = db.DeleteResource(resourceId);
if (!deleteResult.IsSuccessful)
{
logger.Error($"删除资源时出错: {deleteResult.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"删除资源失败: {deleteResult.Error.Message}");
}
logger.Info($"成功删除资源: {resourceId} ({resource.ResourceName})");
return NoContent();
}
catch (Exception ex)
{
logger.Error($"删除资源 {resourceId} 时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"删除资源失败: {ex.Message}");
}
}
}

View File

@@ -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,155 @@ 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-51为最简单
/// </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 Resource
{
/// <summary>
/// 资源的唯一标识符
/// </summary>
[PrimaryKey, Identity]
public int ID { get; set; }
/// <summary>
/// 上传资源的用户ID
/// </summary>
[NotNull]
public required Guid UserID { get; set; }
/// <summary>
/// 所属实验ID可选如果不属于特定实验则为空
/// </summary>
[Nullable]
public string? ExamID { get; set; }
/// <summary>
/// 资源类型images, markdown, bitstream, diagram, project等
/// </summary>
[NotNull]
public required string ResourceType { get; set; }
/// <summary>
/// 资源用途template模板或 user用户上传
/// </summary>
[NotNull]
public required string ResourcePurpose { 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 UploadTime { 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>
public static class ResourcePurposes
{
/// <summary>
/// 模板资源,通常由管理员上传,供用户参考
/// </summary>
public const string Template = "template";
/// <summary>
/// 用户上传的资源
/// </summary>
public const string User = "user";
}
} }
/// <summary> /// <summary>
@@ -228,6 +383,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<Resource>();
logger.Info("数据库表创建完成"); logger.Info("数据库表创建完成");
} }
@@ -240,6 +396,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<Resource>();
logger.Warn("所有数据库表已删除"); logger.Warn("所有数据库表已删除");
} }
@@ -699,75 +856,358 @@ 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<Resource> ResourceTable => this.GetTable<Resource>();
/// <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)
int updateCount = 0;
var subdirectories = Directory.GetDirectories(examFolderPath);
foreach (var examDir in subdirectories)
{
var examId = Path.GetFileName(examDir);
var docPath = Path.Combine(examDir, "doc.md");
if (!File.Exists(docPath))
{ {
logger.Warn($"实验 {examId} 缺少 doc.md 文件"); logger.Error($"实验ID已存在: {id}");
continue; return new(new Exception($"实验ID已存在: {id}"));
} }
try var exam = new Exam
{ {
var docContent = File.ReadAllText(docPath); ID = id,
var existingExam = this.ExamTable.Where(e => e.ID == examId).FirstOrDefault(); Name = name,
Description = description,
Difficulty = Math.Max(1, Math.Min(5, difficulty)),
IsVisibleToUsers = isVisibleToUsers,
CreatedTime = DateTime.Now,
UpdatedTime = DateTime.Now
};
if (existingExam == null) 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="userId">上传用户ID</param>
/// <param name="resourceType">资源类型</param>
/// <param name="resourcePurpose">资源用途template 或 user</param>
/// <param name="resourceName">资源名称</param>
/// <param name="data">资源二进制数据</param>
/// <param name="examId">所属实验ID可选</param>
/// <param name="mimeType">MIME类型可选将根据文件扩展名自动确定</param>
/// <returns>创建的资源</returns>
public Result<Resource> AddResource(Guid userId, string resourceType, string resourcePurpose, string resourceName, byte[] data, string? examId = null, string? mimeType = null)
{
try
{
// 验证用户是否存在
var user = this.UserTable.Where(u => u.ID == userId).FirstOrDefault();
if (user == null)
{
logger.Error($"用户不存在: {userId}");
return new(new Exception($"用户不存在: {userId}"));
}
// 如果指定了实验ID验证实验是否存在
if (!string.IsNullOrEmpty(examId))
{
var exam = this.ExamTable.Where(e => e.ID == examId).FirstOrDefault();
if (exam == null)
{ {
// 创建新实验 logger.Error($"实验不存在: {examId}");
var newExam = new Exam return new(new Exception($"实验不存在: {examId}"));
{
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)
// 验证资源用途
if (resourcePurpose != Resource.ResourcePurposes.Template && resourcePurpose != Resource.ResourcePurposes.User)
{ {
logger.Error($"处理实验 {examId} 时出错: {ex.Message}"); logger.Error($"无效的资源用途: {resourcePurpose}");
return new(new Exception($"无效的资源用途: {resourcePurpose}"));
} }
// 如果未指定MIME类型根据文件扩展名自动确定
if (string.IsNullOrEmpty(mimeType))
{
var extension = Path.GetExtension(resourceName).ToLowerInvariant();
mimeType = GetMimeTypeFromExtension(extension, resourceName);
}
var resource = new Resource
{
UserID = userId,
ExamID = examId,
ResourceType = resourceType,
ResourcePurpose = resourcePurpose,
ResourceName = resourceName,
Data = data,
MimeType = mimeType,
UploadTime = DateTime.Now
};
var insertedId = this.InsertWithIdentity(resource);
resource.ID = Convert.ToInt32(insertedId);
logger.Info($"新资源已添加: {userId}/{resourceType}/{resourcePurpose}/{resourceName} ({data.Length} bytes)" +
(examId != null ? $" [实验: {examId}]" : "") + $" [ID: {resource.ID}]");
return new(resource);
}
catch (Exception ex)
{
logger.Error($"添加资源时出错: {ex.Message}");
return new(ex);
}
}
/// <summary>
/// 获取资源信息列表返回ID和名称
/// <param name="resourceType">资源类型</param>
/// <param name="examId">实验ID可选</param>
/// <param name="resourcePurpose">资源用途(可选)</param>
/// <param name="userId">用户ID可选</param>
/// </summary>
/// <returns>资源信息列表</returns>
public Result<(int ID, string Name)[]> GetResourceList(string resourceType, string? examId = null, string? resourcePurpose = null, Guid? userId = null)
{
try
{
var query = this.ResourceTable.Where(r => r.ResourceType == resourceType);
if (examId != null)
{
query = query.Where(r => r.ExamID == examId);
}
if (resourcePurpose != null)
{
query = query.Where(r => r.ResourcePurpose == resourcePurpose);
}
if (userId != null)
{
query = query.Where(r => r.UserID == userId);
}
var resources = query
.Select(r => new { r.ID, r.ResourceName })
.ToArray();
var result = resources.Select(r => (r.ID, r.ResourceName)).ToArray();
logger.Info($"获取资源列表: {resourceType}" +
(examId != null ? $"/{examId}" : "") +
(resourcePurpose != null ? $"/{resourcePurpose}" : "") +
(userId != null ? $"/{userId}" : "") +
$",共 {result.Length} 个资源");
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="resourcePurpose">资源用途(可选)</param>
/// <param name="userId">用户ID可选</param>
/// <returns>完整的资源对象列表</returns>
public Result<List<Resource>> GetFullResourceList(string? examId = null, string? resourceType = null, string? resourcePurpose = null, Guid? userId = null)
{
try
{
var query = this.ResourceTable.AsQueryable();
if (examId != null)
{
query = query.Where(r => r.ExamID == examId);
}
if (resourceType != null)
{
query = query.Where(r => r.ResourceType == resourceType);
}
if (resourcePurpose != null)
{
query = query.Where(r => r.ResourcePurpose == resourcePurpose);
}
if (userId != null)
{
query = query.Where(r => r.UserID == userId);
}
var resources = query.OrderByDescending(r => r.UploadTime).ToList();
logger.Info($"获取完整资源列表" +
(examId != null ? $" [实验: {examId}]" : "") +
(resourceType != null ? $" [类型: {resourceType}]" : "") +
(resourcePurpose != null ? $" [用途: {resourcePurpose}]" : "") +
(userId != null ? $" [用户: {userId}]" : "") +
$",共 {resources.Count} 个资源");
return new(resources);
}
catch (Exception ex)
{
logger.Error($"获取完整资源列表时出错: {ex.Message}");
return new(ex);
}
}
/// <summary>
/// 根据资源ID获取资源
/// </summary>
/// <param name="resourceId">资源ID</param>
/// <returns>资源数据</returns>
public Result<Optional<Resource>> GetResourceById(int resourceId)
{
try
{
var resource = this.ResourceTable.Where(r => r.ID == resourceId).FirstOrDefault();
if (resource == null)
{
logger.Info($"未找到资源: {resourceId}");
return new(Optional<Resource>.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> DeleteResource(int resourceId)
{
try
{
var result = this.ResourceTable.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";
} }
logger.Info($"实验扫描完成,共更新 {updateCount} 个实验"); return extension.ToLowerInvariant() switch
return updateCount; {
".png" => "image/png",
".jpg" or ".jpeg" => "image/jpeg",
".gif" => "image/gif",
".bmp" => "image/bmp",
".svg" => "image/svg+xml",
".sbit" => "application/octet-stream",
".bit" => "application/octet-stream",
".bin" => "application/octet-stream",
".json" => "application/json",
".zip" => "application/zip",
".md" => "text/markdown",
_ => "application/octet-stream"
};
} }
/// <summary> /// <summary>
@@ -805,4 +1245,22 @@ public class AppDataConnection : DataConnection
logger.Debug($"成功获取实验信息: {examId}"); logger.Debug($"成功获取实验信息: {examId}");
return new(exams[0]); return new(exams[0]);
} }
/// <summary>
/// 根据文件扩展名获取比特流MIME类型
/// </summary>
/// <param name="extension">文件扩展名</param>
/// <returns>MIME类型</returns>
private string GetBitstreamMimeType(string extension)
{
return extension.ToLowerInvariant() switch
{
".bit" => "application/octet-stream",
".sbit" => "application/octet-stream",
".bin" => "application/octet-stream",
".mcs" => "application/octet-stream",
".hex" => "text/plain",
_ => "application/octet-stream"
};
}
} }

View File

@@ -1,4 +1,6 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims; using System.Security.Claims;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using DotNext; using DotNext;
@@ -23,11 +25,19 @@ public interface IJtagReceiver
} }
[Authorize] [Authorize]
[EnableCors("SignalR")]
public class JtagHub : Hub<IJtagReceiver>, IJtagHub public class JtagHub : Hub<IJtagReceiver>, IJtagHub
{ {
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private ConcurrentDictionary<string, int> FreqTable = new(); private static ConcurrentDictionary<string, int> FreqTable = new();
private ConcurrentDictionary<string, CancellationTokenSource> CancellationTokenSourceTable = new(); private static ConcurrentDictionary<string, CancellationTokenSource> CancellationTokenSourceTable = new();
private readonly IHubContext<JtagHub, IJtagReceiver> _hubContext;
public JtagHub(IHubContext<JtagHub, IJtagReceiver> hubContext)
{
_hubContext = hubContext;
}
private Optional<Peripherals.JtagClient.Jtag> GetJtagClient(string userName) private Optional<Peripherals.JtagClient.Jtag> GetJtagClient(string userName)
{ {
@@ -89,33 +99,40 @@ public class JtagHub : Hub<IJtagReceiver>, IJtagHub
} }
await SetBoundaryScanFreq(freq); await SetBoundaryScanFreq(freq);
var cts = CancellationTokenSource.CreateLinkedTokenSource(); var cts = new CancellationTokenSource();
CancellationTokenSourceTable.AddOrUpdate(userName, cts, (key, value) => cts); CancellationTokenSourceTable.AddOrUpdate(userName, cts, (key, value) => cts);
_ = Task _ = Task.Run(
.Run( () => BoundaryScanLogicPorts(Context.ConnectionId, userName, cts.Token),
() => BoundaryScanLogicPorts(
Context.ConnectionId,
userName,
cts.Token),
cts.Token) cts.Token)
.ContinueWith((task) => .ContinueWith((task) =>
{ {
if (!task.IsFaulted) if (task.IsFaulted)
{ {
return; // 遍历所有异常
foreach (var ex in task.Exception.InnerExceptions)
{
if (ex is OperationCanceledException)
{
logger.Info($"Boundary scan operation cancelled for user {userName}");
}
else
{
logger.Error($"Boundary scan operation failed for user {userName}: {ex}");
}
}
} }
else if (task.IsCanceled)
if (task.Exception.InnerException is OperationCanceledException)
{ {
logger.Info($"Boundary scan operation cancelled for user {userName}"); logger.Info($"Boundary scan operation cancelled for user {userName}");
} }
else else
{ {
logger.Error(task.Exception); logger.Info($"Boundary scan completed successfully for user {userName}");
} }
}); });
logger.Info($"Boundary scan started for user {userName}");
return true; return true;
} }
catch (Exception error) catch (Exception error)
@@ -141,10 +158,12 @@ public class JtagHub : Hub<IJtagReceiver>, IJtagHub
cts.Cancel(); cts.Cancel();
cts.Token.WaitHandle.WaitOne(); cts.Token.WaitHandle.WaitOne();
logger.Info($"Boundary scan stopped for user {userName}");
return true; return true;
} }
private async void BoundaryScanLogicPorts(string connectionID, string userName, CancellationToken cancellationToken) private async Task BoundaryScanLogicPorts(string connectionID, string userName, CancellationToken cancellationToken)
{ {
var jtagCtrl = GetJtagClient(userName).OrThrow(() => new InvalidOperationException("JTAG client not found")); var jtagCtrl = GetJtagClient(userName).OrThrow(() => new InvalidOperationException("JTAG client not found"));
var cntFail = 0; var cntFail = 0;
@@ -158,9 +177,10 @@ public class JtagHub : Hub<IJtagReceiver>, IJtagHub
{ {
logger.Error($"User {userName} boundary scan failed for device {jtagCtrl.address}: {ret.Error}"); logger.Error($"User {userName} boundary scan failed for device {jtagCtrl.address}: {ret.Error}");
cntFail++; cntFail++;
continue;
} }
await this.Clients.Client(connectionID).OnReceiveBoundaryScanData(ret.Value); await _hubContext.Clients.Client(connectionID).OnReceiveBoundaryScanData(ret.Value);
// logger.Info($"User {userName} successfully completed boundary scan for device {jtagCtrl.address}"); // logger.Info($"User {userName} successfully completed boundary scan for device {jtagCtrl.address}");
await Task.Delay(FreqTable.TryGetValue(userName, out var freq) ? 1000 / freq : 1000 / 100, cancellationToken); await Task.Delay(FreqTable.TryGetValue(userName, out var freq) ? 1000 / freq : 1000 / 100, cancellationToken);

View File

@@ -1,6 +1,7 @@
using System.Net; using System.Net;
using DotNext; using DotNext;
using Peripherals.PowerClient; using Peripherals.PowerClient;
using WebProtocol;
namespace Peripherals.CameraClient; namespace Peripherals.CameraClient;
@@ -19,7 +20,7 @@ class Camera
{ {
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
readonly int timeout = 2000; readonly int timeout = 500;
readonly int taskID; readonly int taskID;
readonly int port; readonly int port;
readonly string address; readonly string address;
@@ -43,7 +44,7 @@ class Camera
/// <param name="address">摄像头设备IP地址</param> /// <param name="address">摄像头设备IP地址</param>
/// <param name="port">摄像头设备端口</param> /// <param name="port">摄像头设备端口</param>
/// <param name="timeout">超时时间(毫秒)</param> /// <param name="timeout">超时时间(毫秒)</param>
public Camera(string address, int port, int timeout = 2000) public Camera(string address, int port, int timeout = 500)
{ {
if (timeout < 0) if (timeout < 0)
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout)); throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
@@ -225,6 +226,7 @@ class Camera
this.taskID, // taskID this.taskID, // taskID
FrameAddr, FrameAddr,
(int)_currentFrameLength, // 使用当前分辨率的动态大小 (int)_currentFrameLength, // 使用当前分辨率的动态大小
BurstType.ExtendBurst,
this.timeout); this.timeout);
if (!result.IsSuccessful) if (!result.IsSuccessful)
@@ -462,6 +464,20 @@ class Camera
); );
} }
/// <summary>
/// 配置为960x540分辨率
/// </summary>
/// <returns>配置结果</returns>
public async ValueTask<Result<bool>> ConfigureResolution960x540()
{
return await ConfigureResolution(
hStart: 0, vStart: 0,
dvpHo: 960, dvpVo: 540,
hts: 1700, vts: 1500,
hOffset: 16, vOffset: 4
);
}
/// <summary> /// <summary>
/// 配置为320x240分辨率 /// 配置为320x240分辨率
/// </summary> /// </summary>
@@ -543,6 +559,9 @@ class Camera
case "640x480": case "640x480":
result = await ConfigureResolution640x480(); result = await ConfigureResolution640x480();
break; break;
case "960x540":
result = await ConfigureResolution960x540();
break;
case "1280x720": case "1280x720":
result = await ConfigureResolution1280x720(); result = await ConfigureResolution1280x720();
break; break;

View File

@@ -0,0 +1,118 @@
using System.Net;
using DotNext;
using Peripherals.PowerClient;
using WebProtocol;
namespace Peripherals.HdmiInClient;
static class HdmiInAddr
{
public const UInt32 BASE = 0xA000_0000;
public const UInt32 HdmiIn_CTRL = BASE + 0x0; //[0]: rstn, 0 is reset.
public const UInt32 HdmiIn_READFIFO = BASE + 0x1;
}
class HdmiIn
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
readonly int timeout = 500;
readonly int taskID;
readonly int port;
readonly string address;
private IPEndPoint ep;
// 动态分辨率参数
private UInt16 _currentWidth = 960;
private UInt16 _currentHeight = 540;
private UInt32 _currentFrameLength = 960 * 540 * 2 / 4; // RGB565格式2字节/像素按4字节对齐
/// <summary>
/// 初始化HDMI输入客户端
/// </summary>
/// <param name="address">HDMI输入设备IP地址</param>
/// <param name="port">HDMI输入设备端口</param>
/// <param name="timeout">超时时间(毫秒)</param>
public HdmiIn(string address, int port, int timeout = 500)
{
if (timeout < 0)
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
this.address = address;
this.port = port;
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
this.timeout = timeout;
}
public async ValueTask<Result<bool>> EnableTrans(bool isEnable)
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, HdmiInAddr.HdmiIn_CTRL, (isEnable ? 0x00000001u : 0x00000000u));
if (!ret.IsSuccessful)
{
logger.Error($"Failed to write HdmiIn_CTRL to HdmiIn at {this.address}:{this.port}, error: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error($"HdmiIn_CTRL write returned false for HdmiIn at {this.address}:{this.port}");
return false;
}
return true;
}
/// <summary>
/// 读取一帧图像数据
/// </summary>
/// <returns>包含图像数据的字节数组</returns>
public async ValueTask<Result<byte[]>> ReadFrame()
{
// 只在第一次或出错时清除UDP缓冲区避免每帧都清除造成延迟
// MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
logger.Trace($"Reading frame from HdmiIn {this.address}");
// 使用UDPClientPool读取图像帧数据
var result = await UDPClientPool.ReadAddr4BytesAsync(
this.ep,
this.taskID, // taskID
HdmiInAddr.HdmiIn_READFIFO,
(int)_currentFrameLength, // 使用当前分辨率的动态大小
BurstType.FixedBurst,
this.timeout);
if (!result.IsSuccessful)
{
logger.Error($"Failed to read frame from HdmiIn {this.address}:{this.port}, error: {result.Error}");
// 读取失败时清除缓冲区,为下次读取做准备
try
{
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
}
catch (Exception ex)
{
logger.Warn($"Failed to clear UDP data after read error: {ex.Message}");
}
return new(result.Error);
}
logger.Trace($"Successfully read frame from HdmiIn {this.address}:{this.port}, data length: {result.Value.Length} bytes");
return result.Value;
}
/// <summary>
/// 获取当前分辨率
/// </summary>
/// <returns>当前分辨率(宽度, 高度)</returns>
public (int Width, int Height) GetCurrentResolution()
{
return (_currentWidth, _currentHeight);
}
/// <summary>
/// 获取当前帧长度
/// </summary>
/// <returns>当前帧长度</returns>
public UInt32 GetCurrentFrameLength()
{
return _currentFrameLength;
}
}

View File

@@ -439,7 +439,7 @@ public class Jtag
if (retPackLen != 4) if (retPackLen != 4)
return new(new Exception($"RecvDataPackage BodyData Length not Equal to 4: Total {retPackLen} bytes")); return new(new Exception($"RecvDataPackage BodyData Length not Equal to 4: Total {retPackLen} bytes"));
return Convert.ToUInt32(Common.Number.BytesToUInt64(retPackOpts.Data).Value); return Convert.ToUInt32(Common.Number.BytesToUInt32(retPackOpts.Data).Value);
} }
async ValueTask<Result<bool>> WriteFIFO async ValueTask<Result<bool>> WriteFIFO
@@ -612,13 +612,10 @@ public class Jtag
if (ret.Value) if (ret.Value)
{ {
var array = new UInt32[UInt32Num]; var array = new UInt32[UInt32Num];
for (int i = 0; i < UInt32Num; i++) var retData = await UDPClientPool.ReadAddr4Bytes(ep, 0, JtagAddr.READ_DATA, (int)UInt32Num);
{ if (!retData.IsSuccessful)
var retData = await ReadFIFO(JtagAddr.READ_DATA); return new(new Exception("Read FIFO failed when Load DR"));
if (!retData.IsSuccessful) Buffer.BlockCopy(retData.Value, 0, array, 0, (int)UInt32Num * 4);
return new(new Exception("Read FIFO failed when Load DR"));
array[i] = retData.Value;
}
return array; return array;
} }
else else
@@ -788,7 +785,7 @@ public class Jtag
{ {
var paser = new BsdlParser.Parser(); var paser = new BsdlParser.Parser();
var portNum = paser.GetBoundaryRegsNum().Value; var portNum = paser.GetBoundaryRegsNum().Value;
logger.Debug($"Get boundar scan registers number: {portNum}"); logger.Debug($"Get boundary scan registers number: {portNum}");
// Clear Data // Clear Data
MsgBus.UDPServer.ClearUDPData(this.address, 0); MsgBus.UDPServer.ClearUDPData(this.address, 0);

View File

@@ -2,6 +2,7 @@ using System.Collections;
using System.Net; using System.Net;
using Common; using Common;
using DotNext; using DotNext;
using WebProtocol;
namespace Peripherals.LogicAnalyzerClient; namespace Peripherals.LogicAnalyzerClient;
@@ -475,6 +476,7 @@ public class Analyzer
this.taskID, this.taskID,
AnalyzerAddr.STORE_OFFSET_ADDR, AnalyzerAddr.STORE_OFFSET_ADDR,
capture_length, capture_length,
BurstType.ExtendBurst, // 使用扩展突发读取
this.timeout this.timeout
); );
if (!ret.IsSuccessful) if (!ret.IsSuccessful)

View File

@@ -1,6 +1,7 @@
using System.Net; using System.Net;
using Common; using Common;
using DotNext; using DotNext;
using WebProtocol;
namespace Peripherals.OscilloscopeClient; namespace Peripherals.OscilloscopeClient;
@@ -319,6 +320,7 @@ class Oscilloscope
this.taskID, this.taskID,
OscilloscopeAddr.RD_DATA_ADDR, OscilloscopeAddr.RD_DATA_ADDR,
(int)OscilloscopeAddr.RD_DATA_LENGTH / 32, (int)OscilloscopeAddr.RD_DATA_LENGTH / 32,
BurstType.ExtendBurst, // 使用扩展突发读取
this.timeout this.timeout
); );
if (!ret.IsSuccessful) if (!ret.IsSuccessful)

View File

@@ -1109,6 +1109,7 @@ public class HttpVideoStreamService : BackgroundService
return new List<(int, int, string)> return new List<(int, int, string)>
{ {
(640, 480, "640x480 (VGA)"), (640, 480, "640x480 (VGA)"),
(960, 540, "960x540 (qHD)"),
(1280, 720, "1280x720 (HD)"), (1280, 720, "1280x720 (HD)"),
(1280, 960, "1280x960 (SXGA)"), (1280, 960, "1280x960 (SXGA)"),
(1920, 1080, "1920x1080 (Full HD)") (1920, 1080, "1920x1080 (Full HD)")

View File

@@ -336,7 +336,7 @@ public class UDPClientPool
$"Device {address} receive data is {retData.Length} bytes instead of 4 bytes")); $"Device {address} receive data is {retData.Length} bytes instead of 4 bytes"));
// Check result // Check result
var retCode = Convert.ToUInt32(Common.Number.BytesToUInt64(retData).Value); var retCode = Convert.ToUInt32(Common.Number.BytesToUInt32(retData).Value);
if (Common.Number.BitsCheck(retCode, result, resultMask)) return true; if (Common.Number.BitsCheck(retCode, result, resultMask)) return true;
} }
catch (Exception error) catch (Exception error)
@@ -433,11 +433,12 @@ public class UDPClientPool
/// <param name="endPoint">IP端点IP地址与端口</param> /// <param name="endPoint">IP端点IP地址与端口</param>
/// <param name="taskID">任务ID</param> /// <param name="taskID">任务ID</param>
/// <param name="devAddr">设备地址</param> /// <param name="devAddr">设备地址</param>
/// <param name="burstType">突发类型</param>
/// <param name="dataLength">要读取的数据长度4字节</param> /// <param name="dataLength">要读取的数据长度4字节</param>
/// <param name="timeout">超时时间(毫秒)</param> /// <param name="timeout">超时时间(毫秒)</param>
/// <returns>读取结果,包含接收到的字节数组</returns> /// <returns>读取结果,包含接收到的字节数组</returns>
public static async ValueTask<Result<byte[]>> ReadAddr4BytesAsync( public static async ValueTask<Result<byte[]>> ReadAddr4BytesAsync(
IPEndPoint endPoint, int taskID, UInt32 devAddr, int dataLength, int timeout = 1000) IPEndPoint endPoint, int taskID, UInt32 devAddr, int dataLength, BurstType burstType, int timeout = 1000)
{ {
var pkgList = new List<SendAddrPackage>(); var pkgList = new List<SendAddrPackage>();
var resultData = new List<byte>(); var resultData = new List<byte>();
@@ -460,7 +461,7 @@ public class UDPClientPool
var opts = new SendAddrPackOptions var opts = new SendAddrPackOptions
{ {
BurstType = BurstType.FixedBurst, BurstType = burstType,
CommandID = Convert.ToByte(taskID), CommandID = Convert.ToByte(taskID),
IsWrite = false, IsWrite = false,
BurstLength = (byte)(currentSegmentSize - 1), BurstLength = (byte)(currentSegmentSize - 1),

View File

@@ -2855,17 +2855,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
@@ -2878,11 +2883,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") {
@@ -2892,12 +2897,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;
@@ -2913,6 +2925,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);
@@ -2921,7 +2940,7 @@ 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);
} }
} }
@@ -3137,98 +3156,14 @@ export class JtagClient {
return Promise.resolve<void>(null as any); return Promise.resolve<void>(null as any);
} }
/**
* 上传比特流文件到服务器
* @param address (optional) 目标设备地址
* @param file (optional) 比特流文件
* @return 上传结果
*/
uploadBitstream(address: string | undefined, file: FileParameter | undefined, cancelToken?: CancelToken): Promise<boolean> {
let url_ = this.baseUrl + "/api/Jtag/UploadBitstream?";
if (address === null)
throw new Error("The parameter 'address' cannot be null.");
else if (address !== undefined)
url_ += "address=" + encodeURIComponent("" + address) + "&";
url_ = url_.replace(/[?&]$/, "");
const content_ = new FormData();
if (file === null || file === undefined)
throw new Error("The parameter 'file' cannot be null.");
else
content_.append("file", file.data, file.fileName ? file.fileName : "file");
let options_: AxiosRequestConfig = {
data: content_,
method: "POST",
url: url_,
headers: {
"Accept": "application/json"
},
cancelToken
};
return this.instance.request(options_).catch((_error: any) => {
if (isAxiosError(_error) && _error.response) {
return _error.response;
} else {
throw _error;
}
}).then((_response: AxiosResponse) => {
return this.processUploadBitstream(_response);
});
}
protected processUploadBitstream(response: AxiosResponse): Promise<boolean> {
const status = response.status;
let _headers: any = {};
if (response.headers && typeof response.headers === "object") {
for (const k in response.headers) {
if (response.headers.hasOwnProperty(k)) {
_headers[k] = response.headers[k];
}
}
}
if (status === 200) {
const _responseText = response.data;
let result200: any = null;
let resultData200 = _responseText;
result200 = resultData200 !== undefined ? resultData200 : <any>null;
return Promise.resolve<boolean>(result200);
} else if (status === 400) {
const _responseText = response.data;
let result400: any = null;
let resultData400 = _responseText;
result400 = resultData400 !== undefined ? resultData400 : <any>null;
return throwException("A server side error occurred.", status, _responseText, _headers, result400);
} else if (status === 401) {
const _responseText = response.data;
let result401: any = null;
let resultData401 = _responseText;
result401 = ProblemDetails.fromJS(resultData401);
return throwException("A server side error occurred.", status, _responseText, _headers, result401);
} else if (status === 500) {
const _responseText = response.data;
return throwException("A server side error occurred.", status, _responseText, _headers);
} else if (status !== 200 && status !== 204) {
const _responseText = response.data;
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}
return Promise.resolve<boolean>(null as any);
}
/** /**
* 通过 JTAG 下载比特流文件到 FPGA 设备 * 通过 JTAG 下载比特流文件到 FPGA 设备
* @param address (optional) JTAG 设备地址 * @param address (optional) JTAG 设备地址
* @param port (optional) JTAG 设备端口 * @param port (optional) JTAG 设备端口
* @param bitstreamId (optional) 比特流ID
* @return 下载结果 * @return 下载结果
*/ */
downloadBitstream(address: string | undefined, port: number | undefined, cancelToken?: CancelToken): Promise<boolean> { downloadBitstream(address: string | undefined, port: number | undefined, bitstreamId: number | undefined, cancelToken?: CancelToken): Promise<boolean> {
let url_ = this.baseUrl + "/api/Jtag/DownloadBitstream?"; let url_ = this.baseUrl + "/api/Jtag/DownloadBitstream?";
if (address === null) if (address === null)
throw new Error("The parameter 'address' cannot be null."); throw new Error("The parameter 'address' cannot be null.");
@@ -3238,6 +3173,10 @@ export class JtagClient {
throw new Error("The parameter 'port' cannot be null."); throw new Error("The parameter 'port' cannot be null.");
else if (port !== undefined) else if (port !== undefined)
url_ += "port=" + encodeURIComponent("" + port) + "&"; url_ += "port=" + encodeURIComponent("" + port) + "&";
if (bitstreamId === null)
throw new Error("The parameter 'bitstreamId' cannot be null.");
else if (bitstreamId !== undefined)
url_ += "bitstreamId=" + encodeURIComponent("" + bitstreamId) + "&";
url_ = url_.replace(/[?&]$/, ""); url_ = url_.replace(/[?&]$/, "");
let options_: AxiosRequestConfig = { let options_: AxiosRequestConfig = {
@@ -6275,6 +6214,353 @@ export class RemoteUpdateClient {
} }
} }
export class ResourceClient {
protected instance: AxiosInstance;
protected baseUrl: string;
protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;
constructor(baseUrl?: string, instance?: AxiosInstance) {
this.instance = instance || axios.create();
this.baseUrl = baseUrl ?? "http://127.0.0.1:5000";
}
/**
* 添加资源(文件上传)
* @param resourceType (optional) 资源类型
* @param resourcePurpose (optional) 资源用途template/user
* @param examID (optional) 所属实验ID可选
* @param file (optional) 资源文件
* @return 添加结果
*/
addResource(resourceType: string | undefined, resourcePurpose: string | undefined, examID: string | null | undefined, file: FileParameter | undefined, cancelToken?: CancelToken): Promise<ResourceInfo> {
let url_ = this.baseUrl + "/api/Resource";
url_ = url_.replace(/[?&]$/, "");
const content_ = new FormData();
if (resourceType === null || resourceType === undefined)
throw new Error("The parameter 'resourceType' cannot be null.");
else
content_.append("ResourceType", resourceType.toString());
if (resourcePurpose === null || resourcePurpose === undefined)
throw new Error("The parameter 'resourcePurpose' cannot be null.");
else
content_.append("ResourcePurpose", resourcePurpose.toString());
if (examID !== null && examID !== undefined)
content_.append("ExamID", examID.toString());
if (file === null || file === undefined)
throw new Error("The parameter 'file' cannot be null.");
else
content_.append("file", file.data, file.fileName ? file.fileName : "file");
let options_: AxiosRequestConfig = {
data: content_,
method: "POST",
url: url_,
headers: {
"Accept": "application/json"
},
cancelToken
};
return this.instance.request(options_).catch((_error: any) => {
if (isAxiosError(_error) && _error.response) {
return _error.response;
} else {
throw _error;
}
}).then((_response: AxiosResponse) => {
return this.processAddResource(_response);
});
}
protected processAddResource(response: AxiosResponse): Promise<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 === 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<ResourceInfo>(null as any);
}
/**
* 获取资源列表
* @param examId (optional) 实验ID可选
* @param resourceType (optional) 资源类型(可选)
* @param resourcePurpose (optional) 资源用途(可选)
* @return 资源列表
*/
getResourceList(examId: string | null | undefined, resourceType: string | null | undefined, resourcePurpose: string | null | undefined, cancelToken?: CancelToken): Promise<ResourceInfo[]> {
let url_ = this.baseUrl + "/api/Resource?";
if (examId !== undefined && examId !== null)
url_ += "examId=" + encodeURIComponent("" + examId) + "&";
if (resourceType !== undefined && resourceType !== null)
url_ += "resourceType=" + encodeURIComponent("" + resourceType) + "&";
if (resourcePurpose !== undefined && resourcePurpose !== null)
url_ += "resourcePurpose=" + encodeURIComponent("" + resourcePurpose) + "&";
url_ = url_.replace(/[?&]$/, "");
let options_: AxiosRequestConfig = {
method: "GET",
url: url_,
headers: {
"Accept": "application/json"
},
cancelToken
};
return this.instance.request(options_).catch((_error: any) => {
if (isAxiosError(_error) && _error.response) {
return _error.response;
} else {
throw _error;
}
}).then((_response: AxiosResponse) => {
return this.processGetResourceList(_response);
});
}
protected processGetResourceList(response: AxiosResponse): Promise<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 === 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 资源文件
*/
getResourceById(resourceId: number, cancelToken?: CancelToken): Promise<FileResponse> {
let url_ = this.baseUrl + "/api/Resource/{resourceId}";
if (resourceId === undefined || resourceId === null)
throw new Error("The parameter 'resourceId' must be defined.");
url_ = url_.replace("{resourceId}", encodeURIComponent("" + resourceId));
url_ = url_.replace(/[?&]$/, "");
let options_: AxiosRequestConfig = {
responseType: "blob",
method: "GET",
url: url_,
headers: {
"Accept": "application/json"
},
cancelToken
};
return this.instance.request(options_).catch((_error: any) => {
if (isAxiosError(_error) && _error.response) {
return _error.response;
} else {
throw _error;
}
}).then((_response: AxiosResponse) => {
return this.processGetResourceById(_response);
});
}
protected processGetResourceById(response: AxiosResponse): Promise<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);
}
/**
* 删除资源
* @param resourceId 资源ID
* @return 删除结果
*/
deleteResource(resourceId: number, cancelToken?: CancelToken): Promise<void> {
let url_ = this.baseUrl + "/api/Resource/{resourceId}";
if (resourceId === undefined || resourceId === null)
throw new Error("The parameter 'resourceId' must be defined.");
url_ = url_.replace("{resourceId}", encodeURIComponent("" + resourceId));
url_ = url_.replace(/[?&]$/, "");
let options_: AxiosRequestConfig = {
method: "DELETE",
url: url_,
headers: {
},
cancelToken
};
return this.instance.request(options_).catch((_error: any) => {
if (isAxiosError(_error) && _error.response) {
return _error.response;
} else {
throw _error;
}
}).then((_response: AxiosResponse) => {
return this.processDeleteResource(_response);
});
}
protected processDeleteResource(response: AxiosResponse): Promise<void> {
const status = response.status;
let _headers: any = {};
if (response.headers && typeof response.headers === "object") {
for (const k in response.headers) {
if (response.headers.hasOwnProperty(k)) {
_headers[k] = response.headers[k];
}
}
}
if (status === 204) {
const _responseText = response.data;
return Promise.resolve<void>(null as any);
} else if (status === 401) {
const _responseText = response.data;
let result401: any = null;
let resultData401 = _responseText;
result401 = ProblemDetails.fromJS(resultData401);
return throwException("A server side error occurred.", status, _responseText, _headers, result401);
} else if (status === 403) {
const _responseText = response.data;
let result403: any = null;
let resultData403 = _responseText;
result403 = ProblemDetails.fromJS(resultData403);
return throwException("A server side error occurred.", status, _responseText, _headers, result403);
} else if (status === 404) {
const _responseText = response.data;
let result404: any = null;
let resultData404 = _responseText;
result404 = ProblemDetails.fromJS(resultData404);
return throwException("A server side error occurred.", status, _responseText, _headers, result404);
} else if (status === 500) {
const _responseText = response.data;
return throwException("A server side error occurred.", status, _responseText, _headers);
} else if (status !== 200 && status !== 204) {
const _responseText = response.data;
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}
return Promise.resolve<void>(null as any);
}
}
export class TutorialClient { export class TutorialClient {
protected instance: AxiosInstance; protected instance: AxiosInstance;
protected baseUrl: string; protected baseUrl: string;
@@ -7476,12 +7762,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) {
@@ -7490,14 +7782,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"];
} }
} }
@@ -7511,9 +7813,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;
} }
} }
@@ -7522,24 +7831,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) {
@@ -7548,14 +7871,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"];
} }
} }
@@ -7569,9 +7903,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;
} }
} }
@@ -7580,58 +7922,101 @@ 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) { if (data) {
for (var property in data) { for (var property in data) {
if (data.hasOwnProperty(property)) if (data.hasOwnProperty(property))
(<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.message = _data["message"]; this.id = _data["id"];
this.updateCount = _data["updateCount"]; 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): ScanResult { static fromJS(data: any): CreateExamRequest {
data = typeof data === 'object' ? data : {}; data = typeof data === 'object' ? data : {};
let result = new ScanResult(); let result = new CreateExamRequest();
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;
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; return data;
} }
} }
/** 扫描结果类 */ /** 创建实验请求类 */
export interface IScanResult { export interface ICreateExamRequest {
/** 结果消息 */ /** 实验ID */
message: string; id: string;
/** 更新的实验数量 */ /** 实验名称 */
updateCount: number; name: string;
/** 实验描述 */
description: string;
/** 实验标签 */
tags: string[];
/** 实验难度1-5 */
difficulty: number;
/** 普通用户是否可见 */
isVisibleToUsers: boolean;
} }
/** 逻辑分析仪运行状态枚举 */ /** 逻辑分析仪运行状态枚举 */
@@ -8083,6 +8468,82 @@ export interface IOscilloscopeDataResponse {
waveformData: string; waveformData: string;
} }
/** 资源信息类 */
export class ResourceInfo implements IResourceInfo {
/** 资源ID */
id!: number;
/** 资源名称 */
name!: string;
/** 资源类型 */
type!: string;
/** 资源用途template/user */
purpose!: string;
/** 上传时间 */
uploadTime!: Date;
/** 所属实验ID可选 */
examID?: string | undefined;
/** MIME类型 */
mimeType?: string | undefined;
constructor(data?: IResourceInfo) {
if (data) {
for (var property in data) {
if (data.hasOwnProperty(property))
(<any>this)[property] = (<any>data)[property];
}
}
}
init(_data?: any) {
if (_data) {
this.id = _data["id"];
this.name = _data["name"];
this.type = _data["type"];
this.purpose = _data["purpose"];
this.uploadTime = _data["uploadTime"] ? new Date(_data["uploadTime"].toString()) : <any>undefined;
this.examID = _data["examID"];
this.mimeType = _data["mimeType"];
}
}
static fromJS(data: any): ResourceInfo {
data = typeof data === 'object' ? data : {};
let result = new ResourceInfo();
result.init(data);
return result;
}
toJSON(data?: any) {
data = typeof data === 'object' ? data : {};
data["id"] = this.id;
data["name"] = this.name;
data["type"] = this.type;
data["purpose"] = this.purpose;
data["uploadTime"] = this.uploadTime ? this.uploadTime.toISOString() : <any>undefined;
data["examID"] = this.examID;
data["mimeType"] = this.mimeType;
return data;
}
}
/** 资源信息类 */
export interface IResourceInfo {
/** 资源ID */
id: number;
/** 资源名称 */
name: string;
/** 资源类型 */
type: string;
/** 资源用途template/user */
purpose: string;
/** 上传时间 */
uploadTime: Date;
/** 所属实验ID可选 */
examID?: string | undefined;
/** MIME类型 */
mimeType?: string | undefined;
}
/** Package options which to send address to read or write */ /** Package options which to send address to read or write */
export class SendAddrPackOptions implements ISendAddrPackOptions { export class SendAddrPackOptions implements ISendAddrPackOptions {
/** 突发类型 */ /** 突发类型 */

View File

@@ -12,6 +12,14 @@ const isDarkMode = ref(
window.matchMedia("(prefers-color-scheme: dark)").matches, window.matchMedia("(prefers-color-scheme: dark)").matches,
); );
// Navbar显示状态管理
const showNavbar = ref(true);
// 切换Navbar显示状态
const toggleNavbar = () => {
showNavbar.value = !showNavbar.value;
};
// 初始化主题设置 // 初始化主题设置
onMounted(() => { onMounted(() => {
// 应用初始主题 // 应用初始主题
@@ -47,6 +55,12 @@ provide("theme", {
toggleTheme, toggleTheme,
}); });
// 提供Navbar控制给子组件
provide("navbar", {
showNavbar,
toggleNavbar,
});
const currentRoutePath = computed(() => { const currentRoutePath = computed(() => {
return router.currentRoute.value.path; return router.currentRoute.value.path;
}); });
@@ -56,8 +70,8 @@ useAlertProvider();
<template> <template>
<div> <div>
<header class="relative"> <header class="relative" :class="{ 'navbar-hidden': !showNavbar }">
<Navbar /> <Navbar v-show="showNavbar" />
<Dialog /> <Dialog />
<Alert /> <Alert />
</header> </header>
@@ -79,4 +93,25 @@ useAlertProvider();
<style scoped> <style scoped>
/* 特定于App.vue的样式 */ /* 特定于App.vue的样式 */
header {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transform-origin: top;
}
.navbar-hidden {
transform: scaleY(0);
height: 0;
overflow: hidden;
}
/* Navbar显示/隐藏动画 */
header .navbar {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transform-origin: top;
}
/* 当header被隐藏时确保navbar也相应变化 */
.navbar-hidden .navbar {
opacity: 0;
}
</style> </style>

View File

@@ -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"

View File

@@ -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";
@@ -188,7 +187,6 @@ import { useAlertStore } from "@/components/Alert";
// 导入 diagram 管理器 // 导入 diagram 管理器
import { import {
loadDiagramData, loadDiagramData,
saveDiagramData,
updatePartPosition, updatePartPosition,
updatePartAttribute, updatePartAttribute,
parseConnectionPin, parseConnectionPin,
@@ -217,6 +215,7 @@ const emit = defineEmits(["toggle-doc-panel", "open-components"]);
// 定义组件接受的属性 // 定义组件接受的属性
const props = defineProps<{ const props = defineProps<{
showDocPanel?: boolean; // 添加属性接收文档面板的显示状态 showDocPanel?: boolean; // 添加属性接收文档面板的显示状态
examId?: string; // 新增examId属性
}>(); }>();
// 获取componentManager实例 // 获取componentManager实例
@@ -606,14 +605,13 @@ function onComponentDrag(e: MouseEvent) {
// 停止拖拽组件 // 停止拖拽组件
function stopComponentDrag() { function stopComponentDrag() {
// 如果有组件被拖拽,保存当前状态 // 如果有组件被拖拽,仅清除拖拽状态(不保存)
if (draggingComponentId.value) { if (draggingComponentId.value) {
draggingComponentId.value = null; draggingComponentId.value = null;
} }
isComponentDragEventActive.value = false; isComponentDragEventActive.value = false;
// 移除自动保存功能 - 不再自动保存到localStorage
saveDiagramData(diagramData.value);
} }
// 更新组件属性 // 更新组件属性
@@ -977,7 +975,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>();

View File

@@ -1,7 +1,6 @@
import { ref, shallowRef, computed, reactive } from "vue"; import { ref, shallowRef, computed, reactive } from "vue";
import { createInjectionState } from "@vueuse/core"; import { createInjectionState } from "@vueuse/core";
import { import {
saveDiagramData,
type DiagramData, type DiagramData,
type DiagramPart, type DiagramPart,
} from "./diagramManager"; } from "./diagramManager";
@@ -302,7 +301,7 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
// 使用 updateDiagramDataDirectly 避免触发加载状态 // 使用 updateDiagramDataDirectly 避免触发加载状态
canvasInstance.updateDiagramDataDirectly(currentData); canvasInstance.updateDiagramDataDirectly(currentData);
saveDiagramData(currentData); // 移除自动保存功能
console.log("组件添加完成:", newComponent); console.log("组件添加完成:", newComponent);
@@ -431,7 +430,7 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
"=== 更新图表数据完成,新组件数量:", "=== 更新图表数据完成,新组件数量:",
currentData.parts.length, currentData.parts.length,
); );
saveDiagramData(currentData); // 移除自动保存功能
return { success: true, message: `已添加 ${templateData.name} 模板` }; return { success: true, message: `已添加 ${templateData.name} 模板` };
} else { } else {
@@ -504,7 +503,7 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
canvasInstance.updateDiagramDataDirectly(currentData); canvasInstance.updateDiagramDataDirectly(currentData);
saveDiagramData(currentData); // 移除自动保存功能
} }
/** /**
@@ -763,11 +762,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;
} }

View File

@@ -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,62 @@ 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 resourceClient = AuthManager.createAuthenticatedResourceClient();
// 获取diagram类型的资源列表
const resources = await resourceClient.getResourceList(examId, 'canvas', 'template');
if (resources && resources.length > 0) {
// 获取第一个diagram资源
const diagramResource = resources[0];
// 使用动态API获取资源文件内容
const response = await resourceClient.getResourceById(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 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);
// 返回空的默认数据结构 // 返回空的默认数据结构
@@ -114,13 +156,10 @@ export function createEmptyDiagram(): DiagramData {
}; };
} }
// 保存图表数据本地存储 // 保存图表数据(已禁用本地存储
export function saveDiagramData(data: DiagramData): void { export function saveDiagramData(data: DiagramData): void {
try { // 本地存储功能已禁用 - 不再保存到localStorage
localStorage.setItem('diagramData', JSON.stringify(data)); console.debug('saveDiagramData called but localStorage saving is disabled');
} catch (error) {
console.error('Error saving diagram data:', error);
}
} }
// 更新组件位置 // 更新组件位置

View File

@@ -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;
} }

View File

@@ -6,6 +6,7 @@ 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';
import { AuthManager } from '@/utils/AuthManager';
const props = defineProps({ const props = defineProps({
content: { content: {
@@ -15,6 +16,10 @@ const props = defineProps({
removeFirstH1: { removeFirstH1: {
type: Boolean, type: Boolean,
default: false default: false
},
examId: {
type: String,
default: ''
} }
}); });
@@ -23,6 +28,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.createAuthenticatedResourceClient();
const resources = await client.getResourceList(examId, 'images', 'template');
// 查找匹配的图片资源
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.createAuthenticatedResourceClient();
const response = await client.getResourceById(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 +95,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 +129,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;
}); });
// 页面挂载后,确保应用正确的主题样式 // 页面挂载后,确保应用正确的主题样式

View File

@@ -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,93 @@ 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 resourceClient = AuthManager.createAuthenticatedResourceClient();
if (response.ok) { const resourceList = await resourceClient.getResourceList(exam.id, 'cover', 'template');
const text = await response.text(); if (resourceList && resourceList.length > 0) {
// 从Markdown提取标题 // 使用第一个封面资源
const titleMatch = text.match(/^#\s+(.+)$/m); const coverResource = resourceList[0];
if (titleMatch && titleMatch[1]) { const fileResponse = await resourceClient.getResourceById(coverResource.id);
title = titleMatch[1].trim(); // 创建Blob URL作为缩略图
} 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 +230,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 }
}); });
}; };

View File

@@ -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 border-2 border-base-300 rounded-lg">
<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,17 +70,20 @@
<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";
interface Props { interface Props {
uploadEvent?: (file: File) => Promise<boolean>; uploadEvent?: (file: File, examId: string) => Promise<number | null>;
downloadEvent?: () => Promise<boolean>; downloadEvent?: (bitstreamId: number) => 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,97 @@ 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 resourceClient = AuthManager.createAuthenticatedResourceClient();
// 使用新的ResourceClient API获取比特流模板资源列表
const resources = await resourceClient.getResourceList(props.examId, 'bitstream', 'template');
availableBitstreams.value = resources.map(r => ({ id: r.id, name: r.name })) || [];
} catch (error) {
console.error('加载比特流列表失败:', error);
availableBitstreams.value = [];
}
}
// 下载示例比特流
async function downloadExampleBitstream(bitstream: {id: number, name: string}) {
if (isDownloading.value) return;
isDownloading.value = true;
try {
const resourceClient = AuthManager.createAuthenticatedResourceClient();
// 使用新的ResourceClient API获取资源文件
const response = await resourceClient.getResourceById(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) return;
isProgramming.value = true;
try {
const resourceClient = AuthManager.createAuthenticatedResourceClient();
if (props.downloadEvent) {
const downloadSuccess = await props.downloadEvent(bitstream.id);
if (downloadSuccess) {
dialog.info("示例比特流烧录成功");
} else {
dialog.error("烧录失败");
}
} else {
dialog.info("示例比特流props.downloadEvent未定义 无法烧录");
}
} 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]; // 获取选中的第一个文件
@@ -85,6 +218,7 @@ function checkFile(file: File): boolean {
} }
async function handleClick(event: Event): Promise<void> { async function handleClick(event: Event): Promise<void> {
console.log("上传按钮被点击");
if (isNull(bitstream.value) || isUndefined(bitstream.value)) { if (isNull(bitstream.value) || isUndefined(bitstream.value)) {
dialog.error(`未选择文件`); dialog.error(`未选择文件`);
return; return;
@@ -97,19 +231,21 @@ async function handleClick(event: Event): Promise<void> {
} }
isUploading.value = true; isUploading.value = true;
let uploadedBitstreamId: number | null = null;
try { try {
const ret = await props.uploadEvent(bitstream.value); console.log("开始上传比特流文件:", bitstream.value.name);
const bitstreamId = await props.uploadEvent(bitstream.value, props.examId || '');
console.log("上传结果ID:", bitstreamId);
if (isUndefined(props.downloadEvent)) { if (isUndefined(props.downloadEvent)) {
if (ret) { console.log("上传成功,下载未定义");
dialog.info("上传成功");
emits("finishedUpload", bitstream.value);
} else dialog.error("上传失败");
return;
}
if (!ret) {
isUploading.value = false; isUploading.value = false;
return; return;
} }
if (bitstreamId === null || bitstreamId === undefined) {
isUploading.value = false;
return;
}
uploadedBitstreamId = bitstreamId;
} catch (e) { } catch (e) {
dialog.error("上传失败"); dialog.error("上传失败");
console.error(e); console.error(e);
@@ -118,9 +254,14 @@ async function handleClick(event: Event): Promise<void> {
// Download // Download
try { try {
const ret = await props.downloadEvent(); console.log("开始下载比特流ID:", uploadedBitstreamId);
if (ret) dialog.info("下载成功"); if (uploadedBitstreamId === null || uploadedBitstreamId === undefined) {
else dialog.error("下载失败"); dialog.error("uploadedBitstreamId is null or undefined");
} else {
const ret = await props.downloadEvent(uploadedBitstreamId);
if (ret) dialog.info("下载成功");
else dialog.error("下载失败");
}
} catch (e) { } catch (e) {
dialog.error("下载失败"); dialog.error("下载失败");
console.error(e); console.error(e);

View File

@@ -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<{

View File

@@ -22,9 +22,9 @@
</div> </div>
<div class="divider"></div> <div class="divider"></div>
<UploadCard <UploadCard
class="bg-base-200" :exam-id="props.examId"
:upload-event="eqps.jtagUploadBitstream" :upload-event="eqps.jtagUploadBitstream"
:download-event="eqps.jtagDownloadBitstream" :download-event="handleDownloadBitstream"
:bitstream-file="eqps.jtagBitstream" :bitstream-file="eqps.jtagBitstream"
@update:bitstream-file="handleBitstreamChange" @update:bitstream-file="handleBitstreamChange"
> >
@@ -91,6 +91,7 @@ import { RefreshCcwIcon } from "lucide-vue-next";
interface CapsProps { interface CapsProps {
jtagFreq?: string; jtagFreq?: string;
examId?: string; // 新增examId属性
} }
const emits = defineEmits<{ const emits = defineEmits<{
@@ -127,6 +128,11 @@ function handleBitstreamChange(file: File | undefined) {
eqps.jtagBitstream = file; eqps.jtagBitstream = file;
} }
async function handleDownloadBitstream(bitstreamId: number): Promise<boolean> {
console.log("开始下载比特流ID:", bitstreamId);
return await eqps.jtagDownloadBitstream(bitstreamId);
}
function handleSelectJtagSpeed(event: Event) { function handleSelectJtagSpeed(event: Event) {
const target = event.target as HTMLSelectElement; const target = event.target as HTMLSelectElement;
eqps.jtagSetSpeed(target.selectedIndex); eqps.jtagSetSpeed(target.selectedIndex);

View File

@@ -1,7 +1,7 @@
import { ref, reactive, watchPostEffect, onMounted, onUnmounted } from "vue"; import { ref, reactive, watchPostEffect, onMounted, onUnmounted } from "vue";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { useLocalStorage } from "@vueuse/core"; import { useLocalStorage } from "@vueuse/core";
import { isString, toNumber, type Dictionary } from "lodash"; import { isString, toNumber, isUndefined, type Dictionary } from "lodash";
import z from "zod"; import z from "zod";
import { isNumber } from "mathjs"; import { isNumber } from "mathjs";
import { Mutex, withTimeout } from "async-mutex"; import { Mutex, withTimeout } from "async-mutex";
@@ -9,8 +9,10 @@ import { useConstraintsStore } from "@/stores/constraints";
import { useDialogStore } from "./dialog"; import { useDialogStore } from "./dialog";
import { toFileParameterOrUndefined } from "@/utils/Common"; import { toFileParameterOrUndefined } from "@/utils/Common";
import { AuthManager } from "@/utils/AuthManager"; import { AuthManager } from "@/utils/AuthManager";
import { HubConnectionBuilder } from "@microsoft/signalr"; import { HubConnection, HubConnectionBuilder } from "@microsoft/signalr";
import { getHubProxyFactory, getReceiverRegister } from "@/TypedSignalR.Client"; import { getHubProxyFactory, getReceiverRegister } from "@/TypedSignalR.Client";
import type { ResourceInfo } from "@/APIClient";
import type { IJtagHub } from "@/TypedSignalR.Client/server.Hubs.JtagHub";
export const useEquipments = defineStore("equipments", () => { export const useEquipments = defineStore("equipments", () => {
// Global Stores // Global Stores
@@ -23,27 +25,38 @@ export const useEquipments = defineStore("equipments", () => {
// Jtag // Jtag
const jtagBitstream = ref<File>(); const jtagBitstream = ref<File>();
const jtagBoundaryScanFreq = ref(100); const jtagBoundaryScanFreq = ref(100);
const jtagUserBitstreams = ref<ResourceInfo[]>([]);
const jtagClientMutex = withTimeout( const jtagClientMutex = withTimeout(
new Mutex(), new Mutex(),
1000, 1000,
new Error("JtagClient Mutex Timeout!"), new Error("JtagClient Mutex Timeout!"),
); );
const jtagHubConnection = new HubConnectionBuilder() const jtagHubConnection = ref<HubConnection>();
.withUrl("/hubs/JtagHub") const jtagHubProxy = ref<IJtagHub>();
.withAutomaticReconnect()
.build(); onMounted(async () => {
const jtagHubProxy = // 每次挂载都重新创建连接
getHubProxyFactory("IJtagHub").createHubProxy(jtagHubConnection); jtagHubConnection.value =
const jtagHubSubscription = getReceiverRegister("IJtagReceiver").register( AuthManager.createAuthenticatedJtagHubConnection();
jtagHubConnection, jtagHubProxy.value = getHubProxyFactory("IJtagHub").createHubProxy(
{ jtagHubConnection.value,
);
getReceiverRegister("IJtagReceiver").register(jtagHubConnection.value, {
onReceiveBoundaryScanData: async (msg) => { onReceiveBoundaryScanData: async (msg) => {
constrainsts.batchSetConstraintStates(msg); constrainsts.batchSetConstraintStates(msg);
}, },
}, });
); await jtagHubConnection.value.start();
onMounted(() => { });
jtagHubConnection.start();
onUnmounted(() => {
// 断开连接,清理资源
if (jtagHubConnection.value) {
jtagHubConnection.value.stop();
jtagHubConnection.value = undefined;
jtagHubProxy.value = undefined;
}
}); });
// Matrix Key // Matrix Key
@@ -87,33 +100,61 @@ export const useEquipments = defineStore("equipments", () => {
} }
async function jtagBoundaryScanSetOnOff(enable: boolean) { async function jtagBoundaryScanSetOnOff(enable: boolean) {
enableJtagBoundaryScan.value = enable; if (isUndefined(jtagHubProxy.value)) {
if (enable) { console.error("JtagHub Not Initialize...");
jtagHubProxy.startBoundaryScan(jtagBoundaryScanFreq.value); return;
} else {
jtagHubProxy.stopBoundaryScan();
} }
if (enable) {
const ret = await jtagHubProxy.value.startBoundaryScan(
jtagBoundaryScanFreq.value,
);
if (!ret) {
console.error("Failed to start boundary scan");
return;
}
} else {
const ret = await jtagHubProxy.value.stopBoundaryScan();
if (!ret) {
console.error("Failed to stop boundary scan");
return;
}
}
enableJtagBoundaryScan.value = enable;
} }
async function jtagUploadBitstream(bitstream: File): Promise<boolean> { async function jtagUploadBitstream(bitstream: File, examId?: string): Promise<number | null> {
try { try {
// 自动开启电源 // 自动开启电源
await powerSetOnOff(true); await powerSetOnOff(true);
const jtagClient = AuthManager.createAuthenticatedJtagClient(); const resourceClient = AuthManager.createAuthenticatedResourceClient();
const resp = await jtagClient.uploadBitstream( const resp = await resourceClient.addResource(
boardAddr.value, 'bitstream',
'user',
examId || null,
toFileParameterOrUndefined(bitstream), toFileParameterOrUndefined(bitstream),
); );
return resp;
// 如果上传成功,设置为当前选中的比特流
if (resp && resp.id !== undefined && resp.id !== null) {
return resp.id;
}
return null;
} catch (e) { } catch (e) {
dialog.error("上传错误"); dialog.error("上传错误");
console.error(e); console.error(e);
return false; return null;
} }
} }
async function jtagDownloadBitstream(): Promise<boolean> { async function jtagDownloadBitstream(bitstreamId?: number): Promise<boolean> {
if (bitstreamId === null || bitstreamId === undefined) {
dialog.error("请先选择要下载的比特流");
return false;
}
const release = await jtagClientMutex.acquire(); const release = await jtagClientMutex.acquire();
try { try {
// 自动开启电源 // 自动开启电源
@@ -123,10 +164,11 @@ export const useEquipments = defineStore("equipments", () => {
const resp = await jtagClient.downloadBitstream( const resp = await jtagClient.downloadBitstream(
boardAddr.value, boardAddr.value,
boardPort.value, boardPort.value,
bitstreamId,
); );
return resp; return resp;
} catch (e) { } catch (e) {
dialog.error("上传错误"); dialog.error("下载错误");
console.error(e); console.error(e);
return false; return false;
} finally { } finally {
@@ -255,6 +297,7 @@ export const useEquipments = defineStore("equipments", () => {
jtagBoundaryScanSetOnOff, jtagBoundaryScanSetOnOff,
jtagBitstream, jtagBitstream,
jtagBoundaryScanFreq, jtagBoundaryScanFreq,
jtagUserBitstreams,
jtagUploadBitstream, jtagUploadBitstream,
jtagDownloadBitstream, jtagDownloadBitstream,
jtagGetIDCode, jtagGetIDCode,

View File

@@ -14,8 +14,12 @@ import {
OscilloscopeApiClient, OscilloscopeApiClient,
DebuggerClient, DebuggerClient,
ExamClient, ExamClient,
ResourceClient,
} from "@/APIClient"; } from "@/APIClient";
import router from "@/router";
import { HubConnectionBuilder } from "@microsoft/signalr";
import axios, { type AxiosInstance } from "axios"; import axios, { type AxiosInstance } from "axios";
import { isNull } from "lodash";
// 支持的客户端类型联合类型 // 支持的客户端类型联合类型
type SupportedClient = type SupportedClient =
@@ -33,7 +37,8 @@ type SupportedClient =
| NetConfigClient | NetConfigClient
| OscilloscopeApiClient | OscilloscopeApiClient
| DebuggerClient | DebuggerClient
| ExamClient; | ExamClient
| ResourceClient;
export class AuthManager { export class AuthManager {
// 存储token到localStorage // 存储token到localStorage
@@ -117,7 +122,7 @@ export class AuthManager {
if (!token) return null; if (!token) return null;
const instance = axios.create(); const instance = axios.create();
instance.interceptors.request.use(config => { instance.interceptors.request.use((config) => {
config.headers = config.headers || {}; config.headers = config.headers || {};
(config.headers as any)["Authorization"] = `Bearer ${token}`; (config.headers as any)["Authorization"] = `Bearer ${token}`;
return config; return config;
@@ -196,6 +201,25 @@ export class AuthManager {
return AuthManager.createAuthenticatedClient(ExamClient); return AuthManager.createAuthenticatedClient(ExamClient);
} }
public static createAuthenticatedResourceClient(): ResourceClient {
return AuthManager.createAuthenticatedClient(ResourceClient);
}
public static createAuthenticatedJtagHubConnection() {
const token = this.getToken();
if (isNull(token)) {
router.push("/login");
throw Error("Token Null!");
}
return new HubConnectionBuilder()
.withUrl("http://127.0.0.1:5000/hubs/JtagHub", {
accessTokenFactory: () => token,
})
.withAutomaticReconnect()
.build();
}
// 登录函数 // 登录函数
public static async login( public static async login(
username: string, username: string,

File diff suppressed because it is too large Load Diff

View File

@@ -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">

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="h-full flex flex-col gap-7"> <div class="h-full flex flex-col gap-7">
<div class="tabs tabs-box flex-shrink-0 shadow-xl mx-5"> <div class="tabs tabs-lift flex-shrink-0 mx-5">
<label class="tab"> <label class="tab">
<input <input
type="radio" type="radio"

View File

@@ -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"
/> />
@@ -36,13 +37,13 @@
<!-- 拖拽分割线 --> <!-- 拖拽分割线 -->
<SplitterResizeHandle <SplitterResizeHandle
id="splitter-group-h-resize-handle" id="splitter-group-h-resize-handle"
class="w-2 bg-base-100 hover:bg-primary hover:opacity-70 transition-colors" class="w-1 bg-base-300"
/> />
<!-- 右侧编辑区域 --> <!-- 右侧编辑区域 -->
<SplitterPanel <SplitterPanel
id="splitter-group-h-panel-properties" id="splitter-group-h-panel-properties"
:min-size="20" :min-size="20"
class="bg-base-200 h-full overflow-hidden flex flex-col" class="bg-base-100 h-full overflow-hidden flex flex-col"
> >
<div class="overflow-y-auto flex-1"> <div class="overflow-y-auto flex-1">
<!-- 使用条件渲染显示不同的面板 --> <!-- 使用条件渲染显示不同的面板 -->
@@ -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>
@@ -70,7 +74,7 @@
<SplitterResizeHandle <SplitterResizeHandle
v-show="!isBottomBarFullscreen" v-show="!isBottomBarFullscreen"
id="splitter-group-v-resize-handle" id="splitter-group-v-resize-handle"
class="h-2 bg-base-100 hover:bg-primary hover:opacity-70 transition-colors" class="h-1 bg-base-300"
/> />
<!-- 功能底栏 --> <!-- 功能底栏 -->
@@ -100,11 +104,32 @@
@close="handleRequestBoardClose" @close="handleRequestBoardClose"
@success="handleRequestBoardSuccess" @success="handleRequestBoardSuccess"
/> />
<!-- Navbar切换浮动按钮 -->
<div
class="navbar-toggle-btn"
:class="{ 'with-navbar': navbarControl.showNavbar.value }"
>
<button
@click="navbarControl.toggleNavbar"
class="btn btn-circle btn-primary shadow-lg hover:shadow-xl transition-all duration-300"
:class="{ 'btn-outline': navbarControl.showNavbar.value }"
:title="navbarControl.showNavbar.value ? '隐藏顶部导航栏' : '显示顶部导航栏'"
>
<!-- 使用SVG图标表示菜单/关闭状态 -->
<svg v-if="navbarControl.showNavbar.value" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, watch } from "vue"; import { ref, onMounted, watch, inject, type Ref } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { useLocalStorage } from '@vueuse/core'; // 添加VueUse导入 import { useLocalStorage } from '@vueuse/core'; // 添加VueUse导入
import { SplitterGroup, SplitterPanel, SplitterResizeHandle } from "reka-ui"; import { SplitterGroup, SplitterPanel, SplitterResizeHandle } from "reka-ui";
@@ -115,7 +140,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";
@@ -133,6 +157,12 @@ const equipments = useEquipments();
const alert = useAlertStore(); const alert = useAlertStore();
// --- Navbar控制 ---
const navbarControl = inject('navbar') as {
showNavbar: Ref<boolean>;
toggleNavbar: () => void;
};
// --- 使用VueUse保存分栏状态 --- // --- 使用VueUse保存分栏状态 ---
// 左右分栏比例默认60% // 左右分栏比例默认60%
const horizontalSplitterSize = useLocalStorage('project-horizontal-splitter-size', 60); const horizontalSplitterSize = useLocalStorage('project-horizontal-splitter-size', 60);
@@ -182,29 +212,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.createAuthenticatedResourceClient();
// 构建文档路径 // 获取markdown类型的模板资源列表
let docPath = `/doc/${tutorialId}/doc.md`; const resources = await client.getResourceList(examId, 'doc', 'template');
// 检查当前路径是否包含下划线(例如 02_key 格式) if (resources && resources.length > 0) {
// 如果不包含,那么使用更新的命名格式 // 获取第一个markdown资源
if (!tutorialId.includes("_")) { const markdownResource = resources[0];
docPath = `/doc/${tutorialId}/doc.md`;
// 使用新的ResourceClient API获取资源文件内容
const response = await client.getResourceById(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无法加载请求的文档。";
@@ -268,8 +306,8 @@ async function checkAndInitializeBoard() {
// 根据实验板信息更新equipment store // 根据实验板信息更新equipment store
function updateEquipmentFromBoard(board: Board) { function updateEquipmentFromBoard(board: Board) {
equipments.setAddr(board.ipAddr); equipments.boardAddr = board.ipAddr;
equipments.setPort(board.port); equipments.boardPort = board.port;
console.log(`实验板信息已更新到equipment store:`, { console.log(`实验板信息已更新到equipment store:`, {
address: board.ipAddr, address: board.ipAddr,
@@ -312,8 +350,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();
} }
@@ -344,7 +382,7 @@ onMounted(async () => {
} }
} }
/* 确保滚动行为仅在需要时出现 */ /* 确保整个页面禁止滚动 */
html, html,
body { body {
overflow: hidden; overflow: hidden;
@@ -376,7 +414,42 @@ body {
:deep(.markdown-content) { :deep(.markdown-content) {
padding: 1rem; padding: 1rem;
background-color: hsl(var(--b1)); background-color: hsl(var(--b1));
border-radius: 0.5rem; }
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
/* Navbar切换浮动按钮样式 */
.navbar-toggle-btn {
position: fixed;
top: 20px;
left: 20px;
z-index: 1000;
transition: all 0.3s ease-in-out;
}
/* 当Navbar显示时调整按钮位置 */
.navbar-toggle-btn.with-navbar {
top: 80px; /* 调整到Navbar下方 */
}
.navbar-toggle-btn button {
backdrop-filter: blur(10px);
background: rgba(var(--p), 0.9);
border: 2px solid rgba(var(--p), 0.3);
transition: all 0.3s ease-in-out;
}
.navbar-toggle-btn button:hover {
transform: scale(1.1);
background: rgba(var(--p), 1);
}
.navbar-toggle-btn button.btn-outline {
background: rgba(var(--b1), 0.9);
color: hsl(var(--p));
border: 2px solid rgba(var(--p), 0.5);
}
.navbar-toggle-btn button.btn-outline:hover {
background: rgba(var(--p), 0.1);
border: 2px solid rgba(var(--p), 0.8);
} }
</style> </style>

View File

@@ -53,10 +53,6 @@ export default defineConfig({
target: "http://localhost:5000", target: "http://localhost:5000",
changeOrigin: true, changeOrigin: true,
}, },
"/hubs": {
target: "http://localhost:5000",
changeOrigin: true,
},
}, },
port: 5173, port: 5173,
}, },