feat: 实现可编辑已有的实验
This commit is contained in:
		@@ -28,7 +28,7 @@ public class ExamController : ControllerBase
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    [HttpGet("list")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(ExamSummary[]), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(typeof(ExamInfo[]), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public IActionResult GetExamList()
 | 
			
		||||
@@ -37,19 +37,10 @@ public class ExamController : ControllerBase
 | 
			
		||||
        {
 | 
			
		||||
            var exams = _examManager.GetAllExams();
 | 
			
		||||
 | 
			
		||||
            var examSummaries = exams.Select(exam => new ExamSummary
 | 
			
		||||
            {
 | 
			
		||||
                ID = exam.ID,
 | 
			
		||||
                Name = exam.Name,
 | 
			
		||||
                CreatedTime = exam.CreatedTime,
 | 
			
		||||
                UpdatedTime = exam.UpdatedTime,
 | 
			
		||||
                Tags = exam.GetTagsList(),
 | 
			
		||||
                Difficulty = exam.Difficulty,
 | 
			
		||||
                IsVisibleToUsers = exam.IsVisibleToUsers
 | 
			
		||||
            }).ToArray();
 | 
			
		||||
            var examInfos = exams.Select(exam => new ExamInfo(exam)).ToArray();
 | 
			
		||||
 | 
			
		||||
            logger.Info($"成功获取实验列表,共 {examSummaries.Length} 个实验");
 | 
			
		||||
            return Ok(examSummaries);
 | 
			
		||||
            logger.Info($"成功获取实验列表,共 {examInfos.Length} 个实验");
 | 
			
		||||
            return Ok(examInfos);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
@@ -93,17 +84,7 @@ public class ExamController : ControllerBase
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var exam = result.Value.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
 | 
			
		||||
            };
 | 
			
		||||
            var examInfo = new ExamInfo(exam);
 | 
			
		||||
 | 
			
		||||
            logger.Info($"成功获取实验信息: {examId}");
 | 
			
		||||
            return Ok(examInfo);
 | 
			
		||||
@@ -121,7 +102,7 @@ public class ExamController : ControllerBase
 | 
			
		||||
    /// <param name="request">创建实验请求</param>
 | 
			
		||||
    /// <returns>创建结果</returns>
 | 
			
		||||
    [Authorize("Admin")]
 | 
			
		||||
    [HttpPost]
 | 
			
		||||
    [HttpPost("create")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(ExamInfo), StatusCodes.Status201Created)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
			
		||||
@@ -129,7 +110,7 @@ public class ExamController : ControllerBase
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status409Conflict)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public IActionResult CreateExam([FromBody] CreateExamRequest request)
 | 
			
		||||
    public IActionResult CreateExam([FromBody] ExamDto request)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(request.ID) || string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.Description))
 | 
			
		||||
            return BadRequest("实验ID、名称和描述不能为空");
 | 
			
		||||
@@ -148,17 +129,7 @@ public class ExamController : ControllerBase
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            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
 | 
			
		||||
            };
 | 
			
		||||
            var examInfo = new ExamInfo(exam);
 | 
			
		||||
 | 
			
		||||
            logger.Info($"成功创建实验: {request.ID}");
 | 
			
		||||
            return CreatedAtAction(nameof(GetExam), new { examId = request.ID }, examInfo);
 | 
			
		||||
