diff --git a/server/Program.cs b/server/Program.cs index 71d2027..ef87db2 100644 --- a/server/Program.cs +++ b/server/Program.cs @@ -194,6 +194,19 @@ try // Setup Program MsgBus.Init(); + // 扫描并更新实验数据库 + try + { + using var db = new Database.AppDataConnection(); + var examFolderPath = Path.Combine(Directory.GetCurrentDirectory(), "exam"); + var updateCount = db.ScanAndUpdateExams(examFolderPath); + logger.Info($"实验数据库扫描完成,更新了 {updateCount} 个实验"); + } + catch (Exception ex) + { + logger.Error($"扫描实验文件夹时出错: {ex.Message}"); + } + // Generate API Client app.MapGet("GetAPIClientCode", async (HttpContext context) => { diff --git a/server/exam/EXP001/doc.md b/server/exam/EXP001/doc.md new file mode 100644 index 0000000..ee33842 --- /dev/null +++ b/server/exam/EXP001/doc.md @@ -0,0 +1,37 @@ +# 实验001:基础逻辑门电路 + +## 实验目的 + +本实验旨在帮助学生理解基础逻辑门的工作原理,包括与门、或门、非门等基本逻辑运算。 + +## 实验内容 + +### 1. 与门(AND Gate) +与门是一个基本的逻辑门,当所有输入都为高电平(1)时,输出才为高电平(1)。 + +### 2. 或门(OR Gate) +或门是另一个基本的逻辑门,当任意一个输入为高电平(1)时,输出就为高电平(1)。 + +### 3. 非门(NOT Gate) +非门是一个反相器,输入为高电平时输出为低电平,反之亦然。 + +## 实验步骤 + +1. 打开 FPGA 开发环境 +2. 创建新的项目文件 +3. 编写 Verilog 代码实现各种逻辑门 +4. 进行仿真验证 +5. 下载到 FPGA 板进行硬件验证 + +## 预期结果 + +通过本实验,学生应该能够: +- 理解基本逻辑门的真值表 +- 掌握 Verilog 代码的基本语法 +- 学会使用 FPGA 开发工具进行仿真 + +## 注意事项 + +- 确保输入信号的电平正确 +- 注意时序的约束 +- 验证结果时要仔细对比真值表 diff --git a/server/exam/EXP002/doc.md b/server/exam/EXP002/doc.md new file mode 100644 index 0000000..f318158 --- /dev/null +++ b/server/exam/EXP002/doc.md @@ -0,0 +1,35 @@ +# 实验002:组合逻辑电路设计 + +## 实验目的 + +本实验旨在让学生学习如何设计和实现复杂的组合逻辑电路,掌握多个逻辑门的组合使用。 + +## 实验内容 + +### 1. 半加器设计 +设计一个半加器电路,实现两个一位二进制数的加法运算。 + +### 2. 全加器设计 +在半加器的基础上,设计全加器电路,考虑进位输入。 + +### 3. 编码器和译码器 +实现简单的编码器和译码器电路。 + +## 实验要求 + +1. 使用 Verilog HDL 编写代码 +2. 绘制逻辑电路图 +3. 编写测试用例验证功能 +4. 分析电路的延时特性 + +## 评估标准 + +- 电路功能正确性 (40%) +- 代码质量和规范性 (30%) +- 测试覆盖率 (20%) +- 实验报告 (10%) + +## 参考资料 + +- 数字逻辑设计教材第3-4章 +- Verilog HDL 语法参考手册 diff --git a/server/src/Controllers/ExamController.cs b/server/src/Controllers/ExamController.cs new file mode 100644 index 0000000..5a434c7 --- /dev/null +++ b/server/src/Controllers/ExamController.cs @@ -0,0 +1,231 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Mvc; +using DotNext; + +namespace server.Controllers; + +/// +/// 实验控制器 +/// +[ApiController] +[Route("api/[controller]")] +public class ExamController : ControllerBase +{ + private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); + + /// + /// 实验信息类 + /// + public class ExamInfo + { + /// + /// 实验的唯一标识符 + /// + public required string ID { get; set; } + + /// + /// 实验文档内容(Markdown格式) + /// + public required string DocContent { get; set; } + + /// + /// 实验创建时间 + /// + public DateTime CreatedTime { get; set; } + + /// + /// 实验最后更新时间 + /// + public DateTime UpdatedTime { get; set; } + } + + /// + /// 实验简要信息类(用于列表显示) + /// + public class ExamSummary + { + /// + /// 实验的唯一标识符 + /// + public required string ID { get; set; } + + /// + /// 实验创建时间 + /// + public DateTime CreatedTime { get; set; } + + /// + /// 实验最后更新时间 + /// + public DateTime UpdatedTime { get; set; } + + /// + /// 实验标题(从文档内容中提取) + /// + public string Title { get; set; } = ""; + } + + /// + /// 扫描结果类 + /// + public class ScanResult + { + /// + /// 结果消息 + /// + public required string Message { get; set; } + + /// + /// 更新的实验数量 + /// + public int UpdateCount { get; set; } + } + + /// + /// 获取所有实验列表 + /// + /// 实验列表 + [Authorize] + [HttpGet("list")] + [EnableCors("Users")] + [ProducesResponseType(typeof(ExamSummary[]), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public IActionResult GetExamList() + { + try + { + using var db = new Database.AppDataConnection(); + var exams = db.GetAllExams(); + + var examSummaries = exams.Select(exam => new ExamSummary + { + ID = exam.ID, + CreatedTime = exam.CreatedTime, + UpdatedTime = exam.UpdatedTime, + Title = ExtractTitleFromMarkdown(exam.DocContent) + }).ToArray(); + + logger.Info($"成功获取实验列表,共 {examSummaries.Length} 个实验"); + return Ok(examSummaries); + } + catch (Exception ex) + { + logger.Error($"获取实验列表时出错: {ex.Message}"); + return StatusCode(StatusCodes.Status500InternalServerError, $"获取实验列表失败: {ex.Message}"); + } + } + + /// + /// 根据实验ID获取实验详细信息 + /// + /// 实验ID + /// 实验详细信息 + [Authorize] + [HttpGet("{examId}")] + [EnableCors("Users")] + [ProducesResponseType(typeof(ExamInfo), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public IActionResult GetExam(string examId) + { + if (string.IsNullOrWhiteSpace(examId)) + return BadRequest("实验ID不能为空"); + + try + { + using var db = new Database.AppDataConnection(); + var result = db.GetExamByID(examId); + + if (!result.IsSuccessful) + { + logger.Error($"获取实验时出错: {result.Error.Message}"); + return StatusCode(StatusCodes.Status500InternalServerError, $"获取实验失败: {result.Error.Message}"); + } + + if (!result.Value.HasValue) + { + logger.Warn($"实验不存在: {examId}"); + return NotFound($"实验 {examId} 不存在"); + } + + var exam = result.Value.Value; + var examInfo = new ExamInfo + { + ID = exam.ID, + DocContent = exam.DocContent, + CreatedTime = exam.CreatedTime, + UpdatedTime = exam.UpdatedTime + }; + + logger.Info($"成功获取实验信息: {examId}"); + return Ok(examInfo); + } + catch (Exception ex) + { + logger.Error($"获取实验 {examId} 时出错: {ex.Message}"); + return StatusCode(StatusCodes.Status500InternalServerError, $"获取实验失败: {ex.Message}"); + } + } + + /// + /// 重新扫描实验文件夹并更新数据库 + /// + /// 更新结果 + [Authorize("Admin")] + [HttpPost("scan")] + [EnableCors("Users")] + [ProducesResponseType(typeof(ScanResult), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public IActionResult ScanExams() + { + try + { + using var db = new Database.AppDataConnection(); + var examFolderPath = Path.Combine(Directory.GetCurrentDirectory(), "exam"); + var updateCount = db.ScanAndUpdateExams(examFolderPath); + + var result = new ScanResult + { + Message = $"扫描完成,更新了 {updateCount} 个实验", + UpdateCount = updateCount + }; + + logger.Info($"手动扫描实验完成,更新了 {updateCount} 个实验"); + return Ok(result); + } + catch (Exception ex) + { + logger.Error($"扫描实验时出错: {ex.Message}"); + return StatusCode(StatusCodes.Status500InternalServerError, $"扫描实验失败: {ex.Message}"); + } + } + + /// + /// 从 Markdown 内容中提取标题 + /// + /// Markdown 内容 + /// 提取的标题 + 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 ""; + } +} diff --git a/server/src/Database.cs b/server/src/Database.cs index 510ff64..da31649 100644 --- a/server/src/Database.cs +++ b/server/src/Database.cs @@ -150,6 +150,36 @@ public class Board } } +/// +/// 实验类,表示实验信息 +/// +public class Exam +{ + /// + /// 实验的唯一标识符 + /// + [PrimaryKey] + public required string ID { get; set; } + + /// + /// 实验文档内容(Markdown格式) + /// + [NotNull] + public required string DocContent { get; set; } + + /// + /// 实验创建时间 + /// + [NotNull] + public DateTime CreatedTime { get; set; } = DateTime.Now; + + /// + /// 实验最后更新时间 + /// + [NotNull] + public DateTime UpdatedTime { get; set; } = DateTime.Now; +} + /// /// 应用程序数据连接类,用于与数据库交互 /// @@ -197,6 +227,7 @@ public class AppDataConnection : DataConnection logger.Info("正在创建数据库表..."); this.CreateTable(); this.CreateTable(); + this.CreateTable(); logger.Info("数据库表创建完成"); } @@ -208,6 +239,7 @@ public class AppDataConnection : DataConnection logger.Warn("正在删除所有数据库表..."); this.DropTable(); this.DropTable(); + this.DropTable(); logger.Warn("所有数据库表已删除"); } @@ -635,4 +667,117 @@ public class AppDataConnection : DataConnection /// FPGA 板子表 /// public ITable BoardTable => this.GetTable(); + + /// + /// 实验表 + /// + public ITable ExamTable => this.GetTable(); + + /// + /// 扫描 exam 文件夹并更新实验数据库 + /// + /// exam 文件夹的路径 + /// 更新的实验数量 + public int ScanAndUpdateExams(string examFolderPath) + { + if (!Directory.Exists(examFolderPath)) + { + logger.Warn($"实验文件夹不存在: {examFolderPath}"); + return 0; + } + + 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 文件"); + continue; + } + + try + { + var docContent = File.ReadAllText(docPath); + var existingExam = this.ExamTable.Where(e => e.ID == examId).FirstOrDefault(); + + if (existingExam == null) + { + // 创建新实验 + var newExam = new Exam + { + ID = examId, + DocContent = docContent, + CreatedTime = DateTime.Now, + UpdatedTime = DateTime.Now + }; + this.Insert(newExam); + logger.Info($"新实验已添加: {examId}"); + updateCount++; + } + else + { + // 更新现有实验 + var fileLastWrite = File.GetLastWriteTime(docPath); + if (fileLastWrite > existingExam.UpdatedTime) + { + this.ExamTable + .Where(e => e.ID == examId) + .Set(e => e.DocContent, docContent) + .Set(e => e.UpdatedTime, DateTime.Now) + .Update(); + logger.Info($"实验已更新: {examId}"); + updateCount++; + } + } + } + catch (Exception ex) + { + logger.Error($"处理实验 {examId} 时出错: {ex.Message}"); + } + } + + logger.Info($"实验扫描完成,共更新 {updateCount} 个实验"); + return updateCount; + } + + /// + /// 获取所有实验信息 + /// + /// 所有实验的数组 + public Exam[] GetAllExams() + { + var exams = this.ExamTable.OrderBy(e => e.ID).ToArray(); + logger.Debug($"获取所有实验,共 {exams.Length} 个"); + return exams; + } + + /// + /// 根据实验ID获取实验信息 + /// + /// 实验ID + /// 包含实验信息的结果,如果未找到则返回空 + public Result> GetExamByID(string examId) + { + var exams = this.ExamTable.Where(exam => exam.ID == examId).ToArray(); + + if (exams.Length > 1) + { + logger.Error($"数据库中存在多个相同ID的实验: {examId}"); + return new(new Exception($"数据库中存在多个相同ID的实验: {examId}")); + } + + if (exams.Length == 0) + { + logger.Info($"未找到ID对应的实验: {examId}"); + return new(Optional.None); + } + + logger.Debug($"成功获取实验信息: {examId}"); + return new(exams[0]); + } } diff --git a/src/APIClient.ts b/src/APIClient.ts index 3641f6c..324260b 100644 --- a/src/APIClient.ts +++ b/src/APIClient.ts @@ -2354,76 +2354,6 @@ export class DebuggerClient { return Promise.resolve(null as any); } - /** - * 重新开始触发(刷新后再启动触发器) - */ - restartTrigger( cancelToken?: CancelToken): Promise { - let url_ = this.baseUrl + "/api/Debugger/RestartTrigger"; - url_ = url_.replace(/[?&]$/, ""); - - let options_: AxiosRequestConfig = { - 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.processRestartTrigger(_response); - }); - } - - protected processRestartTrigger(response: AxiosResponse): Promise { - const status = response.status; - let _headers: any = {}; - if (response.headers && typeof response.headers === "object") { - for (const k in response.headers) { - if (response.headers.hasOwnProperty(k)) { - _headers[k] = response.headers[k]; - } - } - } - if (status === 200) { - const _responseText = response.data; - let result200: any = null; - let resultData200 = _responseText; - result200 = resultData200 !== undefined ? resultData200 : null; - - return Promise.resolve(result200); - - } else if (status === 400) { - const _responseText = response.data; - let result400: any = null; - let resultData400 = _responseText; - result400 = ProblemDetails.fromJS(resultData400); - return throwException("A server side error occurred.", status, _responseText, _headers, result400); - - } else if (status === 500) { - const _responseText = response.data; - return throwException("A server side error occurred.", status, _responseText, _headers); - - } 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 !== 200 && status !== 204) { - const _responseText = response.data; - return throwException("An unexpected server error occurred.", status, _responseText, _headers); - } - return Promise.resolve(null as any); - } - /** * 读取触发器状态标志 */ @@ -2716,6 +2646,241 @@ export class DebuggerClient { } } +export class ExamClient { + 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"; + + } + + /** + * 获取所有实验列表 + * @return 实验列表 + */ + getExamList( cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/Exam/list"; + 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.processGetExamList(_response); + }); + } + + protected processGetExamList(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200) { + const _responseText = response.data; + let result200: any = null; + let resultData200 = _responseText; + if (Array.isArray(resultData200)) { + result200 = [] as any; + for (let item of resultData200) + result200!.push(ExamSummary.fromJS(item)); + } + else { + result200 = null; + } + return Promise.resolve(result200); + + } else if (status === 401) { + const _responseText = response.data; + let result401: any = null; + let resultData401 = _responseText; + result401 = ProblemDetails.fromJS(resultData401); + return throwException("A server side error occurred.", status, _responseText, _headers, result401); + + } else if (status === 500) { + const _responseText = response.data; + return throwException("A server side error occurred.", status, _responseText, _headers); + + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + + /** + * 根据实验ID获取实验详细信息 + * @param examId 实验ID + * @return 实验详细信息 + */ + getExam(examId: string, cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/Exam/{examId}"; + if (examId === undefined || examId === null) + throw new Error("The parameter 'examId' must be defined."); + url_ = url_.replace("{examId}", encodeURIComponent("" + examId)); + 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.processGetExam(_response); + }); + } + + protected processGetExam(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200) { + const _responseText = response.data; + let result200: any = null; + let resultData200 = _responseText; + result200 = ExamInfo.fromJS(resultData200); + return Promise.resolve(result200); + + } else if (status === 400) { + const _responseText = response.data; + let result400: any = null; + let resultData400 = _responseText; + result400 = ProblemDetails.fromJS(resultData400); + return throwException("A server side error occurred.", status, _responseText, _headers, result400); + + } else if (status === 401) { + const _responseText = response.data; + let result401: any = null; + let resultData401 = _responseText; + result401 = ProblemDetails.fromJS(resultData401); + return throwException("A server side error occurred.", status, _responseText, _headers, result401); + + } else if (status === 404) { + const _responseText = response.data; + let result404: any = null; + let resultData404 = _responseText; + result404 = ProblemDetails.fromJS(resultData404); + return throwException("A server side error occurred.", status, _responseText, _headers, result404); + + } else if (status === 500) { + const _responseText = response.data; + return throwException("A server side error occurred.", status, _responseText, _headers); + + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + + /** + * 重新扫描实验文件夹并更新数据库 + * @return 更新结果 + */ + scanExams( cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/Exam/scan"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: AxiosRequestConfig = { + 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.processScanExams(_response); + }); + } + + protected processScanExams(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200) { + const _responseText = response.data; + let result200: any = null; + let resultData200 = _responseText; + result200 = ScanResult.fromJS(resultData200); + return Promise.resolve(result200); + + } else if (status === 401) { + const _responseText = response.data; + let result401: any = null; + let resultData401 = _responseText; + result401 = ProblemDetails.fromJS(resultData401); + return throwException("A server side error occurred.", status, _responseText, _headers, result401); + + } else if (status === 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 === 500) { + const _responseText = response.data; + return throwException("A server side error occurred.", status, _responseText, _headers); + + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } +} + export class JtagClient { protected instance: AxiosInstance; protected baseUrl: string; @@ -7289,6 +7454,168 @@ export interface IChannelCaptureData { data: string; } +/** 实验简要信息类(用于列表显示) */ +export class ExamSummary implements IExamSummary { + /** 实验的唯一标识符 */ + id!: string; + /** 实验创建时间 */ + createdTime!: Date; + /** 实验最后更新时间 */ + updatedTime!: Date; + /** 实验标题(从文档内容中提取) */ + title!: string; + + constructor(data?: IExamSummary) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data?: any) { + if (_data) { + this.id = _data["id"]; + this.createdTime = _data["createdTime"] ? new Date(_data["createdTime"].toString()) : undefined; + this.updatedTime = _data["updatedTime"] ? new Date(_data["updatedTime"].toString()) : undefined; + this.title = _data["title"]; + } + } + + static fromJS(data: any): ExamSummary { + data = typeof data === 'object' ? data : {}; + let result = new ExamSummary(); + result.init(data); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["id"] = this.id; + data["createdTime"] = this.createdTime ? this.createdTime.toISOString() : undefined; + data["updatedTime"] = this.updatedTime ? this.updatedTime.toISOString() : undefined; + data["title"] = this.title; + return data; + } +} + +/** 实验简要信息类(用于列表显示) */ +export interface IExamSummary { + /** 实验的唯一标识符 */ + id: string; + /** 实验创建时间 */ + createdTime: Date; + /** 实验最后更新时间 */ + updatedTime: Date; + /** 实验标题(从文档内容中提取) */ + title: string; +} + +/** 实验信息类 */ +export class ExamInfo implements IExamInfo { + /** 实验的唯一标识符 */ + id!: string; + /** 实验文档内容(Markdown格式) */ + docContent!: string; + /** 实验创建时间 */ + createdTime!: Date; + /** 实验最后更新时间 */ + updatedTime!: Date; + + constructor(data?: IExamInfo) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data?: any) { + if (_data) { + this.id = _data["id"]; + this.docContent = _data["docContent"]; + this.createdTime = _data["createdTime"] ? new Date(_data["createdTime"].toString()) : undefined; + this.updatedTime = _data["updatedTime"] ? new Date(_data["updatedTime"].toString()) : undefined; + } + } + + static fromJS(data: any): ExamInfo { + data = typeof data === 'object' ? data : {}; + let result = new ExamInfo(); + result.init(data); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["id"] = this.id; + data["docContent"] = this.docContent; + data["createdTime"] = this.createdTime ? this.createdTime.toISOString() : undefined; + data["updatedTime"] = this.updatedTime ? this.updatedTime.toISOString() : undefined; + return data; + } +} + +/** 实验信息类 */ +export interface IExamInfo { + /** 实验的唯一标识符 */ + id: string; + /** 实验文档内容(Markdown格式) */ + docContent: string; + /** 实验创建时间 */ + createdTime: Date; + /** 实验最后更新时间 */ + updatedTime: Date; +} + +/** 扫描结果类 */ +export class ScanResult implements IScanResult { + /** 结果消息 */ + declare message: string; + /** 更新的实验数量 */ + updateCount!: number; + + constructor(data?: IScanResult) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data?: any) { + if (_data) { + this.message = _data["message"]; + this.updateCount = _data["updateCount"]; + } + } + + static fromJS(data: any): ScanResult { + data = typeof data === 'object' ? data : {}; + let result = new ScanResult(); + result.init(data); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["message"] = this.message; + data["updateCount"] = this.updateCount; + return data; + } +} + +/** 扫描结果类 */ +export interface IScanResult { + /** 结果消息 */ + message: string; + /** 更新的实验数量 */ + updateCount: number; +} + /** 逻辑分析仪运行状态枚举 */ export enum CaptureStatus { None = 0, diff --git a/src/components/Navbar.vue b/src/components/Navbar.vue index 6f2a369..2eacc9d 100644 --- a/src/components/Navbar.vue +++ b/src/components/Navbar.vue @@ -31,6 +31,12 @@ 工程界面 +
  • + + + 实验列表 + +
  • diff --git a/src/router/index.ts b/src/router/index.ts index caadc34..56158e0 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -4,6 +4,7 @@ import AuthView from "../views/AuthView.vue"; import ProjectView from "../views/Project/Index.vue"; import TestView from "../views/TestView.vue"; import UserView from "@/views/User/Index.vue"; +import ExamView from "@/views/ExamView.vue"; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -13,6 +14,7 @@ const router = createRouter({ { path: "/project", name: "project", component: ProjectView }, { path: "/test", name: "test", component: TestView }, { path: "/user", name: "user", component: UserView }, + { path: "/exam", name: "exam", component: ExamView }, ], }); diff --git a/src/utils/AuthManager.ts b/src/utils/AuthManager.ts index 8210bcd..25750ae 100644 --- a/src/utils/AuthManager.ts +++ b/src/utils/AuthManager.ts @@ -13,6 +13,7 @@ import { NetConfigClient, OscilloscopeApiClient, DebuggerClient, + ExamClient, } from "@/APIClient"; import axios, { type AxiosInstance } from "axios"; @@ -31,7 +32,8 @@ type SupportedClient = | UDPClient | NetConfigClient | OscilloscopeApiClient - | DebuggerClient; + | DebuggerClient + | ExamClient; export class AuthManager { // 存储token到localStorage @@ -190,6 +192,10 @@ export class AuthManager { return AuthManager.createAuthenticatedClient(DebuggerClient); } + public static createAuthenticatedExamClient(): ExamClient { + return AuthManager.createAuthenticatedClient(ExamClient); + } + // 登录函数 public static async login( username: string, diff --git a/src/views/ExamView.vue b/src/views/ExamView.vue new file mode 100644 index 0000000..3a2d0d8 --- /dev/null +++ b/src/views/ExamView.vue @@ -0,0 +1,516 @@ + + + + +