feat: 实现可编辑已有的实验
This commit is contained in:
parent
76342553ad
commit
7a59c29e06
|
@ -28,7 +28,7 @@ public class ExamController : ControllerBase
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[HttpGet("list")]
|
[HttpGet("list")]
|
||||||
[EnableCors("Users")]
|
[EnableCors("Users")]
|
||||||
[ProducesResponseType(typeof(ExamSummary[]), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(ExamInfo[]), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
public IActionResult GetExamList()
|
public IActionResult GetExamList()
|
||||||
|
@ -37,19 +37,10 @@ public class ExamController : ControllerBase
|
||||||
{
|
{
|
||||||
var exams = _examManager.GetAllExams();
|
var exams = _examManager.GetAllExams();
|
||||||
|
|
||||||
var examSummaries = exams.Select(exam => new ExamSummary
|
var examInfos = exams.Select(exam => new ExamInfo(exam)).ToArray();
|
||||||
{
|
|
||||||
ID = exam.ID,
|
|
||||||
Name = exam.Name,
|
|
||||||
CreatedTime = exam.CreatedTime,
|
|
||||||
UpdatedTime = exam.UpdatedTime,
|
|
||||||
Tags = exam.GetTagsList(),
|
|
||||||
Difficulty = exam.Difficulty,
|
|
||||||
IsVisibleToUsers = exam.IsVisibleToUsers
|
|
||||||
}).ToArray();
|
|
||||||
|
|
||||||
logger.Info($"成功获取实验列表,共 {examSummaries.Length} 个实验");
|
logger.Info($"成功获取实验列表,共 {examInfos.Length} 个实验");
|
||||||
return Ok(examSummaries);
|
return Ok(examInfos);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@ -93,17 +84,7 @@ public class ExamController : ControllerBase
|
||||||
}
|
}
|
||||||
|
|
||||||
var exam = result.Value.Value;
|
var exam = result.Value.Value;
|
||||||
var examInfo = new ExamInfo
|
var examInfo = new ExamInfo(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
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.Info($"成功获取实验信息: {examId}");
|
logger.Info($"成功获取实验信息: {examId}");
|
||||||
return Ok(examInfo);
|
return Ok(examInfo);
|
||||||
|
@ -121,7 +102,7 @@ public class ExamController : ControllerBase
|
||||||
/// <param name="request">创建实验请求</param>
|
/// <param name="request">创建实验请求</param>
|
||||||
/// <returns>创建结果</returns>
|
/// <returns>创建结果</returns>
|
||||||
[Authorize("Admin")]
|
[Authorize("Admin")]
|
||||||
[HttpPost]
|
[HttpPost("create")]
|
||||||
[EnableCors("Users")]
|
[EnableCors("Users")]
|
||||||
[ProducesResponseType(typeof(ExamInfo), StatusCodes.Status201Created)]
|
[ProducesResponseType(typeof(ExamInfo), StatusCodes.Status201Created)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
@ -129,7 +110,7 @@ public class ExamController : ControllerBase
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[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))
|
if (string.IsNullOrWhiteSpace(request.ID) || string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.Description))
|
||||||
return BadRequest("实验ID、名称和描述不能为空");
|
return BadRequest("实验ID、名称和描述不能为空");
|
||||||
|
@ -148,17 +129,7 @@ public class ExamController : ControllerBase
|
||||||
}
|
}
|
||||||
|
|
||||||
var exam = result.Value;
|
var exam = result.Value;
|
||||||
var examInfo = new ExamInfo
|
var examInfo = new ExamInfo(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
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.Info($"成功创建实验: {request.ID}");
|
logger.Info($"成功创建实验: {request.ID}");
|
||||||
return CreatedAtAction(nameof(GetExam), new { examId = request.ID }, examInfo);
|
return CreatedAtAction(nameof(GetExam), new { examId = request.ID }, examInfo);
|
||||||
|
@ -170,26 +141,97 @@ public class ExamController : ControllerBase
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新实验信息
|
||||||
|
/// </summary>
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
var examId = request.ID;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 首先检查实验是否存在
|
||||||
|
var existingExamResult = _examManager.GetExamByID(examId);
|
||||||
|
if (!existingExamResult.IsSuccessful)
|
||||||
|
{
|
||||||
|
logger.Error($"检查实验是否存在时出错: {existingExamResult.Error.Message}");
|
||||||
|
return StatusCode(StatusCodes.Status500InternalServerError, $"检查实验失败: {existingExamResult.Error.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existingExamResult.Value.HasValue)
|
||||||
|
{
|
||||||
|
logger.Warn($"要更新的实验不存在: {examId}");
|
||||||
|
return NotFound($"实验 {examId} 不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行更新
|
||||||
|
var updateResult = _examManager.UpdateExam(
|
||||||
|
examId,
|
||||||
|
request.Name,
|
||||||
|
request.Description,
|
||||||
|
request.Tags,
|
||||||
|
request.Difficulty,
|
||||||
|
request.IsVisibleToUsers
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!updateResult.IsSuccessful)
|
||||||
|
{
|
||||||
|
logger.Error($"更新实验时出错: {updateResult.Error.Message}");
|
||||||
|
return StatusCode(StatusCodes.Status500InternalServerError, $"更新实验失败: {updateResult.Error.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取更新后的实验信息并返回
|
||||||
|
var updatedExamResult = _examManager.GetExamByID(examId);
|
||||||
|
if (!updatedExamResult.IsSuccessful || !updatedExamResult.Value.HasValue)
|
||||||
|
{
|
||||||
|
logger.Error($"获取更新后的实验信息失败: {examId}");
|
||||||
|
return StatusCode(StatusCodes.Status500InternalServerError, "更新成功但获取更新后信息失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
var updatedExam = updatedExamResult.Value.Value;
|
||||||
|
var examInfo = new ExamInfo(updatedExam);
|
||||||
|
|
||||||
|
logger.Info($"成功更新实验: {examId},更新记录数: {updateResult.Value}");
|
||||||
|
return Ok(examInfo);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Error($"更新实验 {examId} 时出错: {ex.Message}");
|
||||||
|
return StatusCode(StatusCodes.Status500InternalServerError, $"更新实验失败: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 实验信息类
|
/// 实验信息
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ExamInfo
|
public class ExamInfo
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 实验的唯一标识符
|
/// 实验的唯一标识符
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required string ID { get; set; }
|
public string ID { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 实验名称
|
/// 实验名称
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 实验描述
|
/// 实验描述
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required string Description { get; set; }
|
public string Description { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 实验创建时间
|
/// 实验创建时间
|
||||||
|
@ -215,12 +257,24 @@ public class ExamController : ControllerBase
|
||||||
/// 普通用户是否可见
|
/// 普通用户是否可见
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsVisibleToUsers { get; set; } = true;
|
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>
|
||||||
/// 实验简要信息类(用于列表显示)
|
/// 统一的实验数据传输对象
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ExamSummary
|
public class ExamDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 实验的唯一标识符
|
/// 实验的唯一标识符
|
||||||
|
@ -232,47 +286,6 @@ public class ExamController : ControllerBase
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required string Name { get; set; }
|
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>
|
||||||
/// 实验描述
|
/// 实验描述
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -293,4 +306,3 @@ public class ExamController : ControllerBase
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsVisibleToUsers { get; set; } = true;
|
public bool IsVisibleToUsers { get; set; } = true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -165,7 +165,7 @@ public class ResourceManager
|
||||||
if (duplicateResource != null && duplicateResource.ResourceName == resourceName)
|
if (duplicateResource != null && duplicateResource.ResourceName == resourceName)
|
||||||
{
|
{
|
||||||
logger.Info($"资源已存在: {resourceName}");
|
logger.Info($"资源已存在: {resourceName}");
|
||||||
return new(new Exception($"资源已存在: {resourceName}"));
|
return duplicateResource;
|
||||||
}
|
}
|
||||||
|
|
||||||
var nowTime = DateTime.Now;
|
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);
|
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?";
|
let url_ = this.baseUrl + "/api/VideoStream/SetVideoStreamEnable?";
|
||||||
if (enable === null)
|
if (enable === null)
|
||||||
throw new Error("The parameter 'enable' cannot be null.");
|
throw new Error("The parameter 'enable' cannot be null.");
|
||||||
|
@ -308,11 +308,10 @@ export class VideoStreamClient {
|
||||||
url_ = url_.replace(/[?&]$/, "");
|
url_ = url_.replace(/[?&]$/, "");
|
||||||
|
|
||||||
let options_: AxiosRequestConfig = {
|
let options_: AxiosRequestConfig = {
|
||||||
responseType: "blob",
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: url_,
|
url: url_,
|
||||||
headers: {
|
headers: {
|
||||||
"Accept": "application/octet-stream"
|
"Accept": "application/json"
|
||||||
},
|
},
|
||||||
cancelToken
|
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;
|
const status = response.status;
|
||||||
let _headers: any = {};
|
let _headers: any = {};
|
||||||
if (response.headers && typeof response.headers === "object") {
|
if (response.headers && typeof response.headers === "object") {
|
||||||
|
@ -338,22 +337,27 @@ export class VideoStreamClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (status === 200 || status === 206) {
|
if (status === 200) {
|
||||||
const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined;
|
const _responseText = response.data;
|
||||||
let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined;
|
let result200: any = null;
|
||||||
let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined;
|
let resultData200 = _responseText;
|
||||||
if (fileName) {
|
result200 = resultData200 !== undefined ? resultData200 : <any>null;
|
||||||
fileName = decodeURIComponent(fileName);
|
|
||||||
} else {
|
return Promise.resolve<any>(result200);
|
||||||
fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined;
|
|
||||||
fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined;
|
} else if (status === 500) {
|
||||||
}
|
const _responseText = response.data;
|
||||||
return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers });
|
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) {
|
} else if (status !== 200 && status !== 204) {
|
||||||
const _responseText = response.data;
|
const _responseText = response.data;
|
||||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||||
}
|
}
|
||||||
return Promise.resolve<FileResponse | null>(null as any);
|
return Promise.resolve<any>(null as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2505,7 +2509,7 @@ export class ExamClient {
|
||||||
* 获取所有实验列表
|
* 获取所有实验列表
|
||||||
* @return 实验列表
|
* @return 实验列表
|
||||||
*/
|
*/
|
||||||
getExamList( cancelToken?: CancelToken): Promise<ExamSummary[]> {
|
getExamList( cancelToken?: CancelToken): Promise<ExamInfo[]> {
|
||||||
let url_ = this.baseUrl + "/api/Exam/list";
|
let url_ = this.baseUrl + "/api/Exam/list";
|
||||||
url_ = url_.replace(/[?&]$/, "");
|
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;
|
const status = response.status;
|
||||||
let _headers: any = {};
|
let _headers: any = {};
|
||||||
if (response.headers && typeof response.headers === "object") {
|
if (response.headers && typeof response.headers === "object") {
|
||||||
|
@ -2546,12 +2550,12 @@ export class ExamClient {
|
||||||
if (Array.isArray(resultData200)) {
|
if (Array.isArray(resultData200)) {
|
||||||
result200 = [] as any;
|
result200 = [] as any;
|
||||||
for (let item of resultData200)
|
for (let item of resultData200)
|
||||||
result200!.push(ExamSummary.fromJS(item));
|
result200!.push(ExamInfo.fromJS(item));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
result200 = <any>null;
|
result200 = <any>null;
|
||||||
}
|
}
|
||||||
return Promise.resolve<ExamSummary[]>(result200);
|
return Promise.resolve<ExamInfo[]>(result200);
|
||||||
|
|
||||||
} else if (status === 401) {
|
} else if (status === 401) {
|
||||||
const _responseText = response.data;
|
const _responseText = response.data;
|
||||||
|
@ -2568,7 +2572,7 @@ export class ExamClient {
|
||||||
const _responseText = response.data;
|
const _responseText = response.data;
|
||||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||||
}
|
}
|
||||||
return Promise.resolve<ExamSummary[]>(null as any);
|
return Promise.resolve<ExamInfo[]>(null as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2657,8 +2661,8 @@ export class ExamClient {
|
||||||
* @param request 创建实验请求
|
* @param request 创建实验请求
|
||||||
* @return 创建结果
|
* @return 创建结果
|
||||||
*/
|
*/
|
||||||
createExam(request: CreateExamRequest, cancelToken?: CancelToken): Promise<ExamInfo> {
|
createExam(request: ExamDto, cancelToken?: CancelToken): Promise<ExamInfo> {
|
||||||
let url_ = this.baseUrl + "/api/Exam";
|
let url_ = this.baseUrl + "/api/Exam/create";
|
||||||
url_ = url_.replace(/[?&]$/, "");
|
url_ = url_.replace(/[?&]$/, "");
|
||||||
|
|
||||||
const content_ = JSON.stringify(request);
|
const content_ = JSON.stringify(request);
|
||||||
|
@ -2740,6 +2744,95 @@ export class ExamClient {
|
||||||
}
|
}
|
||||||
return Promise.resolve<ExamInfo>(null as any);
|
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 {
|
export class HdmiVideoStreamClient {
|
||||||
|
@ -7802,94 +7895,7 @@ export interface IChannelCaptureData {
|
||||||
data: string;
|
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 {
|
export class ExamInfo implements IExamInfo {
|
||||||
/** 实验的唯一标识符 */
|
/** 实验的唯一标识符 */
|
||||||
id!: string;
|
id!: string;
|
||||||
|
@ -7962,7 +7968,7 @@ export class ExamInfo implements IExamInfo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 实验信息类 */
|
/** 实验信息 */
|
||||||
export interface IExamInfo {
|
export interface IExamInfo {
|
||||||
/** 实验的唯一标识符 */
|
/** 实验的唯一标识符 */
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -7982,9 +7988,9 @@ export interface IExamInfo {
|
||||||
isVisibleToUsers: boolean;
|
isVisibleToUsers: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 创建实验请求类 */
|
/** 统一的实验数据传输对象 */
|
||||||
export class CreateExamRequest implements ICreateExamRequest {
|
export class ExamDto implements IExamDto {
|
||||||
/** 实验ID */
|
/** 实验的唯一标识符 */
|
||||||
id!: string;
|
id!: string;
|
||||||
/** 实验名称 */
|
/** 实验名称 */
|
||||||
name!: string;
|
name!: string;
|
||||||
|
@ -7997,7 +8003,7 @@ export class CreateExamRequest implements ICreateExamRequest {
|
||||||
/** 普通用户是否可见 */
|
/** 普通用户是否可见 */
|
||||||
isVisibleToUsers!: boolean;
|
isVisibleToUsers!: boolean;
|
||||||
|
|
||||||
constructor(data?: ICreateExamRequest) {
|
constructor(data?: IExamDto) {
|
||||||
if (data) {
|
if (data) {
|
||||||
for (var property in data) {
|
for (var property in data) {
|
||||||
if (data.hasOwnProperty(property))
|
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 : {};
|
data = typeof data === 'object' ? data : {};
|
||||||
let result = new CreateExamRequest();
|
let result = new ExamDto();
|
||||||
result.init(data);
|
result.init(data);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -8047,9 +8053,9 @@ export class CreateExamRequest implements ICreateExamRequest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 创建实验请求类 */
|
/** 统一的实验数据传输对象 */
|
||||||
export interface ICreateExamRequest {
|
export interface IExamDto {
|
||||||
/** 实验ID */
|
/** 实验的唯一标识符 */
|
||||||
id: string;
|
id: string;
|
||||||
/** 实验名称 */
|
/** 实验名称 */
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
@ -2,7 +2,10 @@
|
||||||
<div class="flex items-center justify-center min-h-screen bg-base-200">
|
<div class="flex items-center justify-center min-h-screen bg-base-200">
|
||||||
<div class="relative w-full max-w-md">
|
<div class="relative w-full max-w-md">
|
||||||
<!-- Login Card -->
|
<!-- 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">
|
<div class="card-body">
|
||||||
<h1 class="card-title place-self-center my-3 text-2xl">用户登录</h1>
|
<h1 class="card-title place-self-center my-3 text-2xl">用户登录</h1>
|
||||||
<div class="flex flex-col w-full h-full">
|
<div class="flex flex-col w-full h-full">
|
||||||
|
@ -44,7 +47,10 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sign Up Card -->
|
<!-- 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">
|
<div class="card-body">
|
||||||
<h1 class="card-title place-self-center my-3 text-2xl">用户注册</h1>
|
<h1 class="card-title place-self-center my-3 text-2xl">用户注册</h1>
|
||||||
<div class="flex flex-col w-full h-full">
|
<div class="flex flex-col w-full h-full">
|
||||||
|
@ -122,7 +128,7 @@ const isSignUpLoading = ref(false);
|
||||||
const signUpData = ref({
|
const signUpData = ref({
|
||||||
username: "",
|
username: "",
|
||||||
email: "",
|
email: "",
|
||||||
password: ""
|
password: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
// 登录处理函数
|
// 登录处理函数
|
||||||
|
@ -149,7 +155,7 @@ const handleLogin = async () => {
|
||||||
|
|
||||||
// 短暂延迟后跳转到project页面
|
// 短暂延迟后跳转到project页面
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
await router.push("/project");
|
router.go(-1);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Login error:", error);
|
console.error("Login error:", error);
|
||||||
|
@ -180,7 +186,7 @@ const handleRegister = () => {
|
||||||
signUpData.value = {
|
signUpData.value = {
|
||||||
username: "",
|
username: "",
|
||||||
email: "",
|
email: "",
|
||||||
password: ""
|
password: "",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -227,7 +233,7 @@ const handleSignUp = async () => {
|
||||||
const result = await dataClient.signUpUser(
|
const result = await dataClient.signUpUser(
|
||||||
signUpData.value.username.trim(),
|
signUpData.value.username.trim(),
|
||||||
signUpData.value.email.trim(),
|
signUpData.value.email.trim(),
|
||||||
signUpData.value.password.trim()
|
signUpData.value.password.trim(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
|
@ -271,7 +277,7 @@ const checkExistingToken = async () => {
|
||||||
const isValid = await AuthManager.verifyToken();
|
const isValid = await AuthManager.verifyToken();
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
// 如果token仍然有效,直接跳转到project页面
|
// 如果token仍然有效,直接跳转到project页面
|
||||||
await router.push("/project");
|
router.go(-1);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// token无效或验证失败,继续显示登录页面
|
// token无效或验证失败,继续显示登录页面
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
<template>
|
<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="modal-box w-full max-w-7xl max-h-[90vh] p-0 overflow-hidden">
|
||||||
<div
|
<div
|
||||||
class="flex justify-between items-center p-6 border-b border-base-300"
|
class="flex justify-between items-center p-6 border-b border-base-300"
|
||||||
>
|
>
|
||||||
<h2 class="text-2xl font-bold text-base-content">创建新实验</h2>
|
<h2 class="text-2xl font-bold text-base-content">
|
||||||
<button
|
{{ mode === "create" ? "新建实验" : "编辑实验" }}
|
||||||
@click="closeCreateModal"
|
</h2>
|
||||||
class="btn btn-sm btn-circle btn-ghost"
|
<button @click="close" class="btn btn-sm btn-circle btn-ghost">
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
class="w-6 h-6"
|
class="w-6 h-6"
|
||||||
fill="none"
|
fill="none"
|
||||||
|
@ -40,7 +39,7 @@
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
v-model="newExam.id"
|
v-model="editExamInfo.id"
|
||||||
class="input input-bordered w-full"
|
class="input input-bordered w-full"
|
||||||
placeholder="例如: EXP001"
|
placeholder="例如: EXP001"
|
||||||
required
|
required
|
||||||
|
@ -54,7 +53,7 @@
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
v-model="newExam.name"
|
v-model="editExamInfo.name"
|
||||||
class="input input-bordered w-full"
|
class="input input-bordered w-full"
|
||||||
placeholder="实验名称"
|
placeholder="实验名称"
|
||||||
required
|
required
|
||||||
|
@ -67,7 +66,7 @@
|
||||||
<span class="label-text font-medium">实验描述 *</span>
|
<span class="label-text font-medium">实验描述 *</span>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
v-model="newExam.description"
|
v-model="editExamInfo.description"
|
||||||
class="textarea textarea-bordered w-full h-32"
|
class="textarea textarea-bordered w-full h-32"
|
||||||
placeholder="详细描述实验内容、目标和要求..."
|
placeholder="详细描述实验内容、目标和要求..."
|
||||||
required
|
required
|
||||||
|
@ -78,7 +77,7 @@
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<div class="flex flex-wrap gap-2 mb-3 min-h-[2rem]">
|
<div class="flex flex-wrap gap-2 mb-3 min-h-[2rem]">
|
||||||
<span
|
<span
|
||||||
v-for="(tag, index) in newExam.tags"
|
v-for="(tag, index) in editExamInfo.tags"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="badge badge-primary gap-2"
|
class="badge badge-primary gap-2"
|
||||||
>
|
>
|
||||||
|
@ -126,12 +125,12 @@
|
||||||
:key="i"
|
:key="i"
|
||||||
type="radio"
|
type="radio"
|
||||||
:value="i"
|
:value="i"
|
||||||
v-model="newExam.difficulty"
|
v-model="editExamInfo.difficulty"
|
||||||
class="mask mask-star-2 bg-orange-400"
|
class="mask mask-star-2 bg-orange-400"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-lg font-medium text-base-content"
|
<span class="text-lg font-medium text-base-content"
|
||||||
>({{ newExam.difficulty }}/5)</span
|
>({{ editExamInfo.difficulty }}/5)</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -143,7 +142,7 @@
|
||||||
<label class="label cursor-pointer justify-start gap-4">
|
<label class="label cursor-pointer justify-start gap-4">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
v-model="newExam.isVisibleToUsers"
|
v-model="editExamInfo.isVisibleToUsers"
|
||||||
class="checkbox checkbox-primary"
|
class="checkbox checkbox-primary"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
|
@ -161,14 +160,22 @@
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
:disabled="isCreating || !canCreateExam"
|
:disabled="isUpdating || !canCreateExam"
|
||||||
class="btn btn-primary w-full"
|
class="btn btn-primary w-full"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-if="isCreating"
|
v-if="isUpdating"
|
||||||
class="loading loading-spinner loading-sm mr-2"
|
class="loading loading-spinner loading-sm mr-2"
|
||||||
></span>
|
></span>
|
||||||
{{ isCreating ? "创建中..." : "创建实验" }}
|
{{
|
||||||
|
mode === "create"
|
||||||
|
? isUpdating
|
||||||
|
? "创建中..."
|
||||||
|
: "创建实验"
|
||||||
|
: isUpdating
|
||||||
|
? "更新中..."
|
||||||
|
: "更新实验"
|
||||||
|
}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -194,44 +201,22 @@
|
||||||
@click="mdFileInput?.click()"
|
@click="mdFileInput?.click()"
|
||||||
@dragover.prevent
|
@dragover.prevent
|
||||||
@dragenter.prevent
|
@dragenter.prevent
|
||||||
@drop.prevent="handleMdFileDrop"
|
@drop.prevent="(e) => handleFileDrop(e, 'md')"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="!uploadFiles.mdFile"
|
v-if="!uploadFiles.mdFile"
|
||||||
class="flex flex-col items-center gap-3"
|
class="flex flex-col items-center gap-3"
|
||||||
>
|
>
|
||||||
<svg
|
<FileTextIcon
|
||||||
class="w-12 h-12 text-base-content/40"
|
class="w-12 h-12 text-base-content opacity-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>
|
|
||||||
<div class="text-sm text-base-content/70 text-center">
|
<div class="text-sm text-base-content/70 text-center">
|
||||||
<div class="font-medium mb-1">点击或拖拽上传</div>
|
<div class="font-medium mb-1">点击或拖拽上传</div>
|
||||||
<div class="text-xs">支持 .md 文件</div>
|
<div class="text-xs">支持 .md 文件</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex flex-col items-center gap-2">
|
<div v-else class="flex flex-col items-center gap-2">
|
||||||
<svg
|
<FileTextIcon class="w-8 h-8 text-success" />
|
||||||
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>
|
|
||||||
<div class="text-xs font-medium text-success text-center">
|
<div class="text-xs font-medium text-success text-center">
|
||||||
{{ uploadFiles.mdFile.name }}
|
{{ uploadFiles.mdFile.name }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -241,7 +226,7 @@
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
ref="mdFileInput"
|
ref="mdFileInput"
|
||||||
@change="handleMdFileChange"
|
@change="(e) => handleFileChange(e, 'md')"
|
||||||
accept=".md"
|
accept=".md"
|
||||||
class="hidden"
|
class="hidden"
|
||||||
/>
|
/>
|
||||||
|
@ -257,44 +242,20 @@
|
||||||
@click="imageFilesInput?.click()"
|
@click="imageFilesInput?.click()"
|
||||||
@dragover.prevent
|
@dragover.prevent
|
||||||
@dragenter.prevent
|
@dragenter.prevent
|
||||||
@drop.prevent="handleImageFilesDrop"
|
@drop.prevent="(e) => handleFileDrop(e, 'image')"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="uploadFiles.imageFiles.length === 0"
|
v-if="uploadFiles.imageFiles.length === 0"
|
||||||
class="flex flex-col items-center gap-3"
|
class="flex flex-col items-center gap-3"
|
||||||
>
|
>
|
||||||
<svg
|
<ImageIcon class="w-12 h-12 text-base-content opacity-40" />
|
||||||
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>
|
|
||||||
<div class="text-sm text-base-content/70 text-center">
|
<div class="text-sm text-base-content/70 text-center">
|
||||||
<div class="font-medium mb-1">点击或拖拽上传</div>
|
<div class="font-medium mb-1">点击或拖拽上传</div>
|
||||||
<div class="text-xs">支持 PNG, JPG, GIF 等图片格式</div>
|
<div class="text-xs">支持 PNG, JPG, GIF 等图片格式</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex flex-col items-center gap-2">
|
<div v-else class="flex flex-col items-center gap-2">
|
||||||
<svg
|
<ImageIcon class="w-8 h-8 text-success" />
|
||||||
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>
|
|
||||||
<div class="text-xs font-medium text-success">
|
<div class="text-xs font-medium text-success">
|
||||||
{{ uploadFiles.imageFiles.length }} 个文件
|
{{ uploadFiles.imageFiles.length }} 个文件
|
||||||
</div>
|
</div>
|
||||||
|
@ -304,7 +265,7 @@
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
ref="imageFilesInput"
|
ref="imageFilesInput"
|
||||||
@change="handleImageFilesChange"
|
@change="(e) => handleFileChange(e, 'image')"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
multiple
|
multiple
|
||||||
class="hidden"
|
class="hidden"
|
||||||
|
@ -324,44 +285,22 @@
|
||||||
@click="bitstreamFilesInput?.click()"
|
@click="bitstreamFilesInput?.click()"
|
||||||
@dragover.prevent
|
@dragover.prevent
|
||||||
@dragenter.prevent
|
@dragenter.prevent
|
||||||
@drop.prevent="handleBitstreamFilesDrop"
|
@drop.prevent="(e) => handleFileDrop(e, 'bitstream')"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="uploadFiles.bitstreamFiles.length === 0"
|
v-if="uploadFiles.bitstreamFiles.length === 0"
|
||||||
class="flex flex-col items-center gap-3"
|
class="flex flex-col items-center gap-3"
|
||||||
>
|
>
|
||||||
<svg
|
<BinaryIcon
|
||||||
class="w-12 h-12 text-base-content/40"
|
class="w-12 h-12 text-base-content opacity-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>
|
|
||||||
<div class="text-sm text-base-content/70 text-center">
|
<div class="text-sm text-base-content/70 text-center">
|
||||||
<div class="font-medium mb-1">点击或拖拽上传</div>
|
<div class="font-medium mb-1">点击或拖拽上传</div>
|
||||||
<div class="text-xs">支持 .sbit, .bit, .bin 文件</div>
|
<div class="text-xs">支持 .sbit, .bit, .bin 文件</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex flex-col items-center gap-2">
|
<div v-else class="flex flex-col items-center gap-2">
|
||||||
<svg
|
<BinaryIcon class="w-8 h-8 text-success" />
|
||||||
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>
|
|
||||||
<div class="text-xs font-medium text-success">
|
<div class="text-xs font-medium text-success">
|
||||||
{{ uploadFiles.bitstreamFiles.length }} 个文件
|
{{ uploadFiles.bitstreamFiles.length }} 个文件
|
||||||
</div>
|
</div>
|
||||||
|
@ -371,7 +310,7 @@
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
ref="bitstreamFilesInput"
|
ref="bitstreamFilesInput"
|
||||||
@change="handleBitstreamFilesChange"
|
@change="(e) => handleFileChange(e, 'bitstream')"
|
||||||
accept=".sbit,.bit,.bin"
|
accept=".sbit,.bit,.bin"
|
||||||
multiple
|
multiple
|
||||||
class="hidden"
|
class="hidden"
|
||||||
|
@ -388,44 +327,22 @@
|
||||||
@click="canvasFilesInput?.click()"
|
@click="canvasFilesInput?.click()"
|
||||||
@dragover.prevent
|
@dragover.prevent
|
||||||
@dragenter.prevent
|
@dragenter.prevent
|
||||||
@drop.prevent="handleCanvasFilesDrop"
|
@drop.prevent="(e) => handleFileDrop(e, 'canvas')"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="uploadFiles.canvasFiles.length === 0"
|
v-if="uploadFiles.canvasFiles.length === 0"
|
||||||
class="flex flex-col items-center gap-3"
|
class="flex flex-col items-center gap-3"
|
||||||
>
|
>
|
||||||
<svg
|
<FileJsonIcon
|
||||||
class="w-12 h-12 text-base-content/40"
|
class="w-12 h-12 text-base-content opacity-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>
|
|
||||||
<div class="text-sm text-base-content/70 text-center">
|
<div class="text-sm text-base-content/70 text-center">
|
||||||
<div class="font-medium mb-1">点击或拖拽上传</div>
|
<div class="font-medium mb-1">点击或拖拽上传</div>
|
||||||
<div class="text-xs">支持 .json 文件</div>
|
<div class="text-xs">支持 .json 文件</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex flex-col items-center gap-2">
|
<div v-else class="flex flex-col items-center gap-2">
|
||||||
<svg
|
<FileJsonIcon class="w-8 h-8 text-success" />
|
||||||
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>
|
|
||||||
<div class="text-xs font-medium text-success">
|
<div class="text-xs font-medium text-success">
|
||||||
{{ uploadFiles.canvasFiles.length }} 个文件
|
{{ uploadFiles.canvasFiles.length }} 个文件
|
||||||
</div>
|
</div>
|
||||||
|
@ -435,7 +352,7 @@
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
ref="canvasFilesInput"
|
ref="canvasFilesInput"
|
||||||
@change="handleCanvasFilesChange"
|
@change="(e) => handleFileChange(e, 'canvas')"
|
||||||
accept=".json"
|
accept=".json"
|
||||||
multiple
|
multiple
|
||||||
class="hidden"
|
class="hidden"
|
||||||
|
@ -454,44 +371,22 @@
|
||||||
@click="resourceFileInput?.click()"
|
@click="resourceFileInput?.click()"
|
||||||
@dragover.prevent
|
@dragover.prevent
|
||||||
@dragenter.prevent
|
@dragenter.prevent
|
||||||
@drop.prevent="handleResourceFileDrop"
|
@drop.prevent="(e) => handleFileDrop(e, 'resource')"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="!uploadFiles.resourceFile"
|
v-if="!uploadFiles.resourceFile"
|
||||||
class="flex flex-col items-center gap-3"
|
class="flex flex-col items-center gap-3"
|
||||||
>
|
>
|
||||||
<svg
|
<FileArchiveIcon
|
||||||
class="w-12 h-12 text-base-content/40"
|
class="w-12 h-12 text-base-content opacity-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>
|
|
||||||
<div class="text-sm text-base-content/70 text-center">
|
<div class="text-sm text-base-content/70 text-center">
|
||||||
<div class="font-medium mb-1">点击或拖拽上传</div>
|
<div class="font-medium mb-1">点击或拖拽上传</div>
|
||||||
<div class="text-xs">支持 .zip, .rar, .7z 文件</div>
|
<div class="text-xs">支持 .zip, .rar, .7z 文件</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex flex-col items-center gap-2">
|
<div v-else class="flex flex-col items-center gap-2">
|
||||||
<svg
|
<FileArchiveIcon class="w-8 h-8 text-success" />
|
||||||
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>
|
|
||||||
<div class="text-xs font-medium text-success text-center">
|
<div class="text-xs font-medium text-success text-center">
|
||||||
{{ uploadFiles.resourceFile.name }}
|
{{ uploadFiles.resourceFile.name }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -501,7 +396,7 @@
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
ref="resourceFileInput"
|
ref="resourceFileInput"
|
||||||
@change="handleResourceFileChange"
|
@change="(e) => handleFileChange(e, 'resource')"
|
||||||
accept=".zip,.rar,.7z"
|
accept=".zip,.rar,.7z"
|
||||||
class="hidden"
|
class="hidden"
|
||||||
/>
|
/>
|
||||||
|
@ -511,28 +406,39 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-backdrop" @click="closeCreateModal"></div>
|
<div class="modal-backdrop" @click="close"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { useAlertStore } from "@/components/Alert";
|
||||||
import { AuthManager } from "@/utils/AuthManager";
|
import { AuthManager } from "@/utils/AuthManager";
|
||||||
import { useRequiredInjection } from "@/utils/Common";
|
import { useRequiredInjection } from "@/utils/Common";
|
||||||
import { defineModel, ref, computed } from "vue";
|
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,
|
default: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emits = defineEmits<{
|
const emits = defineEmits<{
|
||||||
createFinished: [examId: string];
|
editFinished: [examId: string];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const alertStore = useRequiredInjection(useAlertStore);
|
const alert = useRequiredInjection(useAlertStore);
|
||||||
|
|
||||||
const newExam = ref({
|
const editExamInfo = ref({
|
||||||
id: "",
|
id: "",
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
|
@ -541,7 +447,8 @@ const newExam = ref({
|
||||||
isVisibleToUsers: true,
|
isVisibleToUsers: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isCreating = ref(false);
|
const isUpdating = ref(false);
|
||||||
|
const mode = ref<Mode>("create");
|
||||||
const newTagInput = ref("");
|
const newTagInput = ref("");
|
||||||
|
|
||||||
// 文件上传相关
|
// 文件上传相关
|
||||||
|
@ -563,65 +470,62 @@ const resourceFileInput = ref<HTMLInputElement>();
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const canCreateExam = computed(() => {
|
const canCreateExam = computed(() => {
|
||||||
return (
|
return (
|
||||||
newExam.value.id.trim() !== "" &&
|
editExamInfo.value.id.trim() !== "" &&
|
||||||
newExam.value.name.trim() !== "" &&
|
editExamInfo.value.name.trim() !== "" &&
|
||||||
newExam.value.description.trim() !== "" &&
|
editExamInfo.value.description.trim() !== "" &&
|
||||||
uploadFiles.value.mdFile !== null
|
uploadFiles.value.mdFile !== null
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleResourceFileChange = (event: Event) => {
|
// 文件类型定义
|
||||||
const target = event.target as HTMLInputElement;
|
type FileType = "md" | "image" | "bitstream" | "canvas" | "resource";
|
||||||
if (target.files && target.files.length > 0) {
|
|
||||||
uploadFiles.value.resourceFile = target.files[0];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 文件处理方法
|
// 统一文件处理方法
|
||||||
const handleMdFileChange = (event: Event) => {
|
const handleFileChange = (event: Event, fileType: FileType) => {
|
||||||
const target = event.target as HTMLInputElement;
|
const target = event.target as HTMLInputElement;
|
||||||
if (target.files && target.files.length > 0) {
|
if (!target.files) return;
|
||||||
|
|
||||||
|
switch (fileType) {
|
||||||
|
case "md":
|
||||||
|
if (target.files.length > 0) {
|
||||||
uploadFiles.value.mdFile = target.files[0];
|
uploadFiles.value.mdFile = target.files[0];
|
||||||
}
|
}
|
||||||
};
|
break;
|
||||||
|
case "image":
|
||||||
const handleMdFileDrop = (event: DragEvent) => {
|
|
||||||
const files = event.dataTransfer?.files;
|
|
||||||
if (files && files.length > 0) {
|
|
||||||
const file = files[0];
|
|
||||||
if (file.name.endsWith(".md")) {
|
|
||||||
uploadFiles.value.mdFile = file;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleImageFilesChange = (event: Event) => {
|
|
||||||
const target = event.target as HTMLInputElement;
|
|
||||||
if (target.files) {
|
|
||||||
uploadFiles.value.imageFiles = Array.from(target.files);
|
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 handleImageFilesDrop = (event: DragEvent) => {
|
const handleFileDrop = (event: DragEvent, fileType: FileType) => {
|
||||||
const files = event.dataTransfer?.files;
|
const files = event.dataTransfer?.files;
|
||||||
if (files && files.length > 0) {
|
if (!files || files.length === 0) return;
|
||||||
|
|
||||||
|
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) =>
|
const imageFiles = Array.from(files).filter((file) =>
|
||||||
file.type.startsWith("image/"),
|
file.type.startsWith("image/"),
|
||||||
);
|
);
|
||||||
uploadFiles.value.imageFiles = imageFiles;
|
uploadFiles.value.imageFiles = imageFiles;
|
||||||
}
|
break;
|
||||||
};
|
case "bitstream":
|
||||||
|
|
||||||
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(
|
const bitstreamFiles = Array.from(files).filter(
|
||||||
(file) =>
|
(file) =>
|
||||||
file.name.endsWith(".sbit") ||
|
file.name.endsWith(".sbit") ||
|
||||||
|
@ -629,23 +533,23 @@ const handleBitstreamFilesDrop = (event: DragEvent) => {
|
||||||
file.name.endsWith(".bin"),
|
file.name.endsWith(".bin"),
|
||||||
);
|
);
|
||||||
uploadFiles.value.bitstreamFiles = bitstreamFiles;
|
uploadFiles.value.bitstreamFiles = bitstreamFiles;
|
||||||
}
|
break;
|
||||||
};
|
case "canvas":
|
||||||
|
|
||||||
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) =>
|
const canvasFiles = Array.from(files).filter((file) =>
|
||||||
file.name.endsWith(".json"),
|
file.name.endsWith(".json"),
|
||||||
);
|
);
|
||||||
uploadFiles.value.canvasFiles = canvasFiles;
|
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();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
const tag = newTagInput.value.trim();
|
const tag = newTagInput.value.trim();
|
||||||
if (tag && !newExam.value.tags.includes(tag)) {
|
if (tag && !editExamInfo.value.tags.includes(tag)) {
|
||||||
newExam.value.tags.push(tag);
|
editExamInfo.value.tags.push(tag);
|
||||||
newTagInput.value = "";
|
newTagInput.value = "";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeTag = (index: number) => {
|
const removeTag = (index: number) => {
|
||||||
newExam.value.tags.splice(index, 1);
|
editExamInfo.value.tags.splice(index, 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetCreateForm = () => {
|
const resetCreateForm = () => {
|
||||||
newExam.value = {
|
editExamInfo.value = {
|
||||||
id: "",
|
id: "",
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
|
@ -692,75 +596,81 @@ const resetCreateForm = () => {
|
||||||
if (resourceFileInput.value) resourceFileInput.value.value = "";
|
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 () => {
|
const submitCreateExam = async () => {
|
||||||
if (isCreating.value) return;
|
if (isUpdating.value) return;
|
||||||
|
|
||||||
// 验证必填字段
|
// 验证必填字段
|
||||||
if (!newExam.value.id || !newExam.value.name || !newExam.value.description) {
|
if (
|
||||||
alertStore?.error("请填写所有必填字段");
|
!editExamInfo.value.id ||
|
||||||
|
!editExamInfo.value.name ||
|
||||||
|
!editExamInfo.value.description
|
||||||
|
) {
|
||||||
|
alert?.error("请填写所有必填字段");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!uploadFiles.value.mdFile) {
|
if (!uploadFiles.value.mdFile) {
|
||||||
alertStore?.error("请上传MD文档");
|
alert.error("请上传MD文档");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
isCreating.value = true;
|
isUpdating.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = AuthManager.createAuthenticatedExamClient();
|
const client = AuthManager.createAuthenticatedExamClient();
|
||||||
|
|
||||||
|
let exam: ExamInfo;
|
||||||
|
if (mode.value === "create") {
|
||||||
// 创建实验请求
|
// 创建实验请求
|
||||||
const createRequest = new CreateExamRequest({
|
const createRequest = new ExamDto({
|
||||||
id: newExam.value.id,
|
id: editExamInfo.value.id,
|
||||||
name: newExam.value.name,
|
name: editExamInfo.value.name,
|
||||||
description: newExam.value.description,
|
description: editExamInfo.value.description,
|
||||||
tags: newExam.value.tags,
|
tags: editExamInfo.value.tags,
|
||||||
difficulty: newExam.value.difficulty,
|
difficulty: editExamInfo.value.difficulty,
|
||||||
isVisibleToUsers: newExam.value.isVisibleToUsers,
|
isVisibleToUsers: editExamInfo.value.isVisibleToUsers,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 创建实验
|
// 创建实验
|
||||||
const createdExam = await client.createExam(createRequest);
|
exam = await client.createExam(createRequest);
|
||||||
console.log("实验创建成功:", createdExam);
|
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("实验创建成功");
|
alert.success("实验创建成功");
|
||||||
closeCreateModal();
|
close();
|
||||||
emits("createFinished", createdExam.id);
|
emits("editFinished", exam.id);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("创建实验失败:", err);
|
console.error("创建实验失败:", err);
|
||||||
alertStore?.error(err.message || "创建实验失败");
|
alert.error(err.message || "创建实验失败");
|
||||||
} finally {
|
} finally {
|
||||||
isCreating.value = false;
|
isUpdating.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 上传实验资源
|
// 上传实验资源
|
||||||
const uploadExamResources = async (examId: string) => {
|
async function uploadExamResources(examId: string) {
|
||||||
const client = AuthManager.createAuthenticatedResourceClient();
|
const client = AuthManager.createAuthenticatedResourceClient();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -825,9 +735,42 @@ const uploadExamResources = async (examId: string) => {
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("资源上传失败:", err);
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="postcss" scoped></style>
|
<style lang="postcss" scoped></style>
|
||||||
|
|
|
@ -62,7 +62,7 @@
|
||||||
<div
|
<div
|
||||||
v-if="isAdmin"
|
v-if="isAdmin"
|
||||||
class="card bg-base-200 shadow-lg hover:shadow-xl transition-all duration-200 cursor-pointer hover:scale-[1.02]"
|
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="card-body flex items-center justify-center text-center">
|
||||||
<div class="text-primary text-6xl mb-4">+</div>
|
<div class="text-primary text-6xl mb-4">+</div>
|
||||||
|
@ -75,16 +75,27 @@
|
||||||
v-for="exam in exams"
|
v-for="exam in exams"
|
||||||
:key="exam.id"
|
: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"
|
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="card-body">
|
||||||
<div class="flex justify-between items-start mb-4">
|
<div class="flex justify-between items-start mb-4">
|
||||||
<h3 class="card-title text-base-content">{{ exam.name }}</h3>
|
<h3 class="card-title text-base-content">{{ exam.name }}</h3>
|
||||||
|
<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
|
<span
|
||||||
class="card-title text-sm text-blue-600/50 dark:text-sky-400/50"
|
class="card-title text-sm text-blue-600/50 dark:text-sky-400/50"
|
||||||
>{{ exam.id }}</span
|
>{{ exam.id }}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 实验标签 -->
|
<!-- 实验标签 -->
|
||||||
<div
|
<div
|
||||||
|
@ -160,8 +171,8 @@
|
||||||
|
|
||||||
<!-- 创建实验模态框 -->
|
<!-- 创建实验模态框 -->
|
||||||
<ExamEditModal
|
<ExamEditModal
|
||||||
v-model:show="showCreateModal"
|
ref="examEditModalRef"
|
||||||
@create-finished="handleCreateExamFinished"
|
@edit-finished="handleEditExamFinished"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -170,36 +181,27 @@
|
||||||
import { ref, onMounted, computed } from "vue";
|
import { ref, onMounted, computed } from "vue";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import { AuthManager } from "@/utils/AuthManager";
|
import { AuthManager } from "@/utils/AuthManager";
|
||||||
import { type ExamSummary, type ExamInfo } from "@/APIClient";
|
import { type ExamInfo } from "@/APIClient";
|
||||||
import { formatDate } from "@/utils/Common";
|
import { formatDate } from "@/utils/Common";
|
||||||
import ExamInfoModal from "./ExamInfoModal.vue";
|
import ExamInfoModal from "./ExamInfoModal.vue";
|
||||||
import ExamEditModal from "./ExamEditModal.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 route = useRoute();
|
||||||
const exams = ref<ExamSummary[]>([]);
|
const exams = ref<ExamInfo[]>([]);
|
||||||
const selectedExam = ref<ExamInfo | null>(null);
|
const selectedExam = ref<ExamInfo | null>(null);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const error = ref<string>("");
|
const error = ref<string>("");
|
||||||
const isAdmin = ref(false);
|
const isAdmin = ref(false);
|
||||||
|
|
||||||
// Modal
|
// Modal
|
||||||
const showCreateModal = ref(false);
|
const examEditModalRef = templateRef("examEditModalRef");
|
||||||
const showInfoModal = ref(false);
|
const showInfoModal = ref(false);
|
||||||
|
|
||||||
// 方法
|
async function refreshExams() {
|
||||||
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 () => {
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
error.value = "";
|
error.value = "";
|
||||||
|
|
||||||
|
@ -212,9 +214,9 @@ const refreshExams = async () => {
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const viewExam = async (examId: string) => {
|
async function viewExam(examId: string) {
|
||||||
try {
|
try {
|
||||||
const client = AuthManager.createAuthenticatedExamClient();
|
const client = AuthManager.createAuthenticatedExamClient();
|
||||||
selectedExam.value = await client.getExam(examId);
|
selectedExam.value = await client.getExam(examId);
|
||||||
|
@ -222,16 +224,32 @@ const viewExam = async (examId: string) => {
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
error.value = err.message || "获取实验详情失败";
|
error.value = err.message || "获取实验详情失败";
|
||||||
console.error("获取实验详情失败:", err);
|
console.error("获取实验详情失败:", err);
|
||||||
|
showInfoModal.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
async function handleCreateExamFinished() {
|
async function handleEditExamFinished() {
|
||||||
await refreshExams();
|
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 () => {
|
onMounted(async () => {
|
||||||
await checkAdminStatus();
|
const isAuthenticated = await AuthManager.isAuthenticated();
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
router.push("/login");
|
||||||
|
}
|
||||||
|
|
||||||
|
isAdmin.value = await AuthManager.verifyAdminAuth();
|
||||||
|
|
||||||
await refreshExams();
|
await refreshExams();
|
||||||
|
|
||||||
// 处理路由参数,如果有examId则自动打开该实验的详情模态框
|
// 处理路由参数,如果有examId则自动打开该实验的详情模态框
|
||||||
|
|
Loading…
Reference in New Issue