@@ -170,127 +141,168 @@ public class ExamController : ControllerBase
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验信息类
 | 
			
		||||
    /// 更新实验信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class ExamInfo
 | 
			
		||||
    /// <param name="request">更新实验请求</param>
 | 
			
		||||
    /// <returns>更新结果</returns>
 | 
			
		||||
    [Authorize("Admin")]
 | 
			
		||||
    [HttpPost("update")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(ExamInfo), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public IActionResult UpdateExam([FromBody] ExamDto request)
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验的唯一标识符
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public required string ID { get; set; }
 | 
			
		||||
        var examId = request.ID;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验名称
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public required string Name { get; set; }
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            // 首先检查实验是否存在
 | 
			
		||||
            var existingExamResult = _examManager.GetExamByID(examId);
 | 
			
		||||
            if (!existingExamResult.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"检查实验是否存在时出错: {existingExamResult.Error.Message}");
 | 
			
		||||
                return StatusCode(StatusCodes.Status500InternalServerError, $"检查实验失败: {existingExamResult.Error.Message}");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验描述
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public required string Description { get; set; }
 | 
			
		||||
            if (!existingExamResult.Value.HasValue)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Warn($"要更新的实验不存在: {examId}");
 | 
			
		||||
                return NotFound($"实验 {examId} 不存在");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验创建时间
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public DateTime CreatedTime { get; set; }
 | 
			
		||||
            // 执行更新
 | 
			
		||||
            var updateResult = _examManager.UpdateExam(
 | 
			
		||||
                examId,
 | 
			
		||||
                request.Name,
 | 
			
		||||
                request.Description,
 | 
			
		||||
                request.Tags,
 | 
			
		||||
                request.Difficulty,
 | 
			
		||||
                request.IsVisibleToUsers
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验最后更新时间
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public DateTime UpdatedTime { get; set; }
 | 
			
		||||
            if (!updateResult.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"更新实验时出错: {updateResult.Error.Message}");
 | 
			
		||||
                return StatusCode(StatusCodes.Status500InternalServerError, $"更新实验失败: {updateResult.Error.Message}");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验标签
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public string[] Tags { get; set; } = Array.Empty<string>();
 | 
			
		||||
            // 获取更新后的实验信息并返回
 | 
			
		||||
            var updatedExamResult = _examManager.GetExamByID(examId);
 | 
			
		||||
            if (!updatedExamResult.IsSuccessful || !updatedExamResult.Value.HasValue)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"获取更新后的实验信息失败: {examId}");
 | 
			
		||||
                return StatusCode(StatusCodes.Status500InternalServerError, "更新成功但获取更新后信息失败");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验难度(1-5)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public int Difficulty { get; set; } = 1;
 | 
			
		||||
            var updatedExam = updatedExamResult.Value.Value;
 | 
			
		||||
            var examInfo = new ExamInfo(updatedExam);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 普通用户是否可见
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public bool IsVisibleToUsers { get; set; } = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验简要信息类(用于列表显示)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class ExamSummary
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验的唯一标识符
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public required string ID { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验名称
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public required string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验创建时间
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public DateTime CreatedTime { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验最后更新时间
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        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>
 | 
			
		||||
    public class CreateExamRequest
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验ID
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public required string ID { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验名称
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public required string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验描述
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public required string Description { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验标签
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public string[] Tags { get; set; } = Array.Empty<string>();
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 实验难度(1-5)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public int Difficulty { get; set; } = 1;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 普通用户是否可见
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public bool IsVisibleToUsers { get; set; } = true;
 | 
			
		||||
            logger.Info($"成功更新实验: {examId},更新记录数: {updateResult.Value}");
 | 
			
		||||
            return Ok(examInfo);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"更新实验 {examId} 时出错: {ex.Message}");
 | 
			
		||||
            return StatusCode(StatusCodes.Status500InternalServerError, $"更新实验失败: {ex.Message}");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 实验信息
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class ExamInfo
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验的唯一标识符
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string ID { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验名称
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验描述
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string Description { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验创建时间
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public DateTime CreatedTime { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验最后更新时间
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    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;
 | 
			
		||||
 | 
			
		||||
    public ExamInfo(Database.Exam exam)
 | 
			
		||||
    {
 | 
			
		||||
        ID = exam.ID;
 | 
			
		||||
        Name = exam.Name;
 | 
			
		||||
        Description = exam.Description;
 | 
			
		||||
        CreatedTime = exam.CreatedTime;
 | 
			
		||||
        UpdatedTime = exam.UpdatedTime;
 | 
			
		||||
        Tags = exam.GetTagsList();
 | 
			
		||||
        Difficulty = exam.Difficulty;
 | 
			
		||||
        IsVisibleToUsers = exam.IsVisibleToUsers;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 统一的实验数据传输对象
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class ExamDto
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验的唯一标识符
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public required string ID { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验名称
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public required string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验描述
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public required string Description { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验标签
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string[] Tags { get; set; } = Array.Empty<string>();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 实验难度(1-5)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public int Difficulty { get; set; } = 1;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 普通用户是否可见
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool IsVisibleToUsers { get; set; } = true;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -165,7 +165,7 @@ public class ResourceManager
 | 
			
		||||
            if (duplicateResource != null && duplicateResource.ResourceName == resourceName)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Info($"资源已存在: {resourceName}");
 | 
			
		||||
                return new(new Exception($"资源已存在: {resourceName}"));
 | 
			
		||||
                return duplicateResource;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var nowTime = DateTime.Now;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										248
									
								
								src/APIClient.ts
									
									
									
									
									
								
							
							
						
						
									
										248
									
								
								src/APIClient.ts
									
									
									
									
									
								
							@@ -299,7 +299,7 @@ export class VideoStreamClient {
 | 
			
		||||
        return Promise.resolve<boolean>(null as any);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setVideoStreamEnable(enable: boolean | undefined, cancelToken?: CancelToken): Promise<FileResponse | null> {
 | 
			
		||||
    setVideoStreamEnable(enable: boolean | undefined, cancelToken?: CancelToken): Promise<any> {
 | 
			
		||||
        let url_ = this.baseUrl + "/api/VideoStream/SetVideoStreamEnable?";
 | 
			
		||||
        if (enable === null)
 | 
			
		||||
            throw new Error("The parameter 'enable' cannot be null.");
 | 
			
		||||
@@ -308,11 +308,10 @@ export class VideoStreamClient {
 | 
			
		||||
        url_ = url_.replace(/[?&]$/, "");
 | 
			
		||||
 | 
			
		||||
        let options_: AxiosRequestConfig = {
 | 
			
		||||
            responseType: "blob",
 | 
			
		||||
            method: "POST",
 | 
			
		||||
            url: url_,
 | 
			
		||||
            headers: {
 | 
			
		||||
                "Accept": "application/octet-stream"
 | 
			
		||||
                "Accept": "application/json"
 | 
			
		||||
            },
 | 
			
		||||
            cancelToken
 | 
			
		||||
        };
 | 
			
		||||
@@ -328,7 +327,7 @@ export class VideoStreamClient {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected processSetVideoStreamEnable(response: AxiosResponse): Promise<FileResponse | null> {
 | 
			
		||||
    protected processSetVideoStreamEnable(response: AxiosResponse): Promise<any> {
 | 
			
		||||
        const status = response.status;
 | 
			
		||||
        let _headers: any = {};
 | 
			
		||||
        if (response.headers && typeof response.headers === "object") {
 | 
			
		||||
@@ -338,22 +337,27 @@ export class VideoStreamClient {
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        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 });
 | 
			
		||||
        if (status === 200) {
 | 
			
		||||
            const _responseText = response.data;
 | 
			
		||||
            let result200: any = null;
 | 
			
		||||
            let resultData200  = _responseText;
 | 
			
		||||
                result200 = resultData200 !== undefined ? resultData200 : <any>null;
 | 
			
		||||
    
 | 
			
		||||
            return Promise.resolve<any>(result200);
 | 
			
		||||
 | 
			
		||||
        } else if (status === 500) {
 | 
			
		||||
            const _responseText = response.data;
 | 
			
		||||
            let result500: any = null;
 | 
			
		||||
            let resultData500  = _responseText;
 | 
			
		||||
                result500 = resultData500 !== undefined ? resultData500 : <any>null;
 | 
			
		||||
    
 | 
			
		||||
            return throwException("A server side error occurred.", status, _responseText, _headers, result500);
 | 
			
		||||
 | 
			
		||||
        } 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>(null as any);
 | 
			
		||||
        return Promise.resolve<any>(null as any);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -2505,7 +2509,7 @@ export class ExamClient {
 | 
			
		||||
     * 获取所有实验列表
 | 
			
		||||
     * @return 实验列表
 | 
			
		||||
     */
 | 
			
		||||
    getExamList( cancelToken?: CancelToken): Promise<ExamSummary[]> {
 | 
			
		||||
    getExamList( cancelToken?: CancelToken): Promise<ExamInfo[]> {
 | 
			
		||||
        let url_ = this.baseUrl + "/api/Exam/list";
 | 
			
		||||
        url_ = url_.replace(/[?&]$/, "");
 | 
			
		||||
 | 
			
		||||
@@ -2529,7 +2533,7 @@ export class ExamClient {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected processGetExamList(response: AxiosResponse): Promise<ExamSummary[]> {
 | 
			
		||||
    protected processGetExamList(response: AxiosResponse): Promise<ExamInfo[]> {
 | 
			
		||||
        const status = response.status;
 | 
			
		||||
        let _headers: any = {};
 | 
			
		||||
        if (response.headers && typeof response.headers === "object") {
 | 
			
		||||
@@ -2546,12 +2550,12 @@ export class ExamClient {
 | 
			
		||||
            if (Array.isArray(resultData200)) {
 | 
			
		||||
                result200 = [] as any;
 | 
			
		||||
                for (let item of resultData200)
 | 
			
		||||
                    result200!.push(ExamSummary.fromJS(item));
 | 
			
		||||
                    result200!.push(ExamInfo.fromJS(item));
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                result200 = <any>null;
 | 
			
		||||
            }
 | 
			
		||||
            return Promise.resolve<ExamSummary[]>(result200);
 | 
			
		||||
            return Promise.resolve<ExamInfo[]>(result200);
 | 
			
		||||
 | 
			
		||||
        } else if (status === 401) {
 | 
			
		||||
            const _responseText = response.data;
 | 
			
		||||
@@ -2568,7 +2572,7 @@ export class ExamClient {
 | 
			
		||||
            const _responseText = response.data;
 | 
			
		||||
            return throwException("An unexpected server error occurred.", status, _responseText, _headers);
 | 
			
		||||
        }
 | 
			
		||||
        return Promise.resolve<ExamSummary[]>(null as any);
 | 
			
		||||
        return Promise.resolve<ExamInfo[]>(null as any);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -2657,8 +2661,8 @@ export class ExamClient {
 | 
			
		||||
     * @param request 创建实验请求
 | 
			
		||||
     * @return 创建结果
 | 
			
		||||
     */
 | 
			
		||||
    createExam(request: CreateExamRequest, cancelToken?: CancelToken): Promise<ExamInfo> {
 | 
			
		||||
        let url_ = this.baseUrl + "/api/Exam";
 | 
			
		||||
    createExam(request: ExamDto, cancelToken?: CancelToken): Promise<ExamInfo> {
 | 
			
		||||
        let url_ = this.baseUrl + "/api/Exam/create";
 | 
			
		||||
        url_ = url_.replace(/[?&]$/, "");
 | 
			
		||||
 | 
			
		||||
        const content_ = JSON.stringify(request);
 | 
			
		||||
@@ -2740,6 +2744,95 @@ export class ExamClient {
 | 
			
		||||
        }
 | 
			
		||||
        return Promise.resolve<ExamInfo>(null as any);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 更新实验信息
 | 
			
		||||
     * @param request 更新实验请求
 | 
			
		||||
     * @return 更新结果
 | 
			
		||||
     */
 | 
			
		||||
    updateExam(request: ExamDto, cancelToken?: CancelToken): Promise<ExamInfo> {
 | 
			
		||||
        let url_ = this.baseUrl + "/api/Exam/update";
 | 
			
		||||
        url_ = url_.replace(/[?&]$/, "");
 | 
			
		||||
 | 
			
		||||
        const content_ = JSON.stringify(request);
 | 
			
		||||
 | 
			
		||||
        let options_: AxiosRequestConfig = {
 | 
			
		||||
            data: content_,
 | 
			
		||||
            method: "POST",
 | 
			
		||||
            url: url_,
 | 
			
		||||
            headers: {
 | 
			
		||||
                "Content-Type": "application/json",
 | 
			
		||||
                "Accept": "application/json"
 | 
			
		||||
            },
 | 
			
		||||
            cancelToken
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return this.instance.request(options_).catch((_error: any) => {
 | 
			
		||||
            if (isAxiosError(_error) && _error.response) {
 | 
			
		||||
                return _error.response;
 | 
			
		||||
            } else {
 | 
			
		||||
                throw _error;
 | 
			
		||||
            }
 | 
			
		||||
        }).then((_response: AxiosResponse) => {
 | 
			
		||||
            return this.processUpdateExam(_response);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected processUpdateExam(response: AxiosResponse): Promise<ExamInfo> {
 | 
			
		||||
        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<ExamInfo>(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 === 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<ExamInfo>(null as any);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class HdmiVideoStreamClient {
 | 
			
		||||
@@ -7802,94 +7895,7 @@ export interface IChannelCaptureData {
 | 
			
		||||
    data: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 实验简要信息类(用于列表显示) */
 | 
			
		||||
export class ExamSummary implements IExamSummary {
 | 
			
		||||
    /** 实验的唯一标识符 */
 | 
			
		||||
    id!: string;
 | 
			
		||||
    /** 实验名称 */
 | 
			
		||||
    name!: string;
 | 
			
		||||
    /** 实验创建时间 */
 | 
			
		||||
    createdTime!: Date;
 | 
			
		||||
    /** 实验最后更新时间 */
 | 
			
		||||
    updatedTime!: Date;
 | 
			
		||||
    /** 实验标签 */
 | 
			
		||||
    tags!: string[];
 | 
			
		||||
    /** 实验难度(1-5) */
 | 
			
		||||
    difficulty!: number;
 | 
			
		||||
    /** 普通用户是否可见 */
 | 
			
		||||
    isVisibleToUsers!: boolean;
 | 
			
		||||
 | 
			
		||||
    constructor(data?: IExamSummary) {
 | 
			
		||||
        if (data) {
 | 
			
		||||
            for (var property in data) {
 | 
			
		||||
                if (data.hasOwnProperty(property))
 | 
			
		||||
                    (<any>this)[property] = (<any>data)[property];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (!data) {
 | 
			
		||||
            this.tags = [];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    init(_data?: any) {
 | 
			
		||||
        if (_data) {
 | 
			
		||||
            this.id = _data["id"];
 | 
			
		||||
            this.name = _data["name"];
 | 
			
		||||
            this.createdTime = _data["createdTime"] ? new Date(_data["createdTime"].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"];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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["name"] = this.name;
 | 
			
		||||
        data["createdTime"] = this.createdTime ? this.createdTime.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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 实验简要信息类(用于列表显示) */
 | 
			
		||||
export interface IExamSummary {
 | 
			
		||||
    /** 实验的唯一标识符 */
 | 
			
		||||
    id: string;
 | 
			
		||||
    /** 实验名称 */
 | 
			
		||||
    name: string;
 | 
			
		||||
    /** 实验创建时间 */
 | 
			
		||||
    createdTime: Date;
 | 
			
		||||
    /** 实验最后更新时间 */
 | 
			
		||||
    updatedTime: Date;
 | 
			
		||||
    /** 实验标签 */
 | 
			
		||||
    tags: string[];
 | 
			
		||||
    /** 实验难度(1-5) */
 | 
			
		||||
    difficulty: number;
 | 
			
		||||
    /** 普通用户是否可见 */
 | 
			
		||||
    isVisibleToUsers: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 实验信息类 */
 | 
			
		||||
/** 实验信息 */
 | 
			
		||||
export class ExamInfo implements IExamInfo {
 | 
			
		||||
    /** 实验的唯一标识符 */
 | 
			
		||||
    id!: string;
 | 
			
		||||
@@ -7962,7 +7968,7 @@ export class ExamInfo implements IExamInfo {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 实验信息类 */
 | 
			
		||||
/** 实验信息 */
 | 
			
		||||
export interface IExamInfo {
 | 
			
		||||
    /** 实验的唯一标识符 */
 | 
			
		||||
    id: string;
 | 
			
		||||
@@ -7982,9 +7988,9 @@ export interface IExamInfo {
 | 
			
		||||
    isVisibleToUsers: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 创建实验请求类 */
 | 
			
		||||
export class CreateExamRequest implements ICreateExamRequest {
 | 
			
		||||
    /** 实验ID */
 | 
			
		||||
/** 统一的实验数据传输对象 */
 | 
			
		||||
export class ExamDto implements IExamDto {
 | 
			
		||||
    /** 实验的唯一标识符 */
 | 
			
		||||
    id!: string;
 | 
			
		||||
    /** 实验名称 */
 | 
			
		||||
    name!: string;
 | 
			
		||||
@@ -7997,7 +8003,7 @@ export class CreateExamRequest implements ICreateExamRequest {
 | 
			
		||||
    /** 普通用户是否可见 */
 | 
			
		||||
    isVisibleToUsers!: boolean;
 | 
			
		||||
 | 
			
		||||
    constructor(data?: ICreateExamRequest) {
 | 
			
		||||
    constructor(data?: IExamDto) {
 | 
			
		||||
        if (data) {
 | 
			
		||||
            for (var property in data) {
 | 
			
		||||
                if (data.hasOwnProperty(property))
 | 
			
		||||
@@ -8024,9 +8030,9 @@ export class CreateExamRequest implements ICreateExamRequest {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static fromJS(data: any): CreateExamRequest {
 | 
			
		||||
    static fromJS(data: any): ExamDto {
 | 
			
		||||
        data = typeof data === 'object' ? data : {};
 | 
			
		||||
        let result = new CreateExamRequest();
 | 
			
		||||
        let result = new ExamDto();
 | 
			
		||||
        result.init(data);
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
@@ -8047,9 +8053,9 @@ export class CreateExamRequest implements ICreateExamRequest {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 创建实验请求类 */
 | 
			
		||||
export interface ICreateExamRequest {
 | 
			
		||||
    /** 实验ID */
 | 
			
		||||
/** 统一的实验数据传输对象 */
 | 
			
		||||
export interface IExamDto {
 | 
			
		||||
    /** 实验的唯一标识符 */
 | 
			
		||||
    id: string;
 | 
			
		||||
    /** 实验名称 */
 | 
			
		||||
    name: string;
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,10 @@
 | 
			
		||||
  <div class="flex items-center justify-center min-h-screen bg-base-200">
 | 
			
		||||
    <div class="relative w-full max-w-md">
 | 
			
		||||
      <!-- Login Card -->
 | 
			
		||||
      <div v-if="!showSignUp" class="card card-dash h-80 w-100 shadow-xl bg-base-100">
 | 
			
		||||
      <div
 | 
			
		||||
        v-if="!showSignUp"
 | 
			
		||||
        class="card card-dash h-80 w-100 shadow-xl bg-base-100"
 | 
			
		||||
      >
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
          <h1 class="card-title place-self-center my-3 text-2xl">用户登录</h1>
 | 
			
		||||
          <div class="flex flex-col w-full h-full">
 | 
			
		||||
@@ -44,7 +47,10 @@
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <!-- Sign Up Card -->
 | 
			
		||||
      <div v-if="showSignUp" class="card card-dash h-96 w-100 shadow-xl bg-base-100">
 | 
			
		||||
      <div
 | 
			
		||||
        v-if="showSignUp"
 | 
			
		||||
        class="card card-dash h-96 w-100 shadow-xl bg-base-100"
 | 
			
		||||
      >
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
          <h1 class="card-title place-self-center my-3 text-2xl">用户注册</h1>
 | 
			
		||||
          <div class="flex flex-col w-full h-full">
 | 
			
		||||
@@ -122,7 +128,7 @@ const isSignUpLoading = ref(false);
 | 
			
		||||
const signUpData = ref({
 | 
			
		||||
  username: "",
 | 
			
		||||
  email: "",
 | 
			
		||||
  password: ""
 | 
			
		||||
  password: "",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 登录处理函数
 | 
			
		||||
@@ -149,7 +155,7 @@ const handleLogin = async () => {
 | 
			
		||||
 | 
			
		||||
    // 短暂延迟后跳转到project页面
 | 
			
		||||
    setTimeout(async () => {
 | 
			
		||||
      await router.push("/project");
 | 
			
		||||
      router.go(-1);
 | 
			
		||||
    }, 1000);
 | 
			
		||||
  } catch (error: any) {
 | 
			
		||||
    console.error("Login error:", error);
 | 
			
		||||
@@ -180,7 +186,7 @@ const handleRegister = () => {
 | 
			
		||||
  signUpData.value = {
 | 
			
		||||
    username: "",
 | 
			
		||||
    email: "",
 | 
			
		||||
    password: ""
 | 
			
		||||
    password: "",
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -227,13 +233,13 @@ const handleSignUp = async () => {
 | 
			
		||||
    const result = await dataClient.signUpUser(
 | 
			
		||||
      signUpData.value.username.trim(),
 | 
			
		||||
      signUpData.value.email.trim(),
 | 
			
		||||
      signUpData.value.password.trim()
 | 
			
		||||
      signUpData.value.password.trim(),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (result) {
 | 
			
		||||
      // 注册成功
 | 
			
		||||
      alertStore?.show("注册成功!请登录", "success", 2000);
 | 
			
		||||
      
 | 
			
		||||
 | 
			
		||||
      // 延迟后返回登录页面
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        backToLogin();
 | 
			
		||||
@@ -271,7 +277,7 @@ const checkExistingToken = async () => {
 | 
			
		||||
    const isValid = await AuthManager.verifyToken();
 | 
			
		||||
    if (isValid) {
 | 
			
		||||
      // 如果token仍然有效,直接跳转到project页面
 | 
			
		||||
      await router.push("/project");
 | 
			
		||||
      router.go(-1);
 | 
			
		||||
    }
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    // token无效或验证失败,继续显示登录页面
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										0
									
								
								src/views/Exam/ExamCard.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/views/Exam/ExamCard.vue
									
									
									
									
									
										Normal file
									
								
							@@ -1,14 +1,13 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div v-if="show" class="modal modal-open overflow-hidden">
 | 
			
		||||
  <div v-if="isShowModal" class="modal modal-open overflow-hidden">
 | 
			
		||||
    <div class="modal-box w-full max-w-7xl max-h-[90vh] p-0 overflow-hidden">
 | 
			
		||||
      <div
 | 
			
		||||
        class="flex justify-between items-center p-6 border-b border-base-300"
 | 
			
		||||
      >
 | 
			
		||||
        <h2 class="text-2xl font-bold text-base-content">创建新实验</h2>
 | 
			
		||||
        <button
 | 
			
		||||
          @click="closeCreateModal"
 | 
			
		||||
          class="btn btn-sm btn-circle btn-ghost"
 | 
			
		||||
        >
 | 
			
		||||
        <h2 class="text-2xl font-bold text-base-content">
 | 
			
		||||
          {{ mode === "create" ? "新建实验" : "编辑实验" }}
 | 
			
		||||
        </h2>
 | 
			
		||||
        <button @click="close" class="btn btn-sm btn-circle btn-ghost">
 | 
			
		||||
          <svg
 | 
			
		||||
            class="w-6 h-6"
 | 
			
		||||
            fill="none"
 | 
			
		||||
@@ -40,7 +39,7 @@
 | 
			
		||||
              </label>
 | 
			
		||||
              <input
 | 
			
		||||
                type="text"
 | 
			
		||||
                v-model="newExam.id"
 | 
			
		||||
                v-model="editExamInfo.id"
 | 
			
		||||
                class="input input-bordered w-full"
 | 
			
		||||
                placeholder="例如: EXP001"
 | 
			
		||||
                required
 | 
			
		||||
@@ -54,7 +53,7 @@
 | 
			
		||||
              </label>
 | 
			
		||||
              <input
 | 
			
		||||
                type="text"
 | 
			
		||||
                v-model="newExam.name"
 | 
			
		||||
                v-model="editExamInfo.name"
 | 
			
		||||
                class="input input-bordered w-full"
 | 
			
		||||
                placeholder="实验名称"
 | 
			
		||||
                required
 | 
			
		||||
@@ -67,7 +66,7 @@
 | 
			
		||||
                <span class="label-text font-medium">实验描述 *</span>
 | 
			
		||||
              </label>
 | 
			
		||||
              <textarea
 | 
			
		||||
                v-model="newExam.description"
 | 
			
		||||
                v-model="editExamInfo.description"
 | 
			
		||||
                class="textarea textarea-bordered w-full h-32"
 | 
			
		||||
                placeholder="详细描述实验内容、目标和要求..."
 | 
			
		||||
                required
 | 
			
		||||
@@ -78,7 +77,7 @@
 | 
			
		||||
            <div class="form-control">
 | 
			
		||||
              <div class="flex flex-wrap gap-2 mb-3 min-h-[2rem]">
 | 
			
		||||
                <span
 | 
			
		||||
                  v-for="(tag, index) in newExam.tags"
 | 
			
		||||
                  v-for="(tag, index) in editExamInfo.tags"
 | 
			
		||||
                  :key="index"
 | 
			
		||||
                  class="badge badge-primary gap-2"
 | 
			
		||||
                >
 | 
			
		||||
@@ -126,12 +125,12 @@
 | 
			
		||||
                      :key="i"
 | 
			
		||||
                      type="radio"
 | 
			
		||||
                      :value="i"
 | 
			
		||||
                      v-model="newExam.difficulty"
 | 
			
		||||
                      v-model="editExamInfo.difficulty"
 | 
			
		||||
                      class="mask mask-star-2 bg-orange-400"
 | 
			
		||||
                    />
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <span class="text-lg font-medium text-base-content"
 | 
			
		||||
                    >({{ newExam.difficulty }}/5)</span
 | 
			
		||||
                    >({{ editExamInfo.difficulty }}/5)</span
 | 
			
		||||
                  >
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
@@ -143,7 +142,7 @@
 | 
			
		||||
                <label class="label cursor-pointer justify-start gap-4">
 | 
			
		||||
                  <input
 | 
			
		||||
                    type="checkbox"
 | 
			
		||||
                    v-model="newExam.isVisibleToUsers"
 | 
			
		||||
                    v-model="editExamInfo.isVisibleToUsers"
 | 
			
		||||
                    class="checkbox checkbox-primary"
 | 
			
		||||
                  />
 | 
			
		||||
                  <div>
 | 
			
		||||
@@ -161,14 +160,22 @@
 | 
			
		||||
              <div class="space-y-3">
 | 
			
		||||
                <button
 | 
			
		||||
                  type="submit"
 | 
			
		||||
                  :disabled="isCreating || !canCreateExam"
 | 
			
		||||
                  :disabled="isUpdating || !canCreateExam"
 | 
			
		||||
                  class="btn btn-primary w-full"
 | 
			
		||||
                >
 | 
			
		||||
                  <span
 | 
			
		||||
                    v-if="isCreating"
 | 
			
		||||
                    v-if="isUpdating"
 | 
			
		||||
                    class="loading loading-spinner loading-sm mr-2"
 | 
			
		||||
                  ></span>
 | 
			
		||||
                  {{ isCreating ? "创建中..." : "创建实验" }}
 | 
			
		||||
                  {{
 | 
			
		||||
                    mode === "create"
 | 
			
		||||
                      ? isUpdating
 | 
			
		||||
                        ? "创建中..."
 | 
			
		||||
                        : "创建实验"
 | 
			
		||||
                      : isUpdating
 | 
			
		||||
                        ? "更新中..."
 | 
			
		||||
                        : "更新实验"
 | 
			
		||||
                  }}
 | 
			
		||||
                </button>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
@@ -194,44 +201,22 @@
 | 
			
		||||
                  @click="mdFileInput?.click()"
 | 
			
		||||
                  @dragover.prevent
 | 
			
		||||
                  @dragenter.prevent
 | 
			
		||||
                  @drop.prevent="handleMdFileDrop"
 | 
			
		||||
                  @drop.prevent="(e) => handleFileDrop(e, 'md')"
 | 
			
		||||
                >
 | 
			
		||||
                  <div
 | 
			
		||||
                    v-if="!uploadFiles.mdFile"
 | 
			
		||||
                    class="flex flex-col items-center gap-3"
 | 
			
		||||
                  >
 | 
			
		||||
                    <svg
 | 
			
		||||
                      class="w-12 h-12 text-base-content/40"
 | 
			
		||||
                      fill="none"
 | 
			
		||||
                      stroke="currentColor"
 | 
			
		||||
                      viewBox="0 0 24 24"
 | 
			
		||||
                    >
 | 
			
		||||
                      <path
 | 
			
		||||
                        stroke-linecap="round"
 | 
			
		||||
                        stroke-linejoin="round"
 | 
			
		||||
                        stroke-width="2"
 | 
			
		||||
                        d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
 | 
			
		||||
                      />
 | 
			
		||||
                    </svg>
 | 
			
		||||
                    <FileTextIcon
 | 
			
		||||
                      class="w-12 h-12 text-base-content opacity-40"
 | 
			
		||||
                    />
 | 
			
		||||
                    <div class="text-sm text-base-content/70 text-center">
 | 
			
		||||
                      <div class="font-medium mb-1">点击或拖拽上传</div>
 | 
			
		||||
                      <div class="text-xs">支持 .md 文件</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div v-else class="flex flex-col items-center gap-2">
 | 
			
		||||
                    <svg
 | 
			
		||||
                      class="w-8 h-8 text-success"
 | 
			
		||||
                      fill="none"
 | 
			
		||||
                      stroke="currentColor"
 | 
			
		||||
                      viewBox="0 0 24 24"
 | 
			
		||||
                    >
 | 
			
		||||
                      <path
 | 
			
		||||
                        stroke-linecap="round"
 | 
			
		||||
                        stroke-linejoin="round"
 | 
			
		||||
                        stroke-width="2"
 | 
			
		||||
                        d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
 | 
			
		||||
                      />
 | 
			
		||||
                    </svg>
 | 
			
		||||
                    <FileTextIcon class="w-8 h-8 text-success" />
 | 
			
		||||
                    <div class="text-xs font-medium text-success text-center">
 | 
			
		||||
                      {{ uploadFiles.mdFile.name }}
 | 
			
		||||
                    </div>
 | 
			
		||||
@@ -241,7 +226,7 @@
 | 
			
		||||
                <input
 | 
			
		||||
                  type="file"
 | 
			
		||||
                  ref="mdFileInput"
 | 
			
		||||
                  @change="handleMdFileChange"
 | 
			
		||||
                  @change="(e) => handleFileChange(e, 'md')"
 | 
			
		||||
                  accept=".md"
 | 
			
		||||
                  class="hidden"
 | 
			
		||||
                />
 | 
			
		||||
@@ -257,44 +242,20 @@
 | 
			
		||||
                  @click="imageFilesInput?.click()"
 | 
			
		||||
                  @dragover.prevent
 | 
			
		||||
                  @dragenter.prevent
 | 
			
		||||
                  @drop.prevent="handleImageFilesDrop"
 | 
			
		||||
                  @drop.prevent="(e) => handleFileDrop(e, 'image')"
 | 
			
		||||
                >
 | 
			
		||||
                  <div
 | 
			
		||||
                    v-if="uploadFiles.imageFiles.length === 0"
 | 
			
		||||
                    class="flex flex-col items-center gap-3"
 | 
			
		||||
                  >
 | 
			
		||||
                    <svg
 | 
			
		||||
                      class="w-12 h-12 text-base-content/40"
 | 
			
		||||
                      fill="none"
 | 
			
		||||
                      stroke="currentColor"
 | 
			
		||||
                      viewBox="0 0 24 24"
 | 
			
		||||
                    >
 | 
			
		||||
                      <path
 | 
			
		||||
                        stroke-linecap="round"
 | 
			
		||||
                        stroke-linejoin="round"
 | 
			
		||||
                        stroke-width="2"
 | 
			
		||||
                        d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
 | 
			
		||||
                      />
 | 
			
		||||
                    </svg>
 | 
			
		||||
                    <ImageIcon class="w-12 h-12 text-base-content opacity-40" />
 | 
			
		||||
                    <div class="text-sm text-base-content/70 text-center">
 | 
			
		||||
                      <div class="font-medium mb-1">点击或拖拽上传</div>
 | 
			
		||||
                      <div class="text-xs">支持 PNG, JPG, GIF 等图片格式</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div v-else class="flex flex-col items-center gap-2">
 | 
			
		||||
                    <svg
 | 
			
		||||
                      class="w-8 h-8 text-success"
 | 
			
		||||
                      fill="none"
 | 
			
		||||
                      stroke="currentColor"
 | 
			
		||||
                      viewBox="0 0 24 24"
 | 
			
		||||
                    >
 | 
			
		||||
                      <path
 | 
			
		||||
                        stroke-linecap="round"
 | 
			
		||||
                        stroke-linejoin="round"
 | 
			
		||||
                        stroke-width="2"
 | 
			
		||||
                        d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
 | 
			
		||||
                      />
 | 
			
		||||
                    </svg>
 | 
			
		||||
                    <ImageIcon class="w-8 h-8 text-success" />
 | 
			
		||||
                    <div class="text-xs font-medium text-success">
 | 
			
		||||
                      {{ uploadFiles.imageFiles.length }} 个文件
 | 
			
		||||
                    </div>
 | 
			
		||||
@@ -304,7 +265,7 @@
 | 
			
		||||
                <input
 | 
			
		||||
                  type="file"
 | 
			
		||||
                  ref="imageFilesInput"
 | 
			
		||||
                  @change="handleImageFilesChange"
 | 
			
		||||
                  @change="(e) => handleFileChange(e, 'image')"
 | 
			
		||||
                  accept="image/*"
 | 
			
		||||
                  multiple
 | 
			
		||||
                  class="hidden"
 | 
			
		||||
@@ -324,44 +285,22 @@
 | 
			
		||||
                  @click="bitstreamFilesInput?.click()"
 | 
			
		||||
                  @dragover.prevent
 | 
			
		||||
                  @dragenter.prevent
 | 
			
		||||
                  @drop.prevent="handleBitstreamFilesDrop"
 | 
			
		||||
                  @drop.prevent="(e) => handleFileDrop(e, 'bitstream')"
 | 
			
		||||
                >
 | 
			
		||||
                  <div
 | 
			
		||||
                    v-if="uploadFiles.bitstreamFiles.length === 0"
 | 
			
		||||
                    class="flex flex-col items-center gap-3"
 | 
			
		||||
                  >
 | 
			
		||||
                    <svg
 | 
			
		||||
                      class="w-12 h-12 text-base-content/40"
 | 
			
		||||
                      fill="none"
 | 
			
		||||
                      stroke="currentColor"
 | 
			
		||||
                      viewBox="0 0 24 24"
 | 
			
		||||
                    >
 | 
			
		||||
                      <path
 | 
			
		||||
                        stroke-linecap="round"
 | 
			
		||||
                        stroke-linejoin="round"
 | 
			
		||||
                        stroke-width="2"
 | 
			
		||||
                        d="M13 10V3L4 14h7v7l9-11h-7z"
 | 
			
		||||
                      />
 | 
			
		||||
                    </svg>
 | 
			
		||||
                    <BinaryIcon
 | 
			
		||||
                      class="w-12 h-12 text-base-content opacity-40"
 | 
			
		||||
                    />
 | 
			
		||||
                    <div class="text-sm text-base-content/70 text-center">
 | 
			
		||||
                      <div class="font-medium mb-1">点击或拖拽上传</div>
 | 
			
		||||
                      <div class="text-xs">支持 .sbit, .bit, .bin 文件</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div v-else class="flex flex-col items-center gap-2">
 | 
			
		||||
                    <svg
 | 
			
		||||
                      class="w-8 h-8 text-success"
 | 
			
		||||
                      fill="none"
 | 
			
		||||
                      stroke="currentColor"
 | 
			
		||||
                      viewBox="0 0 24 24"
 | 
			
		||||
                    >
 | 
			
		||||
                      <path
 | 
			
		||||
                        stroke-linecap="round"
 | 
			
		||||
                        stroke-linejoin="round"
 | 
			
		||||
                        stroke-width="2"
 | 
			
		||||
                        d="M13 10V3L4 14h7v7l9-11h-7z"
 | 
			
		||||
                      />
 | 
			
		||||
                    </svg>
 | 
			
		||||
                    <BinaryIcon class="w-8 h-8 text-success" />
 | 
			
		||||
                    <div class="text-xs font-medium text-success">
 | 
			
		||||
                      {{ uploadFiles.bitstreamFiles.length }} 个文件
 | 
			
		||||
                    </div>
 | 
			
		||||
@@ -371,7 +310,7 @@
 | 
			
		||||
                <input
 | 
			
		||||
                  type="file"
 | 
			
		||||
                  ref="bitstreamFilesInput"
 | 
			
		||||
                  @change="handleBitstreamFilesChange"
 | 
			
		||||
                  @change="(e) => handleFileChange(e, 'bitstream')"
 | 
			
		||||
                  accept=".sbit,.bit,.bin"
 | 
			
		||||
                  multiple
 | 
			
		||||
                  class="hidden"
 | 
			
		||||
@@ -388,44 +327,22 @@
 | 
			
		||||
                  @click="canvasFilesInput?.click()"
 | 
			
		||||
                  @dragover.prevent
 | 
			
		||||
                  @dragenter.prevent
 | 
			
		||||
                  @drop.prevent="handleCanvasFilesDrop"
 | 
			
		||||
                  @drop.prevent="(e) => handleFileDrop(e, 'canvas')"
 | 
			
		||||
                >
 | 
			
		||||
                  <div
 | 
			
		||||
                    v-if="uploadFiles.canvasFiles.length === 0"
 | 
			
		||||
                    class="flex flex-col items-center gap-3"
 | 
			
		||||
                  >
 | 
			
		||||
                    <svg
 | 
			
		||||
                      class="w-12 h-12 text-base-content/40"
 | 
			
		||||
                      fill="none"
 | 
			
		||||
                      stroke="currentColor"
 | 
			
		||||
                      viewBox="0 0 24 24"
 | 
			
		||||
                    >
 | 
			
		||||
                      <path
 | 
			
		||||
                        stroke-linecap="round"
 | 
			
		||||
                        stroke-linejoin="round"
 | 
			
		||||
                        stroke-width="2"
 | 
			
		||||
                        d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z"
 | 
			
		||||
                      />
 | 
			
		||||
                    </svg>
 | 
			
		||||
                    <FileJsonIcon
 | 
			
		||||
                      class="w-12 h-12 text-base-content opacity-40"
 | 
			
		||||
                    />
 | 
			
		||||
                    <div class="text-sm text-base-content/70 text-center">
 | 
			
		||||
                      <div class="font-medium mb-1">点击或拖拽上传</div>
 | 
			
		||||
                      <div class="text-xs">支持 .json 文件</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div v-else class="flex flex-col items-center gap-2">
 | 
			
		||||
                    <svg
 | 
			
		||||
                      class="w-8 h-8 text-success"
 | 
			
		||||
                      fill="none"
 | 
			
		||||
                      stroke="currentColor"
 | 
			
		||||
                      viewBox="0 0 24 24"
 | 
			
		||||
                    >
 | 
			
		||||
                      <path
 | 
			
		||||
                        stroke-linecap="round"
 | 
			
		||||
                        stroke-linejoin="round"
 | 
			
		||||
                        stroke-width="2"
 | 
			
		||||
                        d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z"
 | 
			
		||||
                      />
 | 
			
		||||
                    </svg>
 | 
			
		||||
                    <FileJsonIcon class="w-8 h-8 text-success" />
 | 
			
		||||
                    <div class="text-xs font-medium text-success">
 | 
			
		||||
                      {{ uploadFiles.canvasFiles.length }} 个文件
 | 
			
		||||
                    </div>
 | 
			
		||||
@@ -435,7 +352,7 @@
 | 
			
		||||
                <input
 | 
			
		||||
                  type="file"
 | 
			
		||||
                  ref="canvasFilesInput"
 | 
			
		||||
                  @change="handleCanvasFilesChange"
 | 
			
		||||
                  @change="(e) => handleFileChange(e, 'canvas')"
 | 
			
		||||
                  accept=".json"
 | 
			
		||||
                  multiple
 | 
			
		||||
                  class="hidden"
 | 
			
		||||
@@ -454,44 +371,22 @@
 | 
			
		||||
                  @click="resourceFileInput?.click()"
 | 
			
		||||
                  @dragover.prevent
 | 
			
		||||
                  @dragenter.prevent
 | 
			
		||||
                  @drop.prevent="handleResourceFileDrop"
 | 
			
		||||
                  @drop.prevent="(e) => handleFileDrop(e, 'resource')"
 | 
			
		||||
                >
 | 
			
		||||
                  <div
 | 
			
		||||
                    v-if="!uploadFiles.resourceFile"
 | 
			
		||||
                    class="flex flex-col items-center gap-3"
 | 
			
		||||
                  >
 | 
			
		||||
                    <svg
 | 
			
		||||
                      class="w-12 h-12 text-base-content/40"
 | 
			
		||||
                      fill="none"
 | 
			
		||||
                      stroke="currentColor"
 | 
			
		||||
                      viewBox="0 0 24 24"
 | 
			
		||||
                    >
 | 
			
		||||
                      <path
 | 
			
		||||
                        stroke-linecap="round"
 | 
			
		||||
                        stroke-linejoin="round"
 | 
			
		||||
                        stroke-width="2"
 | 
			
		||||
                        d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
 | 
			
		||||
                      />
 | 
			
		||||
                    </svg>
 | 
			
		||||
                    <FileArchiveIcon
 | 
			
		||||
                      class="w-12 h-12 text-base-content opacity-40"
 | 
			
		||||
                    />
 | 
			
		||||
                    <div class="text-sm text-base-content/70 text-center">
 | 
			
		||||
                      <div class="font-medium mb-1">点击或拖拽上传</div>
 | 
			
		||||
                      <div class="text-xs">支持 .zip, .rar, .7z 文件</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div v-else class="flex flex-col items-center gap-2">
 | 
			
		||||
                    <svg
 | 
			
		||||
                      class="w-8 h-8 text-success"
 | 
			
		||||
                      fill="none"
 | 
			
		||||
                      stroke="currentColor"
 | 
			
		||||
                      viewBox="0 0 24 24"
 | 
			
		||||
                    >
 | 
			
		||||
                      <path
 | 
			
		||||
                        stroke-linecap="round"
 | 
			
		||||
                        stroke-linejoin="round"
 | 
			
		||||
                        stroke-width="2"
 | 
			
		||||
                        d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
 | 
			
		||||
                      />
 | 
			
		||||
                    </svg>
 | 
			
		||||
                    <FileArchiveIcon class="w-8 h-8 text-success" />
 | 
			
		||||
                    <div class="text-xs font-medium text-success text-center">
 | 
			
		||||
                      {{ uploadFiles.resourceFile.name }}
 | 
			
		||||
                    </div>
 | 
			
		||||
@@ -501,7 +396,7 @@
 | 
			
		||||
                <input
 | 
			
		||||
                  type="file"
 | 
			
		||||
                  ref="resourceFileInput"
 | 
			
		||||
                  @change="handleResourceFileChange"
 | 
			
		||||
                  @change="(e) => handleFileChange(e, 'resource')"
 | 
			
		||||
                  accept=".zip,.rar,.7z"
 | 
			
		||||
                  class="hidden"
 | 
			
		||||
                />
 | 
			
		||||
@@ -511,28 +406,39 @@
 | 
			
		||||
        </div>
 | 
			
		||||
      </form>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="modal-backdrop" @click="closeCreateModal"></div>
 | 
			
		||||
    <div class="modal-backdrop" @click="close"></div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { CreateExamRequest, type FileParameter } from "@/APIClient";
 | 
			
		||||
import {
 | 
			
		||||
  FileTextIcon,
 | 
			
		||||
  ImageIcon,
 | 
			
		||||
  BinaryIcon,
 | 
			
		||||
  FileArchiveIcon,
 | 
			
		||||
  FileJsonIcon,
 | 
			
		||||
} from "lucide-vue-next";
 | 
			
		||||
import { ExamDto, type FileParameter } from "@/APIClient";
 | 
			
		||||
import { useAlertStore } from "@/components/Alert";
 | 
			
		||||
import { AuthManager } from "@/utils/AuthManager";
 | 
			
		||||
import { useRequiredInjection } from "@/utils/Common";
 | 
			
		||||
import { defineModel, ref, computed } from "vue";
 | 
			
		||||
import { mod } from "mathjs";
 | 
			
		||||
import type { ExamInfo } from "@/APIClient";
 | 
			
		||||
 | 
			
		||||
const show = defineModel<boolean>("show", {
 | 
			
		||||
type Mode = "create" | "edit";
 | 
			
		||||
 | 
			
		||||
const isShowModal = defineModel<boolean>("isShowModal", {
 | 
			
		||||
  default: false,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emits = defineEmits<{
 | 
			
		||||
  createFinished: [examId: string];
 | 
			
		||||
  editFinished: [examId: string];
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const alertStore = useRequiredInjection(useAlertStore);
 | 
			
		||||
const alert = useRequiredInjection(useAlertStore);
 | 
			
		||||
 | 
			
		||||
const newExam = ref({
 | 
			
		||||
const editExamInfo = ref({
 | 
			
		||||
  id: "",
 | 
			
		||||
  name: "",
 | 
			
		||||
  description: "",
 | 
			
		||||
@@ -541,7 +447,8 @@ const newExam = ref({
 | 
			
		||||
  isVisibleToUsers: true,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const isCreating = ref(false);
 | 
			
		||||
const isUpdating = ref(false);
 | 
			
		||||
const mode = ref<Mode>("create");
 | 
			
		||||
const newTagInput = ref("");
 | 
			
		||||
 | 
			
		||||
// 文件上传相关
 | 
			
		||||
@@ -563,89 +470,86 @@ const resourceFileInput = ref<HTMLInputElement>();
 | 
			
		||||
// 计算属性
 | 
			
		||||
const canCreateExam = computed(() => {
 | 
			
		||||
  return (
 | 
			
		||||
    newExam.value.id.trim() !== "" &&
 | 
			
		||||
    newExam.value.name.trim() !== "" &&
 | 
			
		||||
    newExam.value.description.trim() !== "" &&
 | 
			
		||||
    editExamInfo.value.id.trim() !== "" &&
 | 
			
		||||
    editExamInfo.value.name.trim() !== "" &&
 | 
			
		||||
    editExamInfo.value.description.trim() !== "" &&
 | 
			
		||||
    uploadFiles.value.mdFile !== null
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const handleResourceFileChange = (event: Event) => {
 | 
			
		||||
// 文件类型定义
 | 
			
		||||
type FileType = "md" | "image" | "bitstream" | "canvas" | "resource";
 | 
			
		||||
 | 
			
		||||
// 统一文件处理方法
 | 
			
		||||
const handleFileChange = (event: Event, fileType: FileType) => {
 | 
			
		||||
  const target = event.target as HTMLInputElement;
 | 
			
		||||
  if (target.files && target.files.length > 0) {
 | 
			
		||||
    uploadFiles.value.resourceFile = target.files[0];
 | 
			
		||||
  if (!target.files) return;
 | 
			
		||||
 | 
			
		||||
  switch (fileType) {
 | 
			
		||||
    case "md":
 | 
			
		||||
      if (target.files.length > 0) {
 | 
			
		||||
        uploadFiles.value.mdFile = target.files[0];
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    case "image":
 | 
			
		||||
      uploadFiles.value.imageFiles = Array.from(target.files);
 | 
			
		||||
      break;
 | 
			
		||||
    case "bitstream":
 | 
			
		||||
      uploadFiles.value.bitstreamFiles = Array.from(target.files);
 | 
			
		||||
      break;
 | 
			
		||||
    case "canvas":
 | 
			
		||||
      uploadFiles.value.canvasFiles = Array.from(target.files);
 | 
			
		||||
      break;
 | 
			
		||||
    case "resource":
 | 
			
		||||
      if (target.files.length > 0) {
 | 
			
		||||
        uploadFiles.value.resourceFile = target.files[0];
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 文件处理方法
 | 
			
		||||
const handleMdFileChange = (event: Event) => {
 | 
			
		||||
  const target = event.target as HTMLInputElement;
 | 
			
		||||
  if (target.files && target.files.length > 0) {
 | 
			
		||||
    uploadFiles.value.mdFile = target.files[0];
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleMdFileDrop = (event: DragEvent) => {
 | 
			
		||||
const handleFileDrop = (event: DragEvent, fileType: FileType) => {
 | 
			
		||||
  const files = event.dataTransfer?.files;
 | 
			
		||||
  if (files && files.length > 0) {
 | 
			
		||||
    const file = files[0];
 | 
			
		||||
    if (file.name.endsWith(".md")) {
 | 
			
		||||
      uploadFiles.value.mdFile = file;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
  if (!files || files.length === 0) return;
 | 
			
		||||
 | 
			
		||||
const handleImageFilesChange = (event: Event) => {
 | 
			
		||||
  const target = event.target as HTMLInputElement;
 | 
			
		||||
  if (target.files) {
 | 
			
		||||
    uploadFiles.value.imageFiles = Array.from(target.files);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleImageFilesDrop = (event: DragEvent) => {
 | 
			
		||||
  const files = event.dataTransfer?.files;
 | 
			
		||||
  if (files && files.length > 0) {
 | 
			
		||||
    const imageFiles = Array.from(files).filter((file) =>
 | 
			
		||||
      file.type.startsWith("image/"),
 | 
			
		||||
    );
 | 
			
		||||
    uploadFiles.value.imageFiles = imageFiles;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleBitstreamFilesChange = (event: Event) => {
 | 
			
		||||
  const target = event.target as HTMLInputElement;
 | 
			
		||||
  if (target.files) {
 | 
			
		||||
    uploadFiles.value.bitstreamFiles = Array.from(target.files);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleBitstreamFilesDrop = (event: DragEvent) => {
 | 
			
		||||
  const files = event.dataTransfer?.files;
 | 
			
		||||
  if (files && files.length > 0) {
 | 
			
		||||
    const bitstreamFiles = Array.from(files).filter(
 | 
			
		||||
      (file) =>
 | 
			
		||||
        file.name.endsWith(".sbit") ||
 | 
			
		||||
        file.name.endsWith(".bit") ||
 | 
			
		||||
        file.name.endsWith(".bin"),
 | 
			
		||||
    );
 | 
			
		||||
    uploadFiles.value.bitstreamFiles = bitstreamFiles;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleCanvasFilesChange = (event: Event) => {
 | 
			
		||||
  const target = event.target as HTMLInputElement;
 | 
			
		||||
  if (target.files) {
 | 
			
		||||
    uploadFiles.value.canvasFiles = Array.from(target.files);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleCanvasFilesDrop = (event: DragEvent) => {
 | 
			
		||||
  const files = event.dataTransfer?.files;
 | 
			
		||||
  if (files && files.length > 0) {
 | 
			
		||||
    const canvasFiles = Array.from(files).filter((file) =>
 | 
			
		||||
      file.name.endsWith(".json"),
 | 
			
		||||
    );
 | 
			
		||||
    uploadFiles.value.canvasFiles = canvasFiles;
 | 
			
		||||
  switch (fileType) {
 | 
			
		||||
    case "md":
 | 
			
		||||
      const mdFile = files[0];
 | 
			
		||||
      if (mdFile.name.endsWith(".md")) {
 | 
			
		||||
        uploadFiles.value.mdFile = mdFile;
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    case "image":
 | 
			
		||||
      const imageFiles = Array.from(files).filter((file) =>
 | 
			
		||||
        file.type.startsWith("image/"),
 | 
			
		||||
      );
 | 
			
		||||
      uploadFiles.value.imageFiles = imageFiles;
 | 
			
		||||
      break;
 | 
			
		||||
    case "bitstream":
 | 
			
		||||
      const bitstreamFiles = Array.from(files).filter(
 | 
			
		||||
        (file) =>
 | 
			
		||||
          file.name.endsWith(".sbit") ||
 | 
			
		||||
          file.name.endsWith(".bit") ||
 | 
			
		||||
          file.name.endsWith(".bin"),
 | 
			
		||||
      );
 | 
			
		||||
      uploadFiles.value.bitstreamFiles = bitstreamFiles;
 | 
			
		||||
      break;
 | 
			
		||||
    case "canvas":
 | 
			
		||||
      const canvasFiles = Array.from(files).filter((file) =>
 | 
			
		||||
        file.name.endsWith(".json"),
 | 
			
		||||
      );
 | 
			
		||||
      uploadFiles.value.canvasFiles = canvasFiles;
 | 
			
		||||
      break;
 | 
			
		||||
    case "resource":
 | 
			
		||||
      const resourceFile = files[0];
 | 
			
		||||
      if (
 | 
			
		||||
        resourceFile.name.endsWith(".zip") ||
 | 
			
		||||
        resourceFile.name.endsWith(".rar") ||
 | 
			
		||||
        resourceFile.name.endsWith(".7z")
 | 
			
		||||
      ) {
 | 
			
		||||
        uploadFiles.value.resourceFile = resourceFile;
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -656,18 +560,18 @@ const addTag = (event?: Event) => {
 | 
			
		||||
    event.stopPropagation();
 | 
			
		||||
  }
 | 
			
		||||
  const tag = newTagInput.value.trim();
 | 
			
		||||
  if (tag && !newExam.value.tags.includes(tag)) {
 | 
			
		||||
    newExam.value.tags.push(tag);
 | 
			
		||||
  if (tag && !editExamInfo.value.tags.includes(tag)) {
 | 
			
		||||
    editExamInfo.value.tags.push(tag);
 | 
			
		||||
    newTagInput.value = "";
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const removeTag = (index: number) => {
 | 
			
		||||
  newExam.value.tags.splice(index, 1);
 | 
			
		||||
  editExamInfo.value.tags.splice(index, 1);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const resetCreateForm = () => {
 | 
			
		||||
  newExam.value = {
 | 
			
		||||
  editExamInfo.value = {
 | 
			
		||||
    id: "",
 | 
			
		||||
    name: "",
 | 
			
		||||
    description: "",
 | 
			
		||||
@@ -692,75 +596,81 @@ const resetCreateForm = () => {
 | 
			
		||||
  if (resourceFileInput.value) resourceFileInput.value.value = "";
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const closeCreateModal = () => {
 | 
			
		||||
  show.value = false;
 | 
			
		||||
  resetCreateForm();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleResourceFileDrop = (event: DragEvent) => {
 | 
			
		||||
  const files = event.dataTransfer?.files;
 | 
			
		||||
  if (files && files.length > 0) {
 | 
			
		||||
    const file = files[0];
 | 
			
		||||
    if (
 | 
			
		||||
      file.name.endsWith(".zip") ||
 | 
			
		||||
      file.name.endsWith(".rar") ||
 | 
			
		||||
      file.name.endsWith(".7z")
 | 
			
		||||
    ) {
 | 
			
		||||
      uploadFiles.value.resourceFile = file;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 提交创建实验
 | 
			
		||||
const submitCreateExam = async () => {
 | 
			
		||||
  if (isCreating.value) return;
 | 
			
		||||
  if (isUpdating.value) return;
 | 
			
		||||
 | 
			
		||||
  // 验证必填字段
 | 
			
		||||
  if (!newExam.value.id || !newExam.value.name || !newExam.value.description) {
 | 
			
		||||
    alertStore?.error("请填写所有必填字段");
 | 
			
		||||
  if (
 | 
			
		||||
    !editExamInfo.value.id ||
 | 
			
		||||
    !editExamInfo.value.name ||
 | 
			
		||||
    !editExamInfo.value.description
 | 
			
		||||
  ) {
 | 
			
		||||
    alert?.error("请填写所有必填字段");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!uploadFiles.value.mdFile) {
 | 
			
		||||
    alertStore?.error("请上传MD文档");
 | 
			
		||||
    alert.error("请上传MD文档");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isCreating.value = true;
 | 
			
		||||
  isUpdating.value = true;
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    const client = AuthManager.createAuthenticatedExamClient();
 | 
			
		||||
 | 
			
		||||
    // 创建实验请求
 | 
			
		||||
    const createRequest = new CreateExamRequest({
 | 
			
		||||
      id: newExam.value.id,
 | 
			
		||||
      name: newExam.value.name,
 | 
			
		||||
      description: newExam.value.description,
 | 
			
		||||
      tags: newExam.value.tags,
 | 
			
		||||
      difficulty: newExam.value.difficulty,
 | 
			
		||||
      isVisibleToUsers: newExam.value.isVisibleToUsers,
 | 
			
		||||
    });
 | 
			
		||||
    let exam: ExamInfo;
 | 
			
		||||
    if (mode.value === "create") {
 | 
			
		||||
      // 创建实验请求
 | 
			
		||||
      const createRequest = new ExamDto({
 | 
			
		||||
        id: editExamInfo.value.id,
 | 
			
		||||
        name: editExamInfo.value.name,
 | 
			
		||||
        description: editExamInfo.value.description,
 | 
			
		||||
        tags: editExamInfo.value.tags,
 | 
			
		||||
        difficulty: editExamInfo.value.difficulty,
 | 
			
		||||
        isVisibleToUsers: editExamInfo.value.isVisibleToUsers,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
    // 创建实验
 | 
			
		||||
    const createdExam = await client.createExam(createRequest);
 | 
			
		||||
    console.log("实验创建成功:", createdExam);
 | 
			
		||||
      // 创建实验
 | 
			
		||||
      exam = await client.createExam(createRequest);
 | 
			
		||||
      console.log("实验创建成功:", exam);
 | 
			
		||||
    } else if (mode.value === "edit") {
 | 
			
		||||
      // 编辑实验请求
 | 
			
		||||
      const editRequest = new ExamDto({
 | 
			
		||||
        id: editExamInfo.value.id,
 | 
			
		||||
        name: editExamInfo.value.name,
 | 
			
		||||
        description: editExamInfo.value.description,
 | 
			
		||||
        tags: editExamInfo.value.tags,
 | 
			
		||||
        difficulty: editExamInfo.value.difficulty,
 | 
			
		||||
        isVisibleToUsers: editExamInfo.value.isVisibleToUsers,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      // 编辑实验
 | 
			
		||||
      exam = await client.updateExam(editRequest);
 | 
			
		||||
      console.log("实验编辑成功:", exam);
 | 
			
		||||
    } else {
 | 
			
		||||
      // 处理其他模式
 | 
			
		||||
      console.error("未知的模式:", mode.value);
 | 
			
		||||
      throw new Error("未知的模式");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 上传文件
 | 
			
		||||
    await uploadExamResources(createdExam.id);
 | 
			
		||||
    await uploadExamResources(exam.id);
 | 
			
		||||
 | 
			
		||||
    alertStore?.success("实验创建成功");
 | 
			
		||||
    closeCreateModal();
 | 
			
		||||
    emits("createFinished", createdExam.id);
 | 
			
		||||
    alert.success("实验创建成功");
 | 
			
		||||
    close();
 | 
			
		||||
    emits("editFinished", exam.id);
 | 
			
		||||
  } catch (err: any) {
 | 
			
		||||
    console.error("创建实验失败:", err);
 | 
			
		||||
    alertStore?.error(err.message || "创建实验失败");
 | 
			
		||||
    alert.error(err.message || "创建实验失败");
 | 
			
		||||
  } finally {
 | 
			
		||||
    isCreating.value = false;
 | 
			
		||||
    isUpdating.value = false;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 上传实验资源
 | 
			
		||||
const uploadExamResources = async (examId: string) => {
 | 
			
		||||
async function uploadExamResources(examId: string) {
 | 
			
		||||
  const client = AuthManager.createAuthenticatedResourceClient();
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
@@ -825,9 +735,42 @@ const uploadExamResources = async (examId: string) => {
 | 
			
		||||
    }
 | 
			
		||||
  } catch (err: any) {
 | 
			
		||||
    console.error("资源上传失败:", err);
 | 
			
		||||
    alertStore?.error("部分资源上传失败: " + (err.message || "未知错误"));
 | 
			
		||||
    alert?.error("部分资源上传失败: " + (err.message || "未知错误"));
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function show() {
 | 
			
		||||
  isShowModal.value = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function close() {
 | 
			
		||||
  isShowModal.value = false;
 | 
			
		||||
  mode.value = "create";
 | 
			
		||||
  resetCreateForm();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function editExam(examId: string) {
 | 
			
		||||
  const client = AuthManager.createAuthenticatedExamClient();
 | 
			
		||||
  const examInfo = await client.getExam(examId);
 | 
			
		||||
 | 
			
		||||
  editExamInfo.value = {
 | 
			
		||||
    id: examInfo.id,
 | 
			
		||||
    name: examInfo.name,
 | 
			
		||||
    description: examInfo.description,
 | 
			
		||||
    tags: examInfo.tags,
 | 
			
		||||
    difficulty: examInfo.difficulty,
 | 
			
		||||
    isVisibleToUsers: examInfo.isVisibleToUsers,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  mode.value = "edit";
 | 
			
		||||
  show();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
defineExpose({
 | 
			
		||||
  show,
 | 
			
		||||
  close,
 | 
			
		||||
  editExam,
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="postcss" scoped></style>
 | 
			
		||||
 
 | 
			
		||||
@@ -62,7 +62,7 @@
 | 
			
		||||
          <div
 | 
			
		||||
            v-if="isAdmin"
 | 
			
		||||
            class="card bg-base-200 shadow-lg hover:shadow-xl transition-all duration-200 cursor-pointer hover:scale-[1.02]"
 | 
			
		||||
            @click="showCreateModal = true"
 | 
			
		||||
            @click="() => examEditModalRef?.show()"
 | 
			
		||||
          >
 | 
			
		||||
            <div class="card-body flex items-center justify-center text-center">
 | 
			
		||||
              <div class="text-primary text-6xl mb-4">+</div>
 | 
			
		||||
@@ -75,15 +75,26 @@
 | 
			
		||||
            v-for="exam in exams"
 | 
			
		||||
            :key="exam.id"
 | 
			
		||||
            class="card bg-base-200 shadow-lg hover:shadow-xl transition-all duration-200 cursor-pointer hover:scale-[1.02] relative overflow-hidden"
 | 
			
		||||
            @click="viewExam(exam.id)"
 | 
			
		||||
            @click="handleCardClicked($event, exam.id)"
 | 
			
		||||
          >
 | 
			
		||||
            <div class="card-body">
 | 
			
		||||
              <div class="flex justify-between items-start mb-4">
 | 
			
		||||
                <h3 class="card-title text-base-content">{{ exam.name }}</h3>
 | 
			
		||||
                <span
 | 
			
		||||
                  class="card-title text-sm text-blue-600/50 dark:text-sky-400/50"
 | 
			
		||||
                  >{{ exam.id }}</span
 | 
			
		||||
                >
 | 
			
		||||
                <div class="flex flex-row items-center gap-2">
 | 
			
		||||
                  <button
 | 
			
		||||
                    class="btn btn-ghost text-error hover:underline group"
 | 
			
		||||
                    @click="handleEditExamClicked($event, exam.id)"
 | 
			
		||||
                  >
 | 
			
		||||
                    <EditIcon
 | 
			
		||||
                      class="w-4 h-4 transition-transform duration-200 group-hover:scale-110"
 | 
			
		||||
                    />
 | 
			
		||||
                    编辑
 | 
			
		||||
                  </button>
 | 
			
		||||
                  <span
 | 
			
		||||
                    class="card-title text-sm text-blue-600/50 dark:text-sky-400/50"
 | 
			
		||||
                    >{{ exam.id }}</span
 | 
			
		||||
                  >
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
 | 
			
		||||
              <!-- 实验标签 -->
 | 
			
		||||
@@ -160,8 +171,8 @@
 | 
			
		||||
 | 
			
		||||
    <!-- 创建实验模态框 -->
 | 
			
		||||
    <ExamEditModal
 | 
			
		||||
      v-model:show="showCreateModal"
 | 
			
		||||
      @create-finished="handleCreateExamFinished"
 | 
			
		||||
      ref="examEditModalRef"
 | 
			
		||||
      @edit-finished="handleEditExamFinished"
 | 
			
		||||
    />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -170,36 +181,27 @@
 | 
			
		||||
import { ref, onMounted, computed } from "vue";
 | 
			
		||||
import { useRoute } from "vue-router";
 | 
			
		||||
import { AuthManager } from "@/utils/AuthManager";
 | 
			
		||||
import { type ExamSummary, type ExamInfo } from "@/APIClient";
 | 
			
		||||
import { type ExamInfo } from "@/APIClient";
 | 
			
		||||
import { formatDate } from "@/utils/Common";
 | 
			
		||||
import ExamInfoModal from "./ExamInfoModal.vue";
 | 
			
		||||
import ExamEditModal from "./ExamEditModal.vue";
 | 
			
		||||
import router from "@/router";
 | 
			
		||||
import { EditIcon } from "lucide-vue-next";
 | 
			
		||||
import { templateRef } from "@vueuse/core";
 | 
			
		||||
 | 
			
		||||
// 响应式数据
 | 
			
		||||
const route = useRoute();
 | 
			
		||||
const exams = ref<ExamSummary[]>([]);
 | 
			
		||||
const exams = ref<ExamInfo[]>([]);
 | 
			
		||||
const selectedExam = ref<ExamInfo | null>(null);
 | 
			
		||||
const loading = ref(false);
 | 
			
		||||
const error = ref<string>("");
 | 
			
		||||
const isAdmin = ref(false);
 | 
			
		||||
 | 
			
		||||
// Modal
 | 
			
		||||
const showCreateModal = ref(false);
 | 
			
		||||
const examEditModalRef = templateRef("examEditModalRef");
 | 
			
		||||
const showInfoModal = ref(false);
 | 
			
		||||
 | 
			
		||||
// 方法
 | 
			
		||||
const checkAdminStatus = async () => {
 | 
			
		||||
  console.log("检查管理员权限...");
 | 
			
		||||
  try {
 | 
			
		||||
    isAdmin.value = await AuthManager.verifyAdminAuth();
 | 
			
		||||
    console.log("管理员权限:", isAdmin.value);
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
    console.warn("无法验证管理员权限:", err);
 | 
			
		||||
    isAdmin.value = false;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const refreshExams = async () => {
 | 
			
		||||
async function refreshExams() {
 | 
			
		||||
  loading.value = true;
 | 
			
		||||
  error.value = "";
 | 
			
		||||
 | 
			
		||||
@@ -212,9 +214,9 @@ const refreshExams = async () => {
 | 
			
		||||
  } finally {
 | 
			
		||||
    loading.value = false;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const viewExam = async (examId: string) => {
 | 
			
		||||
async function viewExam(examId: string) {
 | 
			
		||||
  try {
 | 
			
		||||
    const client = AuthManager.createAuthenticatedExamClient();
 | 
			
		||||
    selectedExam.value = await client.getExam(examId);
 | 
			
		||||
@@ -222,16 +224,32 @@ const viewExam = async (examId: string) => {
 | 
			
		||||
  } catch (err: any) {
 | 
			
		||||
    error.value = err.message || "获取实验详情失败";
 | 
			
		||||
    console.error("获取实验详情失败:", err);
 | 
			
		||||
    showInfoModal.value = false;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function handleCreateExamFinished() {
 | 
			
		||||
async function handleEditExamFinished() {
 | 
			
		||||
  await refreshExams();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function handleCardClicked(event: MouseEvent, examId: string) {
 | 
			
		||||
  if (event.target instanceof HTMLButtonElement) return;
 | 
			
		||||
  await viewExam(examId);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function handleEditExamClicked(event: MouseEvent, examId: string) {
 | 
			
		||||
  examEditModalRef?.value?.editExam(examId);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 生命周期
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
  await checkAdminStatus();
 | 
			
		||||
  const isAuthenticated = await AuthManager.isAuthenticated();
 | 
			
		||||
  if (!isAuthenticated) {
 | 
			
		||||
    router.push("/login");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isAdmin.value = await AuthManager.verifyAdminAuth();
 | 
			
		||||
 | 
			
		||||
  await refreshExams();
 | 
			
		||||
 | 
			
		||||
  // 处理路由参数,如果有examId则自动打开该实验的详情模态框
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user