Compare commits
10 Commits
34ab5abbb0
...
c59762a392
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c59762a392 | ||
![]() |
017cc2169c | ||
![]() |
a21ca80782 | ||
![]() |
14fbe6397a | ||
![]() |
262e7d6396 | ||
![]() |
f9ff57ff72 | ||
![]() |
0ee411bf50 | ||
![]() |
681c0862b6 | ||
![]() |
d20c051c51 | ||
![]() |
f37262d72e |
8
EmailLib/Dockerfile
Normal file
8
EmailLib/Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# ./EmailLib/Dockerfile
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS eamillib
|
||||||
|
WORKDIR /src
|
||||||
|
COPY ../EmailLib/*.csproj ./EmailLib/
|
||||||
|
RUN dotnet restore "EmailLib/EmailLib.csproj"
|
||||||
|
|
||||||
|
COPY ../EmailLib/. ./EmailLib/
|
||||||
|
RUN dotnet publish "EmailLib/EmailLib.csproj" -c Release -o /publish
|
101
Entities/Contracts/AppMainStruct.cs
Normal file
101
Entities/Contracts/AppMainStruct.cs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Entities.Contracts
|
||||||
|
{
|
||||||
|
public enum Layout : byte
|
||||||
|
{
|
||||||
|
horizontal = 0,
|
||||||
|
vertical = 1,
|
||||||
|
|
||||||
|
Auto = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Publisher : byte
|
||||||
|
{
|
||||||
|
Unknown = 0,
|
||||||
|
统编版,
|
||||||
|
部编版,
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum GradeEnum : byte
|
||||||
|
{
|
||||||
|
Unknown = 0,
|
||||||
|
一年级 = 1,
|
||||||
|
二年级 = 2,
|
||||||
|
三年级 = 3,
|
||||||
|
四年级 = 4,
|
||||||
|
五年级 = 5,
|
||||||
|
六年级 = 6
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum DifficultyLevel : byte
|
||||||
|
{
|
||||||
|
easy,
|
||||||
|
medium,
|
||||||
|
hard
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum QuestionType : byte
|
||||||
|
{
|
||||||
|
Unknown = 0,
|
||||||
|
Spelling, // 拼写
|
||||||
|
Pronunciation, // 给带点字选择正确读音
|
||||||
|
WordFormation, // 组词
|
||||||
|
FillInTheBlanks, // 选词填空 / 补充词语
|
||||||
|
SentenceDictation, // 默写句子
|
||||||
|
SentenceRewriting, // 仿句 / 改写句子
|
||||||
|
ReadingComprehension, // 阅读理解
|
||||||
|
Composition // 作文
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum SubjectAreaEnum : byte
|
||||||
|
{
|
||||||
|
Unknown = 0,
|
||||||
|
Mathematics, // 数学
|
||||||
|
Physics, // 物理
|
||||||
|
Chemistry, // 化学
|
||||||
|
Biology, // 生物
|
||||||
|
History, // 历史
|
||||||
|
Geography, // 地理
|
||||||
|
Literature, // 语文/文学
|
||||||
|
English, // 英语
|
||||||
|
ComputerScience, // 计算机科学
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum AssignmentStructType : byte
|
||||||
|
{
|
||||||
|
Root,
|
||||||
|
Question,
|
||||||
|
Group,
|
||||||
|
Struct,
|
||||||
|
SubQuestion,
|
||||||
|
Option
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public enum ExamType : byte
|
||||||
|
{
|
||||||
|
MidtermExam, // 期中
|
||||||
|
FinalExam, // 期末
|
||||||
|
MonthlyExam, // 月考
|
||||||
|
WeeklyExam, // 周考
|
||||||
|
DailyTest, // 平时测试
|
||||||
|
AITest, // AI测试
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public enum SubmissionStatus
|
||||||
|
{
|
||||||
|
Pending, // 待提交/未开始
|
||||||
|
Submitted, // 已提交
|
||||||
|
Graded, // 已批改
|
||||||
|
Resubmission, // 待重新提交 (如果允许)
|
||||||
|
Late, // 迟交
|
||||||
|
Draft, // 草稿
|
||||||
|
}
|
||||||
|
}
|
@@ -5,6 +5,7 @@ using System.ComponentModel.DataAnnotations;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Entities.DTO;
|
||||||
|
|
||||||
namespace Entities.Contracts
|
namespace Entities.Contracts
|
||||||
{
|
{
|
||||||
@@ -24,32 +25,46 @@ namespace Entities.Contracts
|
|||||||
public string Description { get; set; }
|
public string Description { get; set; }
|
||||||
|
|
||||||
[Column("subject_area")]
|
[Column("subject_area")]
|
||||||
public string SubjectArea { get; set; }
|
public SubjectAreaEnum SubjectArea { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[Column("exam_struct_id")]
|
||||||
|
public Guid ExamStructId { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[Column("due_date")]
|
[Column("due_date")]
|
||||||
public DateTime DueDate { get; set; }
|
public DateTime DueDate { get; set; }
|
||||||
|
|
||||||
[Column("total_points")]
|
[Column("total_points")]
|
||||||
public float? TotalPoints { get; set; }
|
public byte TotalQuestions { get; set; }
|
||||||
|
|
||||||
|
[Column("score")]
|
||||||
|
public float Score { get; set; }
|
||||||
|
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public ExamType ExamType { get; set; } = ExamType.DailyTest;
|
||||||
|
|
||||||
[Column("created_by")]
|
[Column("created_by")]
|
||||||
[ForeignKey("Creator")]
|
public Guid CreatorId { get; set; }
|
||||||
public Guid CreatedBy { get; set; }
|
|
||||||
|
|
||||||
[Column("created_at")]
|
[Column("created_at")]
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
[Column("updated_at")]
|
[Column("updated_at")]
|
||||||
public DateTime UpdatedAt { get; set; }
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
[Column("deleted")]
|
[Column("deleted")]
|
||||||
public bool IsDeleted { get; set; }
|
public bool IsDeleted { get; set; } = false;
|
||||||
|
|
||||||
// Navigation Properties
|
// Navigation Properties
|
||||||
|
|
||||||
|
[ForeignKey(nameof(CreatorId))]
|
||||||
public User Creator { get; set; }
|
public User Creator { get; set; }
|
||||||
public ICollection<AssignmentClass> AssignmentClasses { get; set; }
|
public ICollection<AssignmentClass> AssignmentClasses { get; set; }
|
||||||
public ICollection<AssignmentGroup> AssignmentGroups { get; set; }
|
|
||||||
|
[ForeignKey(nameof(ExamStructId))]
|
||||||
|
public AssignmentQuestion ExamStruct { get; set; }
|
||||||
public ICollection<AssignmentAttachment> AssignmentAttachments { get; set; }
|
public ICollection<AssignmentAttachment> AssignmentAttachments { get; set; }
|
||||||
public ICollection<Submission> Submissions { get; set; }
|
public ICollection<Submission> Submissions { get; set; }
|
||||||
|
|
||||||
@@ -58,9 +73,48 @@ namespace Entities.Contracts
|
|||||||
Id = Guid.NewGuid();
|
Id = Guid.NewGuid();
|
||||||
|
|
||||||
Submissions = new HashSet<Submission>();
|
Submissions = new HashSet<Submission>();
|
||||||
AssignmentGroups = new HashSet<AssignmentGroup>();
|
|
||||||
AssignmentClasses = new HashSet<AssignmentClass>();
|
AssignmentClasses = new HashSet<AssignmentClass>();
|
||||||
AssignmentAttachments = new HashSet<AssignmentAttachment>();
|
AssignmentAttachments = new HashSet<AssignmentAttachment>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static class AssignmentExt
|
||||||
|
{
|
||||||
|
public static Submission ConvertToSubmission(this Assignment assignment, Guid studentId, Guid GraderId)
|
||||||
|
{
|
||||||
|
if (assignment == null) return new Submission();
|
||||||
|
var submission = new Submission();
|
||||||
|
|
||||||
|
submission.StudentId = studentId;
|
||||||
|
submission.SubmissionTime = DateTime.Now;
|
||||||
|
submission.Status = SubmissionStatus.Pending;
|
||||||
|
submission.GraderId = GraderId;
|
||||||
|
submission.AssignmentId = assignment.Id;
|
||||||
|
|
||||||
|
ConvertExamSturctToSubmissionDetails(assignment.ExamStruct, studentId, submission.SubmissionDetails);
|
||||||
|
|
||||||
|
return submission;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void ConvertExamSturctToSubmissionDetails(AssignmentQuestion examStruct, Guid studentId, ICollection<SubmissionDetail> submissions)
|
||||||
|
{
|
||||||
|
if (examStruct == null) return;
|
||||||
|
submissions.Add(new SubmissionDetail
|
||||||
|
{
|
||||||
|
StudentId = studentId,
|
||||||
|
AssignmentQuestionId = examStruct.Id,
|
||||||
|
IsCorrect = true,
|
||||||
|
CreatedAt = DateTime.Now,
|
||||||
|
UpdatedAt = DateTime.Now,
|
||||||
|
Status = SubmissionStatus.Pending,
|
||||||
|
});
|
||||||
|
|
||||||
|
examStruct.ChildrenAssignmentQuestion?.ToList().ForEach(s =>
|
||||||
|
{
|
||||||
|
ConvertExamSturctToSubmissionDetails(s, studentId, submissions);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,61 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Entities.Contracts
|
|
||||||
{
|
|
||||||
[Table("assignment_group")]
|
|
||||||
public class AssignmentGroup
|
|
||||||
{
|
|
||||||
[Key]
|
|
||||||
[Column("id")]
|
|
||||||
public Guid Id { get; set; }
|
|
||||||
|
|
||||||
[Column("assignment")]
|
|
||||||
[ForeignKey("Assignment")]
|
|
||||||
public Guid? AssignmentId { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
[Column("title")]
|
|
||||||
[MaxLength(65535)]
|
|
||||||
public string Title { get; set; }
|
|
||||||
|
|
||||||
[Column("descript")]
|
|
||||||
[MaxLength(65535)]
|
|
||||||
public string Descript { get; set; }
|
|
||||||
|
|
||||||
|
|
||||||
[Column("total_points")]
|
|
||||||
public float? TotalPoints { get; set; }
|
|
||||||
|
|
||||||
[Column("number")]
|
|
||||||
public byte Number { get; set; }
|
|
||||||
|
|
||||||
[Column("parent_group")]
|
|
||||||
public Guid? ParentGroup { get; set; }
|
|
||||||
|
|
||||||
[Column("deleted")]
|
|
||||||
public bool IsDeleted { get; set; }
|
|
||||||
|
|
||||||
[Column("valid_question_group")]
|
|
||||||
public bool ValidQuestionGroup { get; set; }
|
|
||||||
|
|
||||||
// Navigation Properties
|
|
||||||
public Assignment? Assignment { get; set; }
|
|
||||||
public AssignmentGroup? ParentAssignmentGroup { get; set;}
|
|
||||||
public ICollection<AssignmentGroup> ChildAssignmentGroups { get; set; }
|
|
||||||
public ICollection<AssignmentQuestion> AssignmentQuestions { get; set; }
|
|
||||||
|
|
||||||
|
|
||||||
public AssignmentGroup()
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid();
|
|
||||||
ChildAssignmentGroups = new HashSet<AssignmentGroup>();
|
|
||||||
AssignmentQuestions = new HashSet<AssignmentQuestion>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -6,6 +6,7 @@ using System.ComponentModel.DataAnnotations;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Entities.DTO;
|
||||||
|
|
||||||
namespace Entities.Contracts
|
namespace Entities.Contracts
|
||||||
{
|
{
|
||||||
@@ -17,22 +18,27 @@ namespace Entities.Contracts
|
|||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
[Column("question_id")]
|
[Column("question_id")]
|
||||||
public Guid? QuestionId { get; set; } // 设为可空
|
public Guid? QuestionId { get; set; }
|
||||||
|
|
||||||
// 当 IsGroup 为 true 时,此为 QuestionGroup 的外键
|
[Column("title")]
|
||||||
[Column("question_group_id")] // 新增一个外键列
|
[MaxLength(1024)]
|
||||||
public Guid? QuestionGroupId { get; set; } // 设为可空
|
public string? Title { get; set; }
|
||||||
|
|
||||||
[Required]
|
|
||||||
[Column("group_id")]
|
|
||||||
[ForeignKey("AssignmentGroup")]
|
|
||||||
public Guid AssignmentGroupId { get; set; }
|
|
||||||
|
|
||||||
|
[Column("description")]
|
||||||
|
public Guid? QuestionContextId { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[Column("question_number")]
|
[Column("question_number")]
|
||||||
public byte QuestionNumber { get; set; }
|
public byte Index { get; set; }
|
||||||
|
|
||||||
|
[Column("sequence")]
|
||||||
|
public string Sequence { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Column("parent_question_group_id")]
|
||||||
|
public Guid? ParentAssignmentQuestionId { get; set; }
|
||||||
|
|
||||||
|
[Column("group_state")]
|
||||||
|
public AssignmentStructType StructType { get; set; } = AssignmentStructType.Question;
|
||||||
|
|
||||||
[Column("created_at")]
|
[Column("created_at")]
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
@@ -40,18 +46,22 @@ namespace Entities.Contracts
|
|||||||
[Column("score")]
|
[Column("score")]
|
||||||
public float? Score { get; set; }
|
public float? Score { get; set; }
|
||||||
|
|
||||||
[Required]
|
|
||||||
[Column("bgroup")]
|
|
||||||
public bool IsGroup { get; set; }
|
|
||||||
|
|
||||||
[Column("deleted")]
|
[Column("deleted")]
|
||||||
public bool IsDeleted { get; set; }
|
public bool IsDeleted { get; set; }
|
||||||
|
|
||||||
|
|
||||||
public Question Question { get; set; }
|
public Question? Question { get; set; }
|
||||||
public QuestionGroup QuestionGroup { get; set; }
|
public Assignment? Assignment { get; set; }
|
||||||
|
|
||||||
|
[ForeignKey(nameof(QuestionContextId))]
|
||||||
|
public QuestionContext? QuestionContext { get; set; }
|
||||||
|
|
||||||
public ICollection<SubmissionDetail> SubmissionDetails { get; set; }
|
public ICollection<SubmissionDetail> SubmissionDetails { get; set; }
|
||||||
public AssignmentGroup AssignmentGroup { get; set; }
|
|
||||||
|
[ForeignKey(nameof(ParentAssignmentQuestionId))]
|
||||||
|
public AssignmentQuestion? ParentAssignmentQuestion { get; set; }
|
||||||
|
public ICollection<AssignmentQuestion> ChildrenAssignmentQuestion { get; set; } = new List<AssignmentQuestion>();
|
||||||
|
|
||||||
|
|
||||||
public AssignmentQuestion()
|
public AssignmentQuestion()
|
||||||
{
|
{
|
||||||
|
@@ -14,14 +14,16 @@ namespace Entities.Contracts
|
|||||||
[Key]
|
[Key]
|
||||||
[Column("class_id")]
|
[Column("class_id")]
|
||||||
public Guid ClassId { get; set; }
|
public Guid ClassId { get; set; }
|
||||||
|
[ForeignKey(nameof(ClassId))]
|
||||||
public Class Class { get; set; }
|
public Class Class { get; set; }
|
||||||
|
|
||||||
[Key]
|
[Key]
|
||||||
[Column("teacher_id")]
|
[Column("teacher_id")]
|
||||||
public Guid TeacherId { get; set; }
|
public Guid TeacherId { get; set; }
|
||||||
|
[ForeignKey(nameof(TeacherId))]
|
||||||
public User Teacher { get; set; }
|
public User Teacher { get; set; }
|
||||||
|
|
||||||
[Column("subject_taught")]
|
[Column("subject_taught")]
|
||||||
public string SubjectTaught { get; set; }
|
public SubjectAreaEnum SubjectTaught { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -16,36 +16,42 @@ namespace Entities.Contracts
|
|||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[Column("question_text")]
|
[Column("title")]
|
||||||
[MaxLength(65535)]
|
[MaxLength(65535)]
|
||||||
public string QuestionText { get; set; }
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
[Column("answer")]
|
||||||
|
[MaxLength(65535)]
|
||||||
|
public string? Answer { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[Column("question_type")]
|
[Column("type")]
|
||||||
[MaxLength(20)]
|
[MaxLength(20)]
|
||||||
public QuestionType QuestionType { get; set; }
|
public QuestionType Type { get; set; } = QuestionType.Unknown;
|
||||||
|
|
||||||
[Column("correct_answer")]
|
|
||||||
[MaxLength(65535)]
|
|
||||||
public string CorrectAnswer { get; set; }
|
|
||||||
|
|
||||||
[Column("question_group_id")]
|
|
||||||
public Guid? QuestionGroupId { get; set; }
|
|
||||||
|
|
||||||
[Column("difficulty_level")]
|
[Column("difficulty_level")]
|
||||||
[MaxLength(10)]
|
[MaxLength(10)]
|
||||||
public DifficultyLevel DifficultyLevel { get; set; }
|
public DifficultyLevel DifficultyLevel { get; set; } = DifficultyLevel.easy;
|
||||||
|
|
||||||
[Column("subject_area")]
|
[Column("subject_area")]
|
||||||
public SubjectAreaEnum SubjectArea { get; set; }
|
public SubjectAreaEnum SubjectArea { get; set; } = SubjectAreaEnum.Unknown;
|
||||||
|
|
||||||
|
|
||||||
[Column("options")]
|
[Column("options")]
|
||||||
public string? Options { get; set; }
|
public string? Options { get; set; }
|
||||||
|
|
||||||
|
[Column("key_point")]
|
||||||
|
public Guid? KeyPointId { get; set; }
|
||||||
|
|
||||||
|
[Column("lesson")]
|
||||||
|
public Guid? LessonId { get; set; }
|
||||||
|
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[Column("created_by")]
|
[Column("created_by")]
|
||||||
[ForeignKey("Creator")]
|
public Guid CreatorId { get; set; }
|
||||||
public Guid CreatedBy { get; set; }
|
|
||||||
|
|
||||||
[Column("created_at")]
|
[Column("created_at")]
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
@@ -56,13 +62,17 @@ namespace Entities.Contracts
|
|||||||
[Column("deleted")]
|
[Column("deleted")]
|
||||||
public bool IsDeleted { get; set; }
|
public bool IsDeleted { get; set; }
|
||||||
|
|
||||||
[Column("valid_question")]
|
|
||||||
public bool ValidQuestion { get; set; }
|
|
||||||
|
|
||||||
// Navigation Properties
|
// Navigation Properties
|
||||||
|
[ForeignKey(nameof(CreatorId))]
|
||||||
public User Creator { get; set; }
|
public User Creator { get; set; }
|
||||||
public QuestionGroup QuestionGroup { get; set; }
|
|
||||||
public ICollection<AssignmentQuestion> AssignmentQuestions { get; set; }
|
[ForeignKey(nameof(KeyPointId))]
|
||||||
|
public KeyPoint? KeyPoint { get; set; }
|
||||||
|
[ForeignKey(nameof(LessonId))]
|
||||||
|
public Lesson? Lesson { get; set; }
|
||||||
|
|
||||||
|
public ICollection<AssignmentQuestion>? AssignmentQuestions { get; set; }
|
||||||
|
|
||||||
public Question()
|
public Question()
|
||||||
{
|
{
|
||||||
@@ -71,39 +81,5 @@ namespace Entities.Contracts
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum DifficultyLevel
|
|
||||||
{
|
|
||||||
easy,
|
|
||||||
medium,
|
|
||||||
hard
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum QuestionType
|
|
||||||
{
|
|
||||||
Unknown, // 可以有一个未知类型或作为默认
|
|
||||||
Spelling, // 拼写
|
|
||||||
Pronunciation, // 给带点字选择正确读音
|
|
||||||
WordFormation, // 组词
|
|
||||||
FillInTheBlanks, // 选词填空 / 补充词语
|
|
||||||
SentenceDictation, // 默写句子
|
|
||||||
SentenceRewriting, // 仿句 / 改写句子
|
|
||||||
ReadingComprehension, // 阅读理解
|
|
||||||
Composition // 作文
|
|
||||||
// ... 添加您其他题目类型
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum SubjectAreaEnum // 建议命名为 SubjectAreaEnum 以避免与属性名冲突
|
|
||||||
{
|
|
||||||
Unknown, // 未知或默认
|
|
||||||
Mathematics, // 数学
|
|
||||||
Physics, // 物理
|
|
||||||
Chemistry, // 化学
|
|
||||||
Biology, // 生物
|
|
||||||
History, // 历史
|
|
||||||
Geography, // 地理
|
|
||||||
Literature, // 语文/文学
|
|
||||||
English, // 英语
|
|
||||||
ComputerScience, // 计算机科学
|
|
||||||
// ... 你可以根据需要添加更多科目
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
25
Entities/Contracts/QuestionContext.cs
Normal file
25
Entities/Contracts/QuestionContext.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Entities.Contracts
|
||||||
|
{
|
||||||
|
public class QuestionContext
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[InverseProperty(nameof(AssignmentQuestion.QuestionContext))]
|
||||||
|
public ICollection<AssignmentQuestion>? Questions { get; set; } = new List<AssignmentQuestion>();
|
||||||
|
|
||||||
|
|
||||||
|
public QuestionContext()
|
||||||
|
{
|
||||||
|
Questions = new HashSet<AssignmentQuestion>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,79 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Entities.Contracts
|
|
||||||
{
|
|
||||||
[Table("question_groups")]
|
|
||||||
public class QuestionGroup
|
|
||||||
{
|
|
||||||
[Key]
|
|
||||||
[Column("id")]
|
|
||||||
public Guid Id { get; set; }
|
|
||||||
|
|
||||||
[Column("title")]
|
|
||||||
[MaxLength(255)]
|
|
||||||
public string Title { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
[Column("description")]
|
|
||||||
[MaxLength(65535)]
|
|
||||||
public string Description { get; set; }
|
|
||||||
|
|
||||||
[Column("type")]
|
|
||||||
[MaxLength(50)]
|
|
||||||
public string Type { get; set; }
|
|
||||||
|
|
||||||
[Column("difficulty_level")]
|
|
||||||
[MaxLength(10)]
|
|
||||||
public DifficultyLevel DifficultyLevel { get; set; }
|
|
||||||
|
|
||||||
[Column("subject_area")]
|
|
||||||
public SubjectAreaEnum SubjectArea { get; set; }
|
|
||||||
|
|
||||||
[Column("total_questions")]
|
|
||||||
public int TotalQuestions { get; set; } = 0;
|
|
||||||
|
|
||||||
[Column("parent_question_group")]
|
|
||||||
public Guid? ParentQG { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
[Column("created_by")]
|
|
||||||
[ForeignKey("Creator")]
|
|
||||||
public Guid CreatedBy { get; set; }
|
|
||||||
|
|
||||||
[Column("created_at")]
|
|
||||||
public DateTime CreatedAt { get; set; }
|
|
||||||
|
|
||||||
[Column("updated_at")]
|
|
||||||
public DateTime UpdatedAt { get; set; }
|
|
||||||
|
|
||||||
[Column("deleted")]
|
|
||||||
public bool IsDeleted { get; set; }
|
|
||||||
|
|
||||||
[Column("valid_group")]
|
|
||||||
public bool ValidGroup { get; set; }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public User Creator { get; set; }
|
|
||||||
public QuestionGroup ParentQuestionGroup { get; set; }
|
|
||||||
public ICollection<QuestionGroup> ChildQuestionGroups { get; set; }
|
|
||||||
public ICollection<AssignmentQuestion> AssignmentQuestions { get; set; }
|
|
||||||
public ICollection<Question> Questions { get; set; }
|
|
||||||
|
|
||||||
public QuestionGroup()
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid();
|
|
||||||
Questions = new HashSet<Question>();
|
|
||||||
CreatedAt = DateTime.UtcNow;
|
|
||||||
UpdatedAt = DateTime.UtcNow;
|
|
||||||
IsDeleted = false;
|
|
||||||
ValidGroup = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -28,7 +28,7 @@ namespace Entities.Contracts
|
|||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[Column("attempt_number")]
|
[Column("attempt_number")]
|
||||||
public Guid AttemptNumber { get; set; }
|
public byte AttemptNumber { get; set; }
|
||||||
|
|
||||||
[Column("submission_time")]
|
[Column("submission_time")]
|
||||||
public DateTime SubmissionTime { get; set; }
|
public DateTime SubmissionTime { get; set; }
|
||||||
@@ -37,11 +37,11 @@ namespace Entities.Contracts
|
|||||||
public float? OverallGrade { get; set; }
|
public float? OverallGrade { get; set; }
|
||||||
|
|
||||||
[Column("overall_feedback")]
|
[Column("overall_feedback")]
|
||||||
public string OverallFeedback { get; set; }
|
public string? OverallFeedback { get; set; }
|
||||||
|
|
||||||
[Column("graded_by")]
|
[Column("graded_by")]
|
||||||
[ForeignKey("Grader")]
|
[ForeignKey("Grader")]
|
||||||
public Guid? GradedBy { get; set; }
|
public Guid? GraderId { get; set; }
|
||||||
|
|
||||||
[Column("graded_at")]
|
[Column("graded_at")]
|
||||||
public DateTime? GradedAt { get; set; }
|
public DateTime? GradedAt { get; set; }
|
||||||
@@ -49,6 +49,12 @@ namespace Entities.Contracts
|
|||||||
[Column("deleted")]
|
[Column("deleted")]
|
||||||
public bool IsDeleted { get; set; }
|
public bool IsDeleted { get; set; }
|
||||||
|
|
||||||
|
public byte TotalQuesNum { get; set; }
|
||||||
|
|
||||||
|
public byte ErrorQuesNum { get; set; }
|
||||||
|
|
||||||
|
public byte TotalScore { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[Column("status")]
|
[Column("status")]
|
||||||
public SubmissionStatus Status { get; set; }
|
public SubmissionStatus Status { get; set; }
|
||||||
@@ -66,14 +72,4 @@ namespace Entities.Contracts
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum SubmissionStatus
|
|
||||||
{
|
|
||||||
Pending, // 待提交/未开始
|
|
||||||
Submitted, // 已提交
|
|
||||||
Graded, // 已批改
|
|
||||||
Resubmission, // 待重新提交 (如果允许)
|
|
||||||
Late, // 迟交
|
|
||||||
Draft, // 草稿
|
|
||||||
// ... 添加你需要的其他状态
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -23,7 +23,6 @@ namespace Entities.Contracts
|
|||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[Column("student_id")]
|
[Column("student_id")]
|
||||||
[ForeignKey("User")]
|
|
||||||
public Guid StudentId { get; set; }
|
public Guid StudentId { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
@@ -32,16 +31,16 @@ namespace Entities.Contracts
|
|||||||
public Guid AssignmentQuestionId { get; set; }
|
public Guid AssignmentQuestionId { get; set; }
|
||||||
|
|
||||||
[Column("student_answer")]
|
[Column("student_answer")]
|
||||||
public string StudentAnswer { get; set; }
|
public string? StudentAnswer { get; set; }
|
||||||
|
|
||||||
[Column("is_correct")]
|
[Column("is_correct")]
|
||||||
public bool? IsCorrect { get; set; }
|
public bool? IsCorrect { get; set; }
|
||||||
|
|
||||||
[Column("points_awarded")]
|
[Column("points_awarded")]
|
||||||
public float? PointsAwarded { get; set; }
|
public float? PointsAwarded { get; set; } // score
|
||||||
|
|
||||||
[Column("teacher_feedback")]
|
[Column("teacher_feedback")]
|
||||||
public string TeacherFeedback { get; set; }
|
public string? TeacherFeedback { get; set; }
|
||||||
|
|
||||||
[Column("created_at")]
|
[Column("created_at")]
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
@@ -52,8 +51,19 @@ namespace Entities.Contracts
|
|||||||
[Column("deleted")]
|
[Column("deleted")]
|
||||||
public bool IsDeleted { get; set; }
|
public bool IsDeleted { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[Column("status")]
|
||||||
|
public SubmissionStatus Status { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[ForeignKey(nameof(StudentId))]
|
||||||
|
public User Student { get; set; }
|
||||||
|
|
||||||
|
[ForeignKey(nameof(SubmissionId))]
|
||||||
public Submission Submission { get; set; }
|
public Submission Submission { get; set; }
|
||||||
public User User { get; set; }
|
|
||||||
|
[ForeignKey(nameof(AssignmentQuestionId))]
|
||||||
public AssignmentQuestion AssignmentQuestion { get; set; }
|
public AssignmentQuestion AssignmentQuestion { get; set; }
|
||||||
|
|
||||||
public SubmissionDetail()
|
public SubmissionDetail()
|
||||||
|
36
Entities/Contracts/Textbook/KeyPoint.cs
Normal file
36
Entities/Contracts/Textbook/KeyPoint.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Entities.Contracts
|
||||||
|
{
|
||||||
|
[Table("key_point")]
|
||||||
|
public class KeyPoint
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
[StringLength(255)]
|
||||||
|
public string Key { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public Guid LessonID { get; set; }
|
||||||
|
|
||||||
|
[ForeignKey(nameof(LessonID))]
|
||||||
|
public Lesson Lesson { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
public ICollection<Question> Questions { get; set; }
|
||||||
|
|
||||||
|
public KeyPoint()
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid();
|
||||||
|
Questions = new HashSet<Question>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
48
Entities/Contracts/Textbook/Lesson.cs
Normal file
48
Entities/Contracts/Textbook/Lesson.cs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Entities.Contracts
|
||||||
|
{
|
||||||
|
[Table("lesson")]
|
||||||
|
public class Lesson
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
[StringLength(255)]
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public Guid TextbookID { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
[ForeignKey(nameof(TextbookID))]
|
||||||
|
public Textbook Textbook { get; set; }
|
||||||
|
|
||||||
|
[InverseProperty(nameof(KeyPoint.Lesson))]
|
||||||
|
public ICollection<KeyPoint>? KeyPoints { get; set; }
|
||||||
|
|
||||||
|
[InverseProperty(nameof(Question.Lesson))]
|
||||||
|
public ICollection<Question>? Questions { get; set; }
|
||||||
|
|
||||||
|
[InverseProperty(nameof(LessonQuestion.Lesson))]
|
||||||
|
public ICollection<LessonQuestion>? LessonQuestions { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public Lesson()
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid();
|
||||||
|
KeyPoints = new HashSet<KeyPoint>();
|
||||||
|
Questions = new HashSet<Question>();
|
||||||
|
LessonQuestions = new HashSet<LessonQuestion>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
Entities/Contracts/Textbook/LessonQuestion.cs
Normal file
32
Entities/Contracts/Textbook/LessonQuestion.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Entities.Contracts
|
||||||
|
{
|
||||||
|
[Table("lesson_question")]
|
||||||
|
public class LessonQuestion
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(65535)]
|
||||||
|
public string Question { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public Guid LessonID { get; set; }
|
||||||
|
|
||||||
|
[ForeignKey(nameof(LessonID))]
|
||||||
|
public Lesson Lesson { get; set; }
|
||||||
|
|
||||||
|
public LessonQuestion()
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
36
Entities/Contracts/Textbook/Textbook.cs
Normal file
36
Entities/Contracts/Textbook/Textbook.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Entities.Contracts
|
||||||
|
{
|
||||||
|
[Table("textbook")]
|
||||||
|
public class Textbook
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
public GradeEnum Grade { get; set; } = GradeEnum.Unknown;
|
||||||
|
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public Publisher Publisher { get; set; } = Publisher.部编版;
|
||||||
|
|
||||||
|
public SubjectAreaEnum SubjectArea { get; set; } = SubjectAreaEnum.Unknown;
|
||||||
|
|
||||||
|
[InverseProperty(nameof(Lesson.Textbook))]
|
||||||
|
public ICollection<Lesson> Lessons { get; set; }
|
||||||
|
|
||||||
|
public Textbook()
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid();
|
||||||
|
Lessons = new HashSet<Lesson>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -21,7 +21,9 @@ namespace Entities.Contracts
|
|||||||
[Column("deleted")]
|
[Column("deleted")]
|
||||||
public bool IsDeleted { get; set; }
|
public bool IsDeleted { get; set; }
|
||||||
|
|
||||||
|
[InverseProperty(nameof(ClassTeacher.Teacher))]
|
||||||
public ICollection<ClassTeacher> TaughtClassesLink { get; set; }
|
public ICollection<ClassTeacher> TaughtClassesLink { get; set; }
|
||||||
|
[InverseProperty(nameof(ClassStudent.Student))]
|
||||||
public ICollection<ClassStudent> EnrolledClassesLink { get; set; }
|
public ICollection<ClassStudent> EnrolledClassesLink { get; set; }
|
||||||
|
|
||||||
public ICollection<Question> CreatedQuestions { get; set; }
|
public ICollection<Question> CreatedQuestions { get; set; }
|
||||||
|
15
Entities/DTO/AssigExamToStudentsDto.cs
Normal file
15
Entities/DTO/AssigExamToStudentsDto.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Entities.DTO
|
||||||
|
{
|
||||||
|
public class AssigExamToStudentsDto
|
||||||
|
{
|
||||||
|
public Guid CreaterId { get; set; }
|
||||||
|
public Guid AssignmentId { get; set; }
|
||||||
|
public List<Guid> StudentIds { get; set; } = new List<Guid>();
|
||||||
|
}
|
||||||
|
}
|
23
Entities/DTO/AssignmentClassDto.cs
Normal file
23
Entities/DTO/AssignmentClassDto.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using Entities.Contracts;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
|
namespace Entities.DTO
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
public class AssignmentClassDto
|
||||||
|
{
|
||||||
|
public AssignmentDto Assignment { get; set; }
|
||||||
|
public Class ClassId { get; set; }
|
||||||
|
public DateTime AssignedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
29
Entities/DTO/AssignmentDto.cs
Normal file
29
Entities/DTO/AssignmentDto.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using Entities.Contracts;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Entities.DTO
|
||||||
|
{
|
||||||
|
public class AssignmentDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.Empty;
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public byte TotalQuestions { get; set; }
|
||||||
|
public float Score { get; set; } = 0;
|
||||||
|
public SubjectAreaEnum SubjectArea { get; set; } = SubjectAreaEnum.Unknown;
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
public DateTime DueDate { get; set; }
|
||||||
|
public Guid CreatorId { get; set; }
|
||||||
|
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public ExamType ExamType { get; set; } = ExamType.DailyTest;
|
||||||
|
|
||||||
|
public AssignmentQuestionDto ExamStruct { get; set; } = new AssignmentQuestionDto();
|
||||||
|
}
|
||||||
|
}
|
31
Entities/DTO/AssignmentQuestionDto.cs
Normal file
31
Entities/DTO/AssignmentQuestionDto.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using Entities.Contracts;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Entities.DTO
|
||||||
|
{
|
||||||
|
public class AssignmentQuestionDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.Empty;
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public QuestionContextDto? Description { get; set; }
|
||||||
|
|
||||||
|
public byte Index { get; set; } = 0;
|
||||||
|
public float Score { get; set; } = 0;
|
||||||
|
public string Sequence { get; set; } = string.Empty;
|
||||||
|
public bool BCorrect { get; set; } = true;
|
||||||
|
|
||||||
|
public Layout Layout { get; set; } = Layout.horizontal;
|
||||||
|
public AssignmentStructType StructType { get; set; } = AssignmentStructType.Question;
|
||||||
|
|
||||||
|
public AssignmentQuestionDto? ParentAssignmentQuestion { get; set; }
|
||||||
|
public List<AssignmentQuestionDto> ChildrenAssignmentQuestion { get; set; } = new List<AssignmentQuestionDto>();
|
||||||
|
|
||||||
|
public QuestionDto? Question { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@@ -1,81 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Xml.Serialization;
|
|
||||||
|
|
||||||
namespace Entities.DTO
|
|
||||||
{
|
|
||||||
public class ExamDto
|
|
||||||
{
|
|
||||||
public Guid? AssignmentId { get; set; }
|
|
||||||
public string CreaterEmail { get; set; }
|
|
||||||
public string AssignmentTitle { get; set; } = string.Empty;
|
|
||||||
public string Description { get; set; }
|
|
||||||
public string SubjectArea { get; set; }
|
|
||||||
public QuestionGroupDto QuestionGroups { get; set; } = new QuestionGroupDto();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class QuestionGroupDto
|
|
||||||
{
|
|
||||||
public byte Index { get; set; }
|
|
||||||
|
|
||||||
public string? Title { get; set; }
|
|
||||||
|
|
||||||
public float Score { get; set; }
|
|
||||||
|
|
||||||
public string? Descript { get; set; }
|
|
||||||
public List<SubQuestionDto> SubQuestions { get; set; } = new List<SubQuestionDto>();
|
|
||||||
public List<QuestionGroupDto> SubQuestionGroups { get; set; } = new List<QuestionGroupDto>();
|
|
||||||
|
|
||||||
// 标记是否是一个具有上下文的单独问题
|
|
||||||
public bool ValidQuestionGroup { get; set; } = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SubQuestionDto
|
|
||||||
{
|
|
||||||
|
|
||||||
public byte Index { get; set; }
|
|
||||||
|
|
||||||
public string? Stem { get; set; }
|
|
||||||
|
|
||||||
public float Score { get; set; }
|
|
||||||
|
|
||||||
public List<OptionDto> Options { get; set; } = new List<OptionDto>();
|
|
||||||
|
|
||||||
public string? SampleAnswer { get; set; }
|
|
||||||
|
|
||||||
public string? QuestionType { get; set; }
|
|
||||||
public string? DifficultyLevel { get; set; }
|
|
||||||
|
|
||||||
// 标记是否是一个独立的问题
|
|
||||||
public bool ValidQuestion { get; set; } = false;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public class OptionDto
|
|
||||||
{
|
|
||||||
public string? Value { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static class ExamDtoExtension
|
|
||||||
{
|
|
||||||
public static void Convert(this ExamDto examDto)
|
|
||||||
{
|
|
||||||
var qg = examDto.QuestionGroups;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Convert(this QuestionGroupDto examDto)
|
|
||||||
{
|
|
||||||
if(examDto.ValidQuestionGroup)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
15
Entities/DTO/QuestionContextDto.cs
Normal file
15
Entities/DTO/QuestionContextDto.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Entities.DTO
|
||||||
|
{
|
||||||
|
|
||||||
|
public class QuestionContextDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.Empty;
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
48
Entities/DTO/QuestionDto.cs
Normal file
48
Entities/DTO/QuestionDto.cs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
using Entities.Contracts;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Entities.DTO
|
||||||
|
{
|
||||||
|
public class QuestionDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.Empty;
|
||||||
|
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public QuestionType Type { get; set; } = QuestionType.Unknown;
|
||||||
|
|
||||||
|
public string? Answer { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string? Options { get; set; }
|
||||||
|
|
||||||
|
public DifficultyLevel DifficultyLevel { get; set; } = DifficultyLevel.easy;
|
||||||
|
|
||||||
|
public SubjectAreaEnum SubjectArea { get; set; } = SubjectAreaEnum.Unknown;
|
||||||
|
|
||||||
|
public Guid CreatorId { get; set; }
|
||||||
|
|
||||||
|
public Guid? KeyPointId { get; set; }
|
||||||
|
|
||||||
|
public Guid? LessonId { get; set; }
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
|
public DateTime UpdatedAt { get; set; } = DateTime.Now;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Can be removed because the class isn't used
|
||||||
|
/// </summary>
|
||||||
|
public class OptionDto
|
||||||
|
{
|
||||||
|
public string? Value { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
20
Entities/DTO/StudentDto.cs
Normal file
20
Entities/DTO/StudentDto.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using Entities.Contracts;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Entities.DTO
|
||||||
|
{
|
||||||
|
public class StudentDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string? DisplayName { get; set; }
|
||||||
|
|
||||||
|
public UInt32 ErrorQuestionNum { get; set; }
|
||||||
|
public Dictionary<QuestionType, UInt32> ErrorQuestionTypes { get; set; } = new Dictionary<QuestionType, UInt32>();
|
||||||
|
public Dictionary<SubjectAreaEnum, UInt32> SubjectAreaErrorQuestionDis { get; set; } = new Dictionary<SubjectAreaEnum, UInt32>();
|
||||||
|
public Dictionary<byte, UInt32> LessonErrorDis { get; set; } = new Dictionary<byte, UInt32>();
|
||||||
|
}
|
||||||
|
}
|
21
Entities/DTO/SubmissionDetailDto.cs
Normal file
21
Entities/DTO/SubmissionDetailDto.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using Entities.Contracts;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Entities.DTO
|
||||||
|
{
|
||||||
|
public class SubmissionDetailDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.Empty;
|
||||||
|
public Guid StudentId { get; set; }
|
||||||
|
public Guid AssignmentQuestionId { get; set; }
|
||||||
|
public string? StudentAnswer { get; set; }
|
||||||
|
public bool? IsCorrect { get; set; }
|
||||||
|
public float? PointsAwarded { get; set; }
|
||||||
|
public string? TeacherFeedback { get; set; }
|
||||||
|
public SubmissionStatus Status { get; set; } = SubmissionStatus.Graded;
|
||||||
|
}
|
||||||
|
}
|
23
Entities/DTO/SubmissionDto.cs
Normal file
23
Entities/DTO/SubmissionDto.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using Entities.Contracts;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Entities.DTO
|
||||||
|
{
|
||||||
|
public class SubmissionDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.Empty;
|
||||||
|
public Guid AssignmentId { get; set; }
|
||||||
|
public Guid StudentId { get; set; }
|
||||||
|
public DateTime SubmissionTime { get; set; }
|
||||||
|
public float OverallGrade { get; set; } = 0;
|
||||||
|
public string OverallFeedback { get; set; } = string.Empty;
|
||||||
|
public Guid? GraderId { get; set; }
|
||||||
|
public DateTime? GradedAt { get; set; }
|
||||||
|
public SubmissionStatus Status { get; set; }
|
||||||
|
public List<SubmissionDetailDto> SubmissionDetails { get; set; } = new List<SubmissionDetailDto>();
|
||||||
|
}
|
||||||
|
}
|
14
Entities/DTO/UserClassRoleDto.cs
Normal file
14
Entities/DTO/UserClassRoleDto.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Entities.DTO
|
||||||
|
{
|
||||||
|
public class UserClassRoleDto
|
||||||
|
{
|
||||||
|
public List<(byte, byte)> ClassInfo { get; set; } = new List<(byte, byte)> ();
|
||||||
|
public string Role { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
15
Entities/DTO/UserDto.cs
Normal file
15
Entities/DTO/UserDto.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Entities.DTO
|
||||||
|
{
|
||||||
|
public class UserDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string? DisplayName { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
8
Entities/Dockerfile
Normal file
8
Entities/Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# ./Entities/Dockerfile
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS entities
|
||||||
|
WORKDIR /src
|
||||||
|
COPY ../Entities/*.csproj ./Entities/
|
||||||
|
RUN dotnet restore "Entities/Entities.csproj"
|
||||||
|
|
||||||
|
COPY ../Entities/. ./Entities/
|
||||||
|
RUN dotnet publish "Entities/Entities.csproj" -c Release -o /publish
|
@@ -1,23 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Components.Authorization;
|
|
||||||
using System.Security.Claims;
|
|
||||||
|
|
||||||
namespace TechHelper.Client.AuthProviders
|
|
||||||
{
|
|
||||||
public class TestAuthStateProvider : AuthenticationStateProvider
|
|
||||||
{
|
|
||||||
|
|
||||||
|
|
||||||
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
|
|
||||||
{
|
|
||||||
var claims = new List<Claim>
|
|
||||||
{
|
|
||||||
new Claim(ClaimTypes.Name, "John Doe"),
|
|
||||||
new Claim(ClaimTypes.Role, "Administrator")
|
|
||||||
};
|
|
||||||
|
|
||||||
var anonymous = new ClaimsIdentity();
|
|
||||||
|
|
||||||
return await Task.FromResult(new AuthenticationState(new ClaimsPrincipal(anonymous)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
24
TechHelper.Client/Content/AutoMapperProFile.cs
Normal file
24
TechHelper.Client/Content/AutoMapperProFile.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using AutoMapper.Internal.Mappers;
|
||||||
|
using Entities.Contracts;
|
||||||
|
using Entities.DTO;
|
||||||
|
using TechHelper.Client.Exam;
|
||||||
|
|
||||||
|
namespace TechHelper.Context
|
||||||
|
{
|
||||||
|
public class AutoMapperProFile : Profile
|
||||||
|
{
|
||||||
|
public AutoMapperProFile()
|
||||||
|
{
|
||||||
|
CreateMap<QuestionEx, QuestionDto>()
|
||||||
|
.ForMember(d => d.Options, o => o.MapFrom(s => string.Join(Environment.NewLine, s.Options.Select(op => op.Text))));
|
||||||
|
CreateMap<AssignmentQuestionEx, AssignmentQuestionDto>()
|
||||||
|
.ForMember(d=>d.Description, o=>o.Ignore());
|
||||||
|
CreateMap<AssignmentEx, AssignmentDto>();
|
||||||
|
|
||||||
|
|
||||||
|
CreateMap<AssignmentCheckData, Submission>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
26
TechHelper.Client/Dockerfile
Normal file
26
TechHelper.Client/Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# 构建阶段
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS client
|
||||||
|
|
||||||
|
|
||||||
|
#COPY --from=entitieslib:latest /publish /publish/entities
|
||||||
|
#COPY --from=emaillib:latest /publish /publish/emaillib
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 复制依赖文件 & 恢复
|
||||||
|
#COPY ./TechHelper.Client.sln ./
|
||||||
|
COPY ../TechHelper.Client/*.csproj ../TechHelper.Client/
|
||||||
|
|
||||||
|
RUN dotnet nuget locals all --clear
|
||||||
|
RUN dotnet restore "/app/TechHelper.Client/TechHelper.Client.csproj"
|
||||||
|
|
||||||
|
# 复制代码 & 发布
|
||||||
|
COPY . ./
|
||||||
|
WORKDIR /app/TechHelper.Client
|
||||||
|
RUN dotnet publish "/app/TechHelper.Client/TechHelper.Client.csproj" -c Release -o /publish
|
||||||
|
|
||||||
|
FROM nginx:alpine AS final
|
||||||
|
RUN rm -rf /usr/share/nginx/html
|
||||||
|
COPY --from=client /publish/wwwroot /usr/share/nginx/html
|
||||||
|
|
||||||
|
EXPOSE 80
|
94
TechHelper.Client/Exam/AssignmentCheckData.cs
Normal file
94
TechHelper.Client/Exam/AssignmentCheckData.cs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
using Entities.DTO;
|
||||||
|
|
||||||
|
namespace TechHelper.Client.Exam
|
||||||
|
{
|
||||||
|
public class AssignmentCheckData
|
||||||
|
{
|
||||||
|
public string Title { get; set; }
|
||||||
|
public Guid AssignmentId { get; set; }
|
||||||
|
public Guid StudentId { get; set; }
|
||||||
|
public List<AssignmentCheckQuestion> Questions { get; set; } = new List<AssignmentCheckQuestion>();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class AssignmentCheckQuestion
|
||||||
|
{
|
||||||
|
public string Sequence { get; set; } = string.Empty;
|
||||||
|
public AssignmentQuestionDto AssignmentQuestionDto { get; set; } = new AssignmentQuestionDto();
|
||||||
|
public float Score { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Student
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QuestionAnswerStatus
|
||||||
|
{
|
||||||
|
public string QuestionSequence { get; set; } = string.Empty; // 题目序号,例如 "1.1"
|
||||||
|
public string QuestionText { get; set; } = string.Empty; // 题目文本
|
||||||
|
public float QuestionScore { get; set; } // 题目分值
|
||||||
|
public Dictionary<Guid, bool> StudentCorrectStatus { get; set; } = new Dictionary<Guid, bool>();
|
||||||
|
// Key: Student.Id, Value: true 表示正确,false 表示错误
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QuestionRowData
|
||||||
|
{
|
||||||
|
public AssignmentCheckQuestion QuestionItem { get; set; } // 原始题目信息
|
||||||
|
public Dictionary<Guid, bool> StudentAnswers { get; set; } = new Dictionary<Guid, bool>();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static class ExamStructExtensions
|
||||||
|
{
|
||||||
|
public static AssignmentCheckData GetStruct(this AssignmentDto dto)
|
||||||
|
{
|
||||||
|
if (dto == null)
|
||||||
|
{
|
||||||
|
return new AssignmentCheckData { Title = "无效试卷", Questions = new List<AssignmentCheckQuestion>() };
|
||||||
|
}
|
||||||
|
|
||||||
|
var examStruct = new AssignmentCheckData
|
||||||
|
{
|
||||||
|
Title = dto.Title
|
||||||
|
};
|
||||||
|
|
||||||
|
GetSeqRecursive(dto.ExamStruct, null, examStruct.Questions);
|
||||||
|
|
||||||
|
return examStruct;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 递归方法,用于生成所有题目和子题目的完整序号。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="currentGroup">当前正在处理的题目组。</param>
|
||||||
|
/// <param name="parentSequence">当前题目组的父级序号(例如:"1", "2.1")。如果为空,则表示顶级题目组。</param>
|
||||||
|
/// <param name="allQuestions">用于收集所有生成题目项的列表。</param>
|
||||||
|
private static void GetSeqRecursive(
|
||||||
|
AssignmentQuestionDto currentGroup,
|
||||||
|
string? parentSequence,
|
||||||
|
List<AssignmentCheckQuestion> allQuestions)
|
||||||
|
{
|
||||||
|
string currentGroupSequence = parentSequence != null
|
||||||
|
? $"{parentSequence}.{currentGroup.Index}"
|
||||||
|
: currentGroup.Index.ToString();
|
||||||
|
|
||||||
|
foreach (var subGroup in currentGroup.ChildrenAssignmentQuestion)
|
||||||
|
{
|
||||||
|
GetSeqRecursive(subGroup, currentGroupSequence, allQuestions);
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrEmpty(currentGroup.Sequence))
|
||||||
|
{
|
||||||
|
|
||||||
|
allQuestions.Add(new AssignmentCheckQuestion
|
||||||
|
{
|
||||||
|
AssignmentQuestionDto = currentGroup,
|
||||||
|
//Sequence = currentGroupSequence,
|
||||||
|
Sequence = currentGroup.Sequence,
|
||||||
|
Score = currentGroup.Score,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -5,245 +5,4 @@ using System.IO; // 用于 XML 反序列化
|
|||||||
|
|
||||||
namespace TechHelper.Client.Exam
|
namespace TechHelper.Client.Exam
|
||||||
{
|
{
|
||||||
|
|
||||||
//[XmlRoot("EP")]
|
|
||||||
//public class StringsList
|
|
||||||
//{
|
|
||||||
|
|
||||||
// [XmlElement("Q")]
|
|
||||||
// public List<string> Items { get; set; }
|
|
||||||
//}
|
|
||||||
|
|
||||||
//// XML 根元素 <EP>
|
|
||||||
//[XmlRoot("EP")]
|
|
||||||
//public class ExamPaper
|
|
||||||
//{
|
|
||||||
// // XML 特性:<QGs> 包含 <QG> 列表
|
|
||||||
// [XmlArray("QGs")]
|
|
||||||
// [XmlArrayItem("QG")]
|
|
||||||
// [JsonProperty("QuestionGroups")]
|
|
||||||
// public List<QuestionGroup> QuestionGroups { get; set; } = new List<QuestionGroup>();
|
|
||||||
//}
|
|
||||||
|
|
||||||
//[XmlRoot("QG")]
|
|
||||||
//public class QuestionGroup
|
|
||||||
//{
|
|
||||||
// // JSON 特性
|
|
||||||
// [JsonProperty("题号")]
|
|
||||||
// // XML 特性:作为 <QG Id="X"> 属性
|
|
||||||
// [XmlAttribute("Id")]
|
|
||||||
// public byte Id { get; set; }
|
|
||||||
|
|
||||||
// [JsonProperty("标题")]
|
|
||||||
// [XmlElement("T")] // T for Title
|
|
||||||
// public string Title { get; set; }
|
|
||||||
|
|
||||||
// [JsonProperty("分值")]
|
|
||||||
// [XmlAttribute("S")] // S for Score
|
|
||||||
// public int Score { get; set; }
|
|
||||||
|
|
||||||
// [JsonProperty("题目引用")]
|
|
||||||
// [XmlElement("QR")] // QR for QuestionReference,作为 <QR> 元素
|
|
||||||
// public string QuestionReference { get; set; } = ""; // 初始化为空字符串
|
|
||||||
|
|
||||||
// [JsonProperty("子题目")]
|
|
||||||
// [XmlArray("SQs")] // SQs 包含 <SQ> 列表
|
|
||||||
// [XmlArrayItem("SQ")]
|
|
||||||
// public List<SubQuestion> SubQuestions { get; set; } = new List<SubQuestion>();
|
|
||||||
|
|
||||||
// [JsonProperty("子题组")]
|
|
||||||
// [XmlArray("SQGs")] // SQGs 包含 <QG> 列表 (嵌套题组)
|
|
||||||
// [XmlArrayItem("QG")]
|
|
||||||
// public List<QuestionGroup> SubQuestionGroups { get; set; } = new List<QuestionGroup>();
|
|
||||||
//}
|
|
||||||
|
|
||||||
//// 子题目类
|
|
||||||
//public class SubQuestion
|
|
||||||
//{
|
|
||||||
|
|
||||||
// [JsonProperty("子题号")]
|
|
||||||
// [XmlAttribute("Id")] // Id for SubId
|
|
||||||
// public byte SubId { get; set; }
|
|
||||||
|
|
||||||
// [JsonProperty("题干")]
|
|
||||||
// [XmlElement("T")] // T for Text (Stem)
|
|
||||||
// public string Stem { get; set; }
|
|
||||||
|
|
||||||
// [JsonProperty("分值")]
|
|
||||||
// [XmlAttribute("S")] // S for Score
|
|
||||||
// public int Score { get; set; } // 分值通常为整数
|
|
||||||
|
|
||||||
// [JsonProperty("选项")]
|
|
||||||
// [XmlArray("Os")] // Os 包含 <O> 列表
|
|
||||||
// [XmlArrayItem("O")]
|
|
||||||
// public List<Option> Options { get; set; } = new List<Option>();
|
|
||||||
|
|
||||||
// [JsonProperty("示例答案")]
|
|
||||||
// [XmlElement("SA")] // SA for SampleAnswer
|
|
||||||
// public string SampleAnswer { get; set; } = "";
|
|
||||||
//}
|
|
||||||
|
|
||||||
//// 选项类,用于适配 <O V="X"/> 结构
|
|
||||||
//public class Option
|
|
||||||
//{
|
|
||||||
// // XML 特性:作为 <O V="X"> 属性
|
|
||||||
// [XmlAttribute("V")] // V for Value
|
|
||||||
// // JSON 特性:如果 JSON 中的选项是 {"Value": "A"} 这样的对象,则需要 JsonProperty("Value")
|
|
||||||
// // 但如果 JSON 选项只是 ["A", "B"] 这样的字符串数组,则此Option类不适合JSON Options
|
|
||||||
// // 需要明确你的JSON Options的结构。我假设你JSON Options是 List<string>
|
|
||||||
// // 如果是 List<string>,则Options属性在SubQuestion中直接是List<string>,Option类则不需要
|
|
||||||
// // 但根据你的精简XML需求,Option类是必要的。
|
|
||||||
// // 所以这里需要你自己根据实际JSON Options结构选择。
|
|
||||||
// // 为了兼容XML,我会保留Option类,但如果JSON是List<string>,Options属性会很复杂
|
|
||||||
// public string Value { get; set; }
|
|
||||||
//}
|
|
||||||
|
|
||||||
//// 独立的服务类来处理序列化和反序列化
|
|
||||||
//public static class ExamParser
|
|
||||||
//{
|
|
||||||
// // JSON 反序列化方法
|
|
||||||
// public static List<T> ParseExamJson<T>(string jsonContent)
|
|
||||||
// {
|
|
||||||
// string cleanedJson = jsonContent.Trim();
|
|
||||||
|
|
||||||
// // 移除可能存在的 Markdown 代码块标记
|
|
||||||
// if (cleanedJson.StartsWith("```json") && cleanedJson.EndsWith("```"))
|
|
||||||
// {
|
|
||||||
// cleanedJson = cleanedJson.Substring("```json".Length, cleanedJson.Length - "```json".Length - "```".Length).Trim();
|
|
||||||
// }
|
|
||||||
// // 移除可能存在的单引号包围(如果 AI 偶尔会这样输出)
|
|
||||||
// if (cleanedJson.StartsWith("'") && cleanedJson.EndsWith("'"))
|
|
||||||
// {
|
|
||||||
// cleanedJson = cleanedJson.Substring(1, cleanedJson.Length - 2).Trim();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// try
|
|
||||||
// {
|
|
||||||
// // 注意:这里假设你的 JSON 根直接是一个 QuestionGroup 列表
|
|
||||||
// // 如果你的 JSON 根是 { "QuestionGroups": [...] },则需要先反序列化到 ExamPaper
|
|
||||||
// List<T> examQuestions = JsonConvert.DeserializeObject<List<T>>(cleanedJson);
|
|
||||||
// return examQuestions;
|
|
||||||
// }
|
|
||||||
// catch (JsonSerializationException ex)
|
|
||||||
// {
|
|
||||||
// Console.WriteLine($"JSON 反序列化错误: {ex.Message}");
|
|
||||||
// Console.WriteLine($"内部异常: {ex.InnerException?.Message}");
|
|
||||||
// return null;
|
|
||||||
// }
|
|
||||||
// catch (Exception ex)
|
|
||||||
// {
|
|
||||||
// Console.WriteLine($"处理错误: {ex.Message}");
|
|
||||||
// return null;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// #region TEST
|
|
||||||
// [XmlRoot("User")]
|
|
||||||
// public class User
|
|
||||||
// {
|
|
||||||
// [XmlAttribute("id")]
|
|
||||||
// public string Id { get; set; }
|
|
||||||
|
|
||||||
// [XmlElement("PersonalInfo")]
|
|
||||||
// public PersonalInfo PersonalInfo { get; set; }
|
|
||||||
|
|
||||||
// [XmlArray("Roles")] // 包装元素 <Roles>
|
|
||||||
// [XmlArrayItem("Role")] // 集合中的每个项是 <Role>
|
|
||||||
// public List<Role> Roles { get; set; } = new List<Role>();
|
|
||||||
|
|
||||||
// // 构造函数,方便测试
|
|
||||||
// public User() { }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public class PersonalInfo
|
|
||||||
// {
|
|
||||||
// [XmlElement("FullName")]
|
|
||||||
// public string FullName { get; set; }
|
|
||||||
|
|
||||||
// [XmlElement("EmailAddress")]
|
|
||||||
// public string EmailAddress { get; set; }
|
|
||||||
|
|
||||||
// // 构造函数,方便测试
|
|
||||||
// public PersonalInfo() { }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public class Role
|
|
||||||
// {
|
|
||||||
// [XmlAttribute("type")]
|
|
||||||
// public string Type { get; set; }
|
|
||||||
|
|
||||||
// // 构造函数,方便测试
|
|
||||||
// public Role() { }
|
|
||||||
// }
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
// // XML 反序列化方法
|
|
||||||
// public static T ParseExamXml<T>(string xmlContent)
|
|
||||||
// {
|
|
||||||
// string cleanedXml = xmlContent.Trim();
|
|
||||||
// if (cleanedXml.StartsWith("'") && cleanedXml.EndsWith("'"))
|
|
||||||
// {
|
|
||||||
// cleanedXml = cleanedXml.Substring(1, cleanedXml.Length - 2);
|
|
||||||
// }
|
|
||||||
// if (cleanedXml.StartsWith("```xml") && cleanedXml.EndsWith("```"))
|
|
||||||
// {
|
|
||||||
// cleanedXml = cleanedXml.Substring("```xml".Length, cleanedXml.Length - "```xml".Length - "```".Length).Trim();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// XmlSerializer serializer = new XmlSerializer(typeof(T));
|
|
||||||
|
|
||||||
// using (StringReader reader = new StringReader(cleanedXml))
|
|
||||||
// {
|
|
||||||
// try
|
|
||||||
// {
|
|
||||||
// T user = (T)serializer.Deserialize(reader);
|
|
||||||
// return user;
|
|
||||||
// }
|
|
||||||
// catch (InvalidOperationException ex)
|
|
||||||
// {
|
|
||||||
// Console.WriteLine($"XML 反序列化操作错误: {ex.Message}");
|
|
||||||
// Console.WriteLine($"内部异常: {ex.InnerException?.Message}");
|
|
||||||
// return default(T);
|
|
||||||
// }
|
|
||||||
// catch (Exception ex)
|
|
||||||
// {
|
|
||||||
// Console.WriteLine($"处理错误: {ex.Message}");
|
|
||||||
// return default(T);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public static List<QuestionGroup> ParseExamXmlFormQG(string xmlContent)
|
|
||||||
// {
|
|
||||||
// // 移除可能存在的 Markdown 代码块标记
|
|
||||||
// if (xmlContent.StartsWith("```xml") && xmlContent.EndsWith("```"))
|
|
||||||
// {
|
|
||||||
// xmlContent = xmlContent.Substring("```xml".Length, xmlContent.Length - "```xml".Length - "```".Length).Trim();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// var serializer = new XmlSerializer(typeof(List<QuestionGroup>), new XmlRootAttribute("QGs"));
|
|
||||||
|
|
||||||
// using (StringReader reader = new StringReader(xmlContent))
|
|
||||||
// {
|
|
||||||
// try
|
|
||||||
// {
|
|
||||||
// List<QuestionGroup> questionGroups = (List<QuestionGroup>)serializer.Deserialize(reader);
|
|
||||||
// return questionGroups;
|
|
||||||
// }
|
|
||||||
// catch (InvalidOperationException ex)
|
|
||||||
// {
|
|
||||||
// Console.WriteLine($"XML 反序列化操作错误: {ex.Message}");
|
|
||||||
// Console.WriteLine($"内部异常: {ex.InnerException?.Message}");
|
|
||||||
// return null;
|
|
||||||
// }
|
|
||||||
// catch (Exception ex)
|
|
||||||
// {
|
|
||||||
// Console.WriteLine($"处理错误: {ex.Message}");
|
|
||||||
// return null;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
}
|
}
|
@@ -1,233 +1,36 @@
|
|||||||
using Entities.DTO;
|
using Entities.DTO;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using Entities.Contracts;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using AutoMapper;
|
||||||
|
|
||||||
namespace TechHelper.Client.Exam
|
namespace TechHelper.Client.Exam
|
||||||
{
|
{
|
||||||
public static class ExamPaperExtensions
|
public static class AssignmentExtensions
|
||||||
{
|
{
|
||||||
public static ExamDto ConvertToExamDTO(this ExamPaper examPaper)
|
|
||||||
{
|
|
||||||
ExamDto dto = new ExamDto();
|
|
||||||
|
|
||||||
dto.AssignmentTitle = examPaper.AssignmentTitle;
|
public static List<string> ParseOptionsFromText(this string optionsText)
|
||||||
dto.Description = examPaper.Description;
|
|
||||||
dto.SubjectArea = examPaper.SubjectArea;
|
|
||||||
dto.QuestionGroups.Title = examPaper.AssignmentTitle;
|
|
||||||
dto.QuestionGroups.Descript = examPaper.Description;
|
|
||||||
|
|
||||||
foreach (var qg in examPaper.QuestionGroups)
|
|
||||||
{
|
{
|
||||||
var qgd = new QuestionGroupDto();
|
return optionsText.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None)
|
||||||
ParseMajorQuestionGroup(qg, qgd, false);
|
.Where(line => !string.IsNullOrWhiteSpace(line)).ToList();
|
||||||
dto.QuestionGroups.SubQuestionGroups.Add(qgd);
|
}
|
||||||
|
|
||||||
|
public static void SeqIndex(this AssignmentDto dto)
|
||||||
|
{
|
||||||
|
dto.ExamStruct.SeqQGroupIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
foreach (var question in examPaper.TopLevelQuestions)
|
public static void SeqQGroupIndex(this AssignmentQuestionDto dto)
|
||||||
{
|
{
|
||||||
if (question.SubQuestions != null && question.SubQuestions.Any())
|
|
||||||
|
foreach (var sqg in dto.ChildrenAssignmentQuestion)
|
||||||
{
|
{
|
||||||
var qgDto = new QuestionGroupDto
|
sqg.Index = (byte)(dto.ChildrenAssignmentQuestion.IndexOf(sqg) + 1);
|
||||||
{
|
|
||||||
Title = question.Stem,
|
|
||||||
Score = (int)question.Score,
|
|
||||||
Descript = "",
|
|
||||||
};
|
|
||||||
|
|
||||||
qgDto.ValidQuestionGroup = !string.IsNullOrEmpty(qgDto.Descript);
|
|
||||||
|
|
||||||
|
|
||||||
ParseQuestionWithSubQuestions(question, qgDto, qgDto.ValidQuestionGroup);
|
|
||||||
dto.QuestionGroups.SubQuestionGroups.Add(qgDto);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var qgDto = new QuestionGroupDto
|
|
||||||
{
|
|
||||||
Title = question.Stem,
|
|
||||||
Score = (int)question.Score,
|
|
||||||
Descript = "",
|
|
||||||
};
|
|
||||||
|
|
||||||
qgDto.ValidQuestionGroup = !string.IsNullOrEmpty(qgDto.Descript);
|
|
||||||
|
|
||||||
var subQuestionDto = new SubQuestionDto();
|
|
||||||
ParseSingleQuestion(question, subQuestionDto, !qgDto.ValidQuestionGroup);
|
|
||||||
qgDto.SubQuestions.Add(subQuestionDto);
|
|
||||||
dto.QuestionGroups.SubQuestionGroups.Add(qgDto);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dto;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ParseMajorQuestionGroup(MajorQuestionGroup qg, QuestionGroupDto qgd, bool isParentGroupValidChain)
|
|
||||||
{
|
|
||||||
qgd.Title = qg.Title;
|
|
||||||
qgd.Score = (int)qg.Score;
|
|
||||||
qgd.Descript = qg.Descript;
|
|
||||||
|
|
||||||
|
|
||||||
qgd.ValidQuestionGroup = !string.IsNullOrEmpty(qg.Descript) && !isParentGroupValidChain;
|
|
||||||
|
|
||||||
|
|
||||||
bool nextIsParentGroupValidChain = qgd.ValidQuestionGroup || isParentGroupValidChain;
|
|
||||||
|
|
||||||
|
|
||||||
if (qg.SubQuestionGroups != null)
|
|
||||||
{
|
|
||||||
qg.SubQuestionGroups.ForEach(sqg =>
|
|
||||||
{
|
|
||||||
var sqgd = new QuestionGroupDto();
|
|
||||||
sqgd.Index = (byte)qg.SubQuestionGroups.IndexOf(sqg);
|
|
||||||
ParseMajorQuestionGroup(sqg, sqgd, nextIsParentGroupValidChain);
|
|
||||||
qgd.SubQuestionGroups.Add(sqgd);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理 MajorQuestionGroup 下的 SubQuestions
|
|
||||||
if (qg.SubQuestions != null)
|
|
||||||
{
|
|
||||||
qg.SubQuestions.ForEach(sq =>
|
|
||||||
{
|
|
||||||
// 如果 MajorQuestionGroup 下的 Question 包含子问题,则转为 QuestionGroupDto
|
|
||||||
if (sq.SubQuestions != null && sq.SubQuestions.Any())
|
|
||||||
{
|
|
||||||
var subQgd = new QuestionGroupDto
|
|
||||||
{
|
|
||||||
Title = sq.Stem,
|
|
||||||
Index = (byte)qg.SubQuestions.IndexOf(sq),
|
|
||||||
Score = (int)sq.Score,
|
|
||||||
Descript = "" // 默认为空
|
|
||||||
};
|
|
||||||
// 判断当前组是否有效:如果有描述,并且其父级链中没有任何一个组是有效组,则当前组有效
|
|
||||||
subQgd.ValidQuestionGroup = !string.IsNullOrEmpty(subQgd.Descript) && !nextIsParentGroupValidChain;
|
|
||||||
|
|
||||||
ParseQuestionWithSubQuestions(sq, subQgd, subQgd.ValidQuestionGroup || nextIsParentGroupValidChain);
|
|
||||||
qgd.SubQuestionGroups.Add(subQgd);
|
|
||||||
}
|
|
||||||
else // 如果 MajorQuestionGroup 下的 Question 没有子问题,则转为 SubQuestionDto
|
|
||||||
{
|
|
||||||
var subQd = new SubQuestionDto();
|
|
||||||
// 只有当所有父组(包括当前组)都不是有效组时,这个题目才有效
|
|
||||||
ParseSingleQuestion(sq, subQd, !nextIsParentGroupValidChain);
|
|
||||||
subQd.Index = (byte)qg.SubQuestions.IndexOf(sq);
|
|
||||||
qgd.SubQuestions.Add(subQd);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析包含子问题的 Question,将其转换为 QuestionGroupDto
|
|
||||||
// isParentGroupValidChain 参数表示从顶层到当前组的任一父组是否已经是“有效组”
|
|
||||||
private static void ParseQuestionWithSubQuestions(Question question, QuestionGroupDto qgd, bool isParentGroupValidChain)
|
|
||||||
{
|
|
||||||
qgd.Title = question.Stem;
|
|
||||||
qgd.Score = (int)question.Score;
|
|
||||||
qgd.Descript = ""; // 默认为空
|
|
||||||
|
|
||||||
// 判断当前组是否有效:如果有描述,并且其父级链中没有任何一个组是有效组,则当前组有效
|
|
||||||
qgd.ValidQuestionGroup = !string.IsNullOrEmpty(qgd.Descript) && !isParentGroupValidChain;
|
|
||||||
|
|
||||||
// 更新传递给子项的 isParentGroupValidChain 状态
|
|
||||||
bool nextIsParentGroupValidChain = qgd.ValidQuestionGroup || isParentGroupValidChain;
|
|
||||||
|
|
||||||
|
|
||||||
if (question.SubQuestions != null)
|
|
||||||
{
|
|
||||||
question.SubQuestions.ForEach(subQ =>
|
|
||||||
{
|
|
||||||
// 如果子问题本身还有子问题(多层嵌套),则继续创建 QuestionGroupDto
|
|
||||||
if (subQ.SubQuestions != null && subQ.SubQuestions.Any())
|
|
||||||
{
|
|
||||||
var nestedQgd = new QuestionGroupDto
|
|
||||||
{
|
|
||||||
Title = subQ.Stem,
|
|
||||||
Score = (int)subQ.Score,
|
|
||||||
Descript = "" // 默认为空
|
|
||||||
};
|
|
||||||
// 判断当前组是否有效:如果有描述,并且其父级链中没有任何一个组是有效组,则当前组有效
|
|
||||||
nestedQgd.ValidQuestionGroup = !string.IsNullOrEmpty(nestedQgd.Descript) && !nextIsParentGroupValidChain;
|
|
||||||
|
|
||||||
ParseQuestionWithSubQuestions(subQ, nestedQgd, nestedQgd.ValidQuestionGroup || nextIsParentGroupValidChain);
|
|
||||||
qgd.SubQuestionGroups.Add(nestedQgd);
|
|
||||||
}
|
|
||||||
else // 如果子问题没有子问题,则直接创建 SubQuestionDto
|
|
||||||
{
|
|
||||||
var subQd = new SubQuestionDto();
|
|
||||||
// 只有当所有父组(包括当前组)都不是有效组时,这个题目才有效
|
|
||||||
ParseSingleQuestion(subQ, subQd, !nextIsParentGroupValidChain);
|
|
||||||
qgd.SubQuestions.Add(subQd);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析单个 Question (没有子问题) 为 SubQuestionDto
|
|
||||||
private static void ParseSingleQuestion(Question question, SubQuestionDto subQd, bool validQ)
|
|
||||||
{
|
|
||||||
subQd.Stem = question.Stem;
|
|
||||||
subQd.Score = (int)question.Score;
|
|
||||||
subQd.ValidQuestion = validQ; // 根据传入的 validQ 确定是否是“有效题目”
|
|
||||||
subQd.SampleAnswer = question.SampleAnswer;
|
|
||||||
subQd.QuestionType = question.QuestionType;
|
|
||||||
// 注意:DifficultyLevel 在本地 Question 中没有,如果服务器需要,可能需要补充默认值或从其他地方获取
|
|
||||||
// subQd.DifficultyLevel = ...;
|
|
||||||
|
|
||||||
if (question.Options != null)
|
|
||||||
{
|
|
||||||
question.Options.ForEach(o =>
|
|
||||||
{
|
|
||||||
subQd.Options.Add(new OptionDto { Value = o.Label + o.Text });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static void SeqIndex(this ExamDto dto)
|
|
||||||
{
|
|
||||||
dto.QuestionGroups.SeqQGroupIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static void SeqQGroupIndex(this QuestionGroupDto dto)
|
|
||||||
{
|
|
||||||
dto.SubQuestions?.ForEach(sq =>
|
|
||||||
{
|
|
||||||
sq.Index = (byte)dto.SubQuestions.IndexOf(sq);
|
|
||||||
});
|
|
||||||
|
|
||||||
dto.SubQuestionGroups?.ForEach(sqg =>
|
|
||||||
{
|
|
||||||
sqg.Index = (byte)dto.SubQuestionGroups.IndexOf(sqg);
|
|
||||||
sqg.SeqQGroupIndex();
|
sqg.SeqQGroupIndex();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static string SerializeExamDto(this ExamDto dto)
|
|
||||||
{
|
|
||||||
// 配置序列化选项(可选)
|
|
||||||
var options = new JsonSerializerOptions
|
|
||||||
{
|
|
||||||
WriteIndented = true,
|
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
||||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
|
||||||
};
|
|
||||||
|
|
||||||
return JsonSerializer.Serialize(dto, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ExamDto DeserializeExamDto(string jsonString)
|
|
||||||
{
|
|
||||||
|
|
||||||
var options = new JsonSerializerOptions
|
|
||||||
{
|
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
|
||||||
};
|
|
||||||
|
|
||||||
return JsonSerializer.Deserialize<ExamDto>(jsonString, options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,19 +1,25 @@
|
|||||||
using Entities.DTO;
|
using Entities.Contracts; // 假设这些实体合约仍然是必需的
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace TechHelper.Client.Exam
|
namespace TechHelper.Client.Exam
|
||||||
{
|
{
|
||||||
// --- 新增错误处理相关类 ---
|
public enum ParseErrorType
|
||||||
|
{
|
||||||
|
Validation = 1,
|
||||||
|
DataParsing = 2,
|
||||||
|
Structural = 3,
|
||||||
|
RegexMatchIssue = 4,
|
||||||
|
UnexpectedError = 5
|
||||||
|
}
|
||||||
|
|
||||||
public class ParseError
|
public class ParseError
|
||||||
{
|
{
|
||||||
public ParseErrorType Type { get; }
|
public ParseErrorType Type { get; }
|
||||||
public string Message { get; }
|
public string Message { get; }
|
||||||
public int? Index { get; } // 错误发生的文本索引或匹配项索引
|
public int? Index { get; }
|
||||||
public string MatchedText { get; } // 如果与某个匹配项相关,记录其文本
|
public string MatchedText { get; }
|
||||||
public Exception InnerException { get; } // 捕获到的原始异常
|
public Exception InnerException { get; }
|
||||||
|
|
||||||
public ParseError(ParseErrorType type, string message, int? index = null, string matchedText = null, Exception innerException = null)
|
public ParseError(ParseErrorType type, string message, int? index = null, string matchedText = null, Exception innerException = null)
|
||||||
{
|
{
|
||||||
@@ -26,7 +32,7 @@ namespace TechHelper.Client.Exam
|
|||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
var sb = new System.Text.StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.Append($"[{Type}] {Message}");
|
sb.Append($"[{Type}] {Message}");
|
||||||
if (Index.HasValue) sb.Append($" (Index: {Index.Value})");
|
if (Index.HasValue) sb.Append($" (Index: {Index.Value})");
|
||||||
if (!string.IsNullOrEmpty(MatchedText)) sb.Append($" (MatchedText: '{MatchedText}')");
|
if (!string.IsNullOrEmpty(MatchedText)) sb.Append($" (MatchedText: '{MatchedText}')");
|
||||||
@@ -35,46 +41,34 @@ namespace TechHelper.Client.Exam
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ParseErrorType
|
public class AssignmentEx
|
||||||
{
|
{
|
||||||
Validation = 1, // 输入验证失败
|
public string Title { get; set; } = "Title";
|
||||||
DataParsing = 2, // 数据解析失败(如数字转换)
|
public string Description { get; set; } = "Description";
|
||||||
Structural = 3, // 结构性问题(如选项没有对应的问题)
|
public SubjectAreaEnum SubjectArea { get; set; } = SubjectAreaEnum.Unknown;
|
||||||
RegexMatchIssue = 4, // 正则表达式匹配结果不符合预期
|
public AssignmentQuestionEx ExamStruct { get; set; } = new AssignmentQuestionEx();
|
||||||
UnexpectedError = 5 // 未预料到的通用错误
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public class ExamPaper
|
|
||||||
{
|
|
||||||
public string AssignmentTitle { get; set; } = "未识别试卷标题";
|
|
||||||
public string Description { get; set; } = "未识别试卷描述";
|
|
||||||
public string SubjectArea { get; set; } = "试卷类别";
|
|
||||||
public List<MajorQuestionGroup> QuestionGroups { get; set; } = new List<MajorQuestionGroup>();
|
|
||||||
public List<Question> TopLevelQuestions { get; set; } = new List<Question>();
|
|
||||||
public List<ParseError> Errors { get; set; } = new List<ParseError>();
|
public List<ParseError> Errors { get; set; } = new List<ParseError>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MajorQuestionGroup
|
// 试题的包裹器, 或者 单独作为一个试题结构存在
|
||||||
|
public class AssignmentQuestionEx
|
||||||
{
|
{
|
||||||
public string Title { get; set; } = string.Empty;
|
public string Title { get; set; } = string.Empty;
|
||||||
public string Descript { get; set; } = string.Empty;
|
public string Description { get; set; } = string.Empty;
|
||||||
|
public byte Index { get; set; } = 0;
|
||||||
public float Score { get; set; }
|
public float Score { get; set; }
|
||||||
public List<MajorQuestionGroup> SubQuestionGroups { get; set; } = new List<MajorQuestionGroup>();
|
public string Sequence { get; set; } = string.Empty;
|
||||||
public List<Question> SubQuestions { get; set; } = new List<Question>();
|
public QuestionEx? Question { get; set; }
|
||||||
|
public AssignmentStructType Type { get; set; }
|
||||||
|
public List<AssignmentQuestionEx> ChildrenAssignmentQuestion { get; set; } = new List<AssignmentQuestionEx>();
|
||||||
public int Priority { get; set; }
|
public int Priority { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Question
|
public class QuestionEx
|
||||||
{
|
{
|
||||||
public string Number { get; set; } = string.Empty;
|
public string Title { get; set; } = string.Empty;
|
||||||
public string Stem { get; set; } = string.Empty;
|
public string Answer { get; set; } = string.Empty;
|
||||||
public float Score { get; set; }
|
|
||||||
public List<Option> Options { get; set; } = new List<Option>();
|
public List<Option> Options { get; set; } = new List<Option>();
|
||||||
public List<Question> SubQuestions { get; set; } = new List<Question>();
|
|
||||||
public string SampleAnswer { get; set; } = string.Empty;
|
|
||||||
public string QuestionType { get; set; } = string.Empty;
|
|
||||||
public int Priority { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Option
|
public class Option
|
||||||
@@ -89,105 +83,96 @@ namespace TechHelper.Client.Exam
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class RegexPatternConfig
|
public class RegexPatternConfig
|
||||||
{
|
{
|
||||||
public string Pattern { get; set; } // 正则表达式字符串
|
public string Pattern { get; set; }
|
||||||
public int Priority { get; set; } // 优先级,数字越小优先级越高
|
public int Priority { get; set; }
|
||||||
public Regex Regex { get; private set; } // 编译后的Regex对象,用于性能优化
|
public AssignmentStructType Type { get; set; }
|
||||||
|
public Regex Regex { get; private set; }
|
||||||
|
|
||||||
public RegexPatternConfig(string pattern, int priority)
|
public RegexPatternConfig(string pattern, int priority, AssignmentStructType type = AssignmentStructType.Question)
|
||||||
{
|
{
|
||||||
Pattern = pattern;
|
Pattern = pattern;
|
||||||
Priority = priority;
|
Priority = priority;
|
||||||
Regex = new Regex(pattern, RegexOptions.Multiline | RegexOptions.Compiled); // 多行模式,编译以提高性能
|
Type = type;
|
||||||
|
Regex = new Regex(pattern, RegexOptions.Multiline | RegexOptions.Compiled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ExamParserEnum
|
|
||||||
{
|
|
||||||
MajorQuestionGroupPatterns = 0,
|
|
||||||
QuestionPatterns,
|
|
||||||
OptionPatterns
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 试卷解析的配置类,包含所有正则表达式
|
/// 试卷解析的配置类,包含所有正则表达式
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ExamParserConfig
|
public class ExamParserConfig
|
||||||
{
|
{
|
||||||
public List<RegexPatternConfig> MajorQuestionGroupPatterns { get; set; } = new List<RegexPatternConfig>();
|
|
||||||
public List<RegexPatternConfig> QuestionPatterns { get; set; } = new List<RegexPatternConfig>();
|
public List<RegexPatternConfig> QuestionPatterns { get; set; } = new List<RegexPatternConfig>();
|
||||||
public List<RegexPatternConfig> OptionPatterns { get; set; } = new List<RegexPatternConfig>();
|
public List<RegexPatternConfig> OptionPatterns { get; set; } = new List<RegexPatternConfig>();
|
||||||
|
public Regex ScoreRegex { get; private set; } // 独立的得分正则表达式
|
||||||
|
|
||||||
public ExamParserConfig()
|
public ExamParserConfig()
|
||||||
{
|
{
|
||||||
MajorQuestionGroupPatterns.Add(new RegexPatternConfig(@"^([一二三四五六七八九十]+)[、\.]\s*(.+?)(?:\s*\(((\d+(?:\.\d+)?))\s*分\))?\s*$", 1));
|
// 题目/题组模式:只匹配行开头,并按优先级区分
|
||||||
MajorQuestionGroupPatterns.Add(new RegexPatternConfig(@"^\(([一二三四五六七八九十]{1,2}|十[一二三四五六七八九])\)\s*(.+?)(?:\s*\(((\d+(?:\.\d+)?))\s*分\))?\s*$", 2));
|
// Group 1: 编号部分
|
||||||
|
// Group 2: 题目/题组标题内容
|
||||||
|
|
||||||
|
// 例如:一. 这是大题一
|
||||||
|
QuestionPatterns.Add(new RegexPatternConfig(@"^([一二三四五六七八九十]+)[.\、]\s*(.+)", 1, AssignmentStructType.Struct));
|
||||||
|
|
||||||
|
// 例如:(一) 这是第一子题组
|
||||||
|
QuestionPatterns.Add(new RegexPatternConfig(@"^\(([一二三四五六七八九十]{1,2}|十[一二三四五六七八九])\)\s*(.+)", 2, AssignmentStructType.Group));
|
||||||
|
|
||||||
|
// 例如:1. 这是第一道题目 或 1 这是第一道题目
|
||||||
|
QuestionPatterns.Add(new RegexPatternConfig(@"^(\d+)\.?\s*(.+)", 3, AssignmentStructType.Question));
|
||||||
|
|
||||||
|
// 例如:(1). 这是小问一 或 (1) 这是小问一
|
||||||
|
QuestionPatterns.Add(new RegexPatternConfig(@"^\((\d+)\)\.?\s*(.+)", 4, AssignmentStructType.Question));
|
||||||
|
|
||||||
|
// 例如:① 这是另一种小问 或 ①. 这是另一种小问 (如果 ① 后面会跟点,这个更通用)
|
||||||
|
// 如果 ① 后面通常没有点,但您希望它也能匹配,则保留原样或根据实际情况调整
|
||||||
|
QuestionPatterns.Add(new RegexPatternConfig(@"^[①②③④⑤⑥⑦⑧⑨⑩]+\.?\s*(.+)", 5, AssignmentStructType.Question));
|
||||||
|
|
||||||
|
|
||||||
// 模式 1: "1. 这是一个题目 (5分)" 或 "1. 这是一个题目"
|
|
||||||
QuestionPatterns.Add(new RegexPatternConfig(@"^(\d+)\.\s*(.+?)(?:\s*\(((\d+(?:\.\d+)?))\s*分\))?\s*$", 1));
|
|
||||||
|
|
||||||
// 模式 2: "(1) 这是一个子题目 (3分)" 或 "(1) 这是一个子题目"
|
// 选项模式 (保持不变,使用 AssignmentStructType.Option 区分)
|
||||||
QuestionPatterns.Add(new RegexPatternConfig(@"^\((\d+)\)\s*(.+?)(?:\s*\(((\d+(?:\.\d+)?))\s*分\))?\s*$", 2));
|
OptionPatterns.Add(new RegexPatternConfig(@"([A-Z]\.)\s*(.*?)(?=[A-Z]\.|$)", 1, AssignmentStructType.Option));
|
||||||
|
OptionPatterns.Add(new RegexPatternConfig(@"([a-z]\.)\s*(.*?)(?=[a-z]\.|$)", 2, AssignmentStructType.Option));
|
||||||
// 模式 3: "① 这是一个更深层次的子题目 (2分)" 或 "① 这是一个更深层次的子题目"
|
|
||||||
QuestionPatterns.Add(new RegexPatternConfig(@"^[①②③④⑤⑥⑦⑧⑨⑩]+\s*(.+?)(?:\s*\(((\d+(?:\.\d+)?))\s*分\))?\s*$", 3));
|
|
||||||
|
|
||||||
|
|
||||||
OptionPatterns.Add(new RegexPatternConfig(@"([A-Z]\.)\s*(.*?)(?=[A-Z]\.|$)", 1)); // 大写字母选项
|
|
||||||
OptionPatterns.Add(new RegexPatternConfig(@"([a-z]\.)\s*(.*?)(?=[a-z]\.|$)", 1)); // 小写字母选项
|
|
||||||
|
|
||||||
|
// 独立的得分正则表达式:匹配行末尾的 "(X分)" 格式
|
||||||
|
// Group 1: 捕获分数(如 "10" 或 "0.5")
|
||||||
|
ScoreRegex = new Regex(@"(?:\s*\(((\d+(?:\.\d+)?))\s*分\)\s*$)", RegexOptions.Multiline | RegexOptions.Compiled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public class PotentialMatch
|
public class PotentialMatch
|
||||||
{
|
{
|
||||||
public int StartIndex { get; set; }
|
public int StartIndex { get; set; }
|
||||||
public int EndIndex { get; set; } // 匹配到的结构在原始文本中的结束位置
|
public int EndIndex { get; set; }
|
||||||
public string MatchedText { get; set; } // 匹配到的完整行或段落
|
public string MatchedText { get; set; }
|
||||||
public Match RegexMatch { get; set; } // 原始的Regex.Match对象,方便获取捕获组
|
public Match RegexMatch { get; set; }
|
||||||
public RegexPatternConfig PatternConfig { get; set; } // 匹配到的模式配置
|
public RegexPatternConfig PatternConfig { get; set; }
|
||||||
public MatchType Type { get; set; } // 枚举:MajorQuestionGroup, Question, Option, etc.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum MatchType
|
|
||||||
{
|
|
||||||
MajorQuestionGroup,
|
|
||||||
Question,
|
|
||||||
Option,
|
|
||||||
Other // 如果有其他需要识别的类型
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 负责扫描原始文本,收集所有潜在的匹配项(题组、题目、选项)。
|
|
||||||
/// 它只进行匹配,不进行结构化归属。
|
|
||||||
/// </summary>
|
|
||||||
public class ExamDocumentScanner
|
public class ExamDocumentScanner
|
||||||
{
|
{
|
||||||
private readonly ExamParserConfig _config;
|
private readonly ExamParserConfig _config;
|
||||||
|
|
||||||
public ExamDocumentScanner(ExamParserConfig config)
|
public ExamDocumentScanner(ExamParserConfig config)
|
||||||
{
|
{
|
||||||
_config = config ?? throw new ArgumentNullException(nameof(config)); // 确保配置不为空
|
_config = config ?? throw new ArgumentNullException(nameof(config));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public List<PotentialMatch> Scan(string text, List<ParseError> errors)
|
||||||
/// 扫描给定的文本,返回所有潜在的匹配项,并按起始位置排序。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="text">要扫描的文本</param>
|
|
||||||
/// <returns>所有匹配到的 PotentialMatch 列表</returns>
|
|
||||||
public List<PotentialMatch> Scan(string text)
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(text))
|
if (string.IsNullOrEmpty(text))
|
||||||
{
|
{
|
||||||
return new List<PotentialMatch>(); // 对于空文本,直接返回空列表
|
return new List<PotentialMatch>();
|
||||||
}
|
}
|
||||||
|
|
||||||
var allPotentialMatches = new List<PotentialMatch>();
|
var allPotentialMatches = new List<PotentialMatch>();
|
||||||
|
var allPatternConfigs = new List<RegexPatternConfig>();
|
||||||
|
allPatternConfigs.AddRange(_config.QuestionPatterns);
|
||||||
|
allPatternConfigs.AddRange(_config.OptionPatterns);
|
||||||
|
|
||||||
// 扫描所有题组模式
|
foreach (var patternConfig in allPatternConfigs)
|
||||||
foreach (var patternConfig in _config.MajorQuestionGroupPatterns)
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
foreach (Match match in patternConfig.Regex.Matches(text))
|
foreach (Match match in patternConfig.Regex.Matches(text))
|
||||||
{
|
{
|
||||||
@@ -198,46 +183,16 @@ namespace TechHelper.Client.Exam
|
|||||||
MatchedText = match.Value,
|
MatchedText = match.Value,
|
||||||
RegexMatch = match,
|
RegexMatch = match,
|
||||||
PatternConfig = patternConfig,
|
PatternConfig = patternConfig,
|
||||||
Type = MatchType.MajorQuestionGroup
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
// 扫描所有题目模式
|
|
||||||
foreach (var patternConfig in _config.QuestionPatterns)
|
|
||||||
{
|
{
|
||||||
foreach (Match match in patternConfig.Regex.Matches(text))
|
errors.Add(new ParseError(ParseErrorType.UnexpectedError,
|
||||||
{
|
$"An error occurred during regex matching for pattern: '{patternConfig.Pattern}'.",
|
||||||
allPotentialMatches.Add(new PotentialMatch
|
innerException: ex));
|
||||||
{
|
|
||||||
StartIndex = match.Index,
|
|
||||||
EndIndex = match.Index + match.Length,
|
|
||||||
MatchedText = match.Value,
|
|
||||||
RegexMatch = match,
|
|
||||||
PatternConfig = patternConfig,
|
|
||||||
Type = MatchType.Question
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 扫描所有选项模式
|
|
||||||
foreach (var patternConfig in _config.OptionPatterns)
|
|
||||||
{
|
|
||||||
foreach (Match match in patternConfig.Regex.Matches(text))
|
|
||||||
{
|
|
||||||
allPotentialMatches.Add(new PotentialMatch
|
|
||||||
{
|
|
||||||
StartIndex = match.Index,
|
|
||||||
EndIndex = match.Index + match.Length,
|
|
||||||
MatchedText = match.Value,
|
|
||||||
RegexMatch = match,
|
|
||||||
PatternConfig = patternConfig,
|
|
||||||
Type = MatchType.Option
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 统一按起始位置排序
|
|
||||||
return allPotentialMatches.OrderBy(pm => pm.StartIndex).ToList();
|
return allPotentialMatches.OrderBy(pm => pm.StartIndex).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -251,18 +206,8 @@ namespace TechHelper.Client.Exam
|
|||||||
_config = config ?? throw new ArgumentNullException(nameof(config), "ExamParserConfig cannot be null.");
|
_config = config ?? throw new ArgumentNullException(nameof(config), "ExamParserConfig cannot be null.");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public AssignmentEx BuildExam(string fullExamText, List<PotentialMatch> allPotentialMatches)
|
||||||
/// Builds the ExamPaper structure from raw text and potential matches.
|
|
||||||
/// Collects and returns parsing errors encountered during the process.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="fullExamText">The complete text of the exam paper.</param>
|
|
||||||
/// <param name="allPotentialMatches">A list of all identified potential matches.</param>
|
|
||||||
/// <returns>An ExamPaper object containing the parsed structure and a list of errors.</returns>
|
|
||||||
/// <exception cref="ArgumentException">Thrown if fullExamText is null or empty.</exception>
|
|
||||||
/// <exception cref="ArgumentNullException">Thrown if allPotentialMatches is null.</exception>
|
|
||||||
public ExamPaper BuildExamPaper(string fullExamText, List<PotentialMatch> allPotentialMatches)
|
|
||||||
{
|
{
|
||||||
// 核心输入验证仍然是必要的,因为这些错误是无法恢复的
|
|
||||||
if (string.IsNullOrWhiteSpace(fullExamText))
|
if (string.IsNullOrWhiteSpace(fullExamText))
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Full exam text cannot be null or empty.", nameof(fullExamText));
|
throw new ArgumentException("Full exam text cannot be null or empty.", nameof(fullExamText));
|
||||||
@@ -272,269 +217,79 @@ namespace TechHelper.Client.Exam
|
|||||||
throw new ArgumentNullException(nameof(allPotentialMatches), "Potential matches list cannot be null.");
|
throw new ArgumentNullException(nameof(allPotentialMatches), "Potential matches list cannot be null.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var examPaper = new ExamPaper(); // ExamPaper 现在包含一个 Errors 列表
|
var assignment = new AssignmentEx();
|
||||||
|
|
||||||
// 尝试获取试卷标题
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
examPaper.AssignmentTitle = GetExamTitle(fullExamText);
|
assignment.Title = GetExamTitle(fullExamText);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// 如果获取标题失败,记录错误而不是抛出致命异常
|
assignment.Errors.Add(new ParseError(ParseErrorType.UnexpectedError, "Failed to extract exam title.", innerException: ex));
|
||||||
examPaper.Errors.Add(new ParseError(ParseErrorType.UnexpectedError, "Failed to extract exam title.", innerException: ex));
|
assignment.Title = "未识别试卷标题";
|
||||||
examPaper.AssignmentTitle = "未识别试卷标题"; // 提供默认值
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var majorQGStack = new Stack<MajorQuestionGroup>();
|
var assignmentQuestionStack = new Stack<AssignmentQuestionEx>();
|
||||||
MajorQuestionGroup currentMajorQG = null;
|
var rootAssignmentQuestion = new AssignmentQuestionEx { Type = AssignmentStructType.Struct, Priority = 0, Title = "Root Exam Structure" };
|
||||||
|
assignmentQuestionStack.Push(rootAssignmentQuestion);
|
||||||
var questionStack = new Stack<Question>();
|
assignment.ExamStruct = rootAssignmentQuestion;
|
||||||
Question currentQuestion = null;
|
|
||||||
|
|
||||||
int currentContentStart = 0;
|
int currentContentStart = 0;
|
||||||
|
|
||||||
// 处理试卷开头的描述性文本
|
|
||||||
if (allPotentialMatches.Any() && allPotentialMatches[0].StartIndex > 0)
|
if (allPotentialMatches.Any() && allPotentialMatches[0].StartIndex > 0)
|
||||||
{
|
{
|
||||||
string introText = fullExamText.Substring(0, allPotentialMatches[0].StartIndex).Trim();
|
string introText = fullExamText.Substring(0, allPotentialMatches[0].StartIndex).Trim();
|
||||||
if (!string.IsNullOrWhiteSpace(introText))
|
if (!string.IsNullOrWhiteSpace(introText))
|
||||||
{
|
{
|
||||||
examPaper.Description += (string.IsNullOrWhiteSpace(examPaper.Description) ? "" : "\n") + introText;
|
assignment.Description += (string.IsNullOrWhiteSpace(assignment.Description) ? "" : "\n") + introText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
currentContentStart = allPotentialMatches.Any() ? allPotentialMatches[0].StartIndex : 0;
|
||||||
currentContentStart = allPotentialMatches[0].StartIndex;
|
|
||||||
|
|
||||||
for (int i = 0; i < allPotentialMatches.Count; i++)
|
for (int i = 0; i < allPotentialMatches.Count; i++)
|
||||||
{
|
{
|
||||||
var pm = allPotentialMatches[i];
|
var pm = allPotentialMatches[i];
|
||||||
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// **数据验证:不再抛出,而是记录错误**
|
if (!IsValidPotentialMatch(pm, i, fullExamText.Length, currentContentStart, assignment.Errors))
|
||||||
if (pm.StartIndex < currentContentStart || pm.EndIndex > fullExamText.Length || pm.StartIndex > pm.EndIndex)
|
|
||||||
{
|
{
|
||||||
examPaper.Errors.Add(new ParseError(ParseErrorType.Validation,
|
currentContentStart = Math.Max(currentContentStart, pm.EndIndex);
|
||||||
$"PotentialMatch at index {i} has invalid start/end indices. Start: {pm.StartIndex}, End: {pm.EndIndex}, CurrentContentStart: {currentContentStart}, FullTextLength: {fullExamText.Length}",
|
continue;
|
||||||
index: i, matchedText: pm.MatchedText));
|
|
||||||
currentContentStart = Math.Max(currentContentStart, pm.EndIndex); // 尝试跳过这个损坏的匹配项
|
|
||||||
continue; // 跳过当前循环迭代,处理下一个匹配项
|
|
||||||
}
|
|
||||||
if (pm.RegexMatch == null || pm.PatternConfig == null)
|
|
||||||
{
|
|
||||||
examPaper.Errors.Add(new ParseError(ParseErrorType.Validation,
|
|
||||||
$"PotentialMatch at index {i} is missing RegexMatch or PatternConfig.",
|
|
||||||
index: i, matchedText: pm.MatchedText));
|
|
||||||
currentContentStart = Math.Max(currentContentStart, pm.EndIndex); // 尝试跳过这个损坏的匹配项
|
|
||||||
continue; // 跳过当前循环迭代,处理下一个匹配项
|
|
||||||
}
|
}
|
||||||
|
|
||||||
string precedingText = fullExamText.Substring(currentContentStart, pm.StartIndex - currentContentStart).Trim();
|
string precedingText = fullExamText.Substring(currentContentStart, pm.StartIndex - currentContentStart).Trim();
|
||||||
if (!string.IsNullOrWhiteSpace(precedingText))
|
if (!string.IsNullOrWhiteSpace(precedingText))
|
||||||
{
|
{
|
||||||
if (currentQuestion != null)
|
if (assignmentQuestionStack.Peek().Question != null)
|
||||||
{
|
{
|
||||||
// 将 examPaper.Errors 传递给 ProcessQuestionContent 收集错误
|
ProcessQuestionContent(assignmentQuestionStack.Peek(), precedingText, assignment.Errors);
|
||||||
ProcessQuestionContent(currentQuestion, precedingText,
|
|
||||||
GetSubMatchesForRange(allPotentialMatches, currentContentStart, pm.StartIndex, examPaper.Errors),
|
|
||||||
examPaper.Errors);
|
|
||||||
}
|
|
||||||
else if (currentMajorQG != null)
|
|
||||||
{
|
|
||||||
currentMajorQG.Descript += (string.IsNullOrWhiteSpace(currentMajorQG.Descript) ? "" : "\n") + precedingText;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
examPaper.Description += (string.IsNullOrWhiteSpace(examPaper.Description) ? "" : "\n") + precedingText;
|
assignment.Description += (string.IsNullOrWhiteSpace(assignment.Description) ? "" : "\n") + precedingText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pm.Type == MatchType.MajorQuestionGroup)
|
if (pm.PatternConfig.Type == AssignmentStructType.Option)
|
||||||
{
|
{
|
||||||
// 对 MajorQuestionGroup 的处理
|
HandleOptionMatch(pm, i, assignmentQuestionStack.Peek(), assignment.Errors);
|
||||||
try
|
|
||||||
{
|
|
||||||
while (majorQGStack.Any() && pm.PatternConfig.Priority <= majorQGStack.Peek().Priority)
|
|
||||||
{
|
|
||||||
majorQGStack.Pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegexMatch Groups 验证:不再抛出,记录错误
|
|
||||||
if (pm.RegexMatch.Groups.Count < 2 || string.IsNullOrWhiteSpace(pm.RegexMatch.Groups[1].Value))
|
|
||||||
{
|
|
||||||
examPaper.Errors.Add(new ParseError(ParseErrorType.RegexMatchIssue,
|
|
||||||
$"MajorQuestionGroup match at index {i} does not have enough regex groups or a valid title group (Group 1). Skipping this group.",
|
|
||||||
index: i, matchedText: pm.MatchedText));
|
|
||||||
currentContentStart = pm.EndIndex; // 继续,尝试跳过此项
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
float score = 0;
|
|
||||||
// 使用 float.TryParse 避免异常
|
|
||||||
if (pm.RegexMatch.Groups.Count > 3 && pm.RegexMatch.Groups[4].Success) // 假设纯数字分数是 Group 4
|
|
||||||
{
|
|
||||||
if (!float.TryParse(pm.RegexMatch.Groups[4].Value, out score))
|
|
||||||
{
|
|
||||||
examPaper.Errors.Add(new ParseError(ParseErrorType.DataParsing,
|
|
||||||
$"Failed to parse score '{pm.RegexMatch.Groups[4].Value}' for MajorQuestionGroup at index {i}. Defaulting to 0.",
|
|
||||||
index: i, matchedText: pm.MatchedText));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MajorQuestionGroup newMajorQG = new MajorQuestionGroup
|
|
||||||
{
|
|
||||||
Title = pm.RegexMatch.Groups[2].Value.Trim(), // 标题是 Group 2
|
|
||||||
Score = score,
|
|
||||||
Priority = pm.PatternConfig.Priority,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (majorQGStack.Any())
|
|
||||||
{
|
|
||||||
majorQGStack.Peek().SubQuestionGroups.Add(newMajorQG);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
examPaper.QuestionGroups.Add(newMajorQG);
|
HandleQuestionGroupMatch(pm, i, assignmentQuestionStack, assignment.Errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
currentContentStart = pm.EndIndex;
|
currentContentStart = pm.EndIndex;
|
||||||
majorQGStack.Push(newMajorQG);
|
|
||||||
currentMajorQG = newMajorQG;
|
|
||||||
questionStack.Clear();
|
|
||||||
currentQuestion = null;
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
examPaper.Errors.Add(new ParseError(ParseErrorType.UnexpectedError,
|
assignment.Errors.Add(new ParseError(ParseErrorType.UnexpectedError,
|
||||||
$"An unexpected error occurred during processing MajorQuestionGroup at index {i}.",
|
|
||||||
index: i, matchedText: pm.MatchedText, innerException: ex));
|
|
||||||
currentContentStart = pm.EndIndex; // 尝试跳过此项
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (pm.Type == MatchType.Question)
|
|
||||||
{
|
|
||||||
// 对 Question 的处理
|
|
||||||
try
|
|
||||||
{
|
|
||||||
while (questionStack.Any() && pm.PatternConfig.Priority <= questionStack.Peek().Priority)
|
|
||||||
{
|
|
||||||
questionStack.Pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegexMatch Groups 验证
|
|
||||||
if (pm.RegexMatch.Groups.Count < 3 || string.IsNullOrWhiteSpace(pm.RegexMatch.Groups[1].Value) || string.IsNullOrWhiteSpace(pm.RegexMatch.Groups[2].Value))
|
|
||||||
{
|
|
||||||
examPaper.Errors.Add(new ParseError(ParseErrorType.RegexMatchIssue,
|
|
||||||
$"Question match at index {i} does not have enough regex groups or valid number/text groups (Group 1/2). Skipping this question.",
|
|
||||||
index: i, matchedText: pm.MatchedText));
|
|
||||||
currentContentStart = pm.EndIndex; // 尝试跳过此项
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
float score = 0;
|
|
||||||
// 使用 float.TryParse 避免异常
|
|
||||||
if (pm.RegexMatch.Groups.Count > 4 && pm.RegexMatch.Groups[4].Success) // 假设纯数字分数是 Group 4
|
|
||||||
{
|
|
||||||
if (!float.TryParse(pm.RegexMatch.Groups[4].Value, out score))
|
|
||||||
{
|
|
||||||
examPaper.Errors.Add(new ParseError(ParseErrorType.DataParsing,
|
|
||||||
$"Failed to parse score '{pm.RegexMatch.Groups[4].Value}' for Question at index {i}. Defaulting to 0.",
|
|
||||||
index: i, matchedText: pm.MatchedText));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Question newQuestion = new Question
|
|
||||||
{
|
|
||||||
Number = pm.RegexMatch.Groups[1].Value.Trim(),
|
|
||||||
Stem = pm.RegexMatch.Groups[2].Value.Trim(),
|
|
||||||
Priority = pm.PatternConfig.Priority,
|
|
||||||
Score = score // 赋值解析到的分数
|
|
||||||
};
|
|
||||||
|
|
||||||
if (questionStack.Any())
|
|
||||||
{
|
|
||||||
questionStack.Peek().SubQuestions.Add(newQuestion);
|
|
||||||
}
|
|
||||||
else if (currentMajorQG != null)
|
|
||||||
{
|
|
||||||
currentMajorQG.SubQuestions.Add(newQuestion);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
examPaper.TopLevelQuestions.Add(newQuestion);
|
|
||||||
}
|
|
||||||
|
|
||||||
currentContentStart = pm.EndIndex;
|
|
||||||
questionStack.Push(newQuestion);
|
|
||||||
currentQuestion = newQuestion;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
examPaper.Errors.Add(new ParseError(ParseErrorType.UnexpectedError,
|
|
||||||
$"An unexpected error occurred during processing Question at index {i}.",
|
|
||||||
index: i, matchedText: pm.MatchedText, innerException: ex));
|
|
||||||
currentContentStart = pm.EndIndex; // 尝试跳过此项
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (pm.Type == MatchType.Option)
|
|
||||||
{
|
|
||||||
// 对 Option 的处理
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (currentQuestion != null)
|
|
||||||
{
|
|
||||||
// RegexMatch Groups 验证
|
|
||||||
if (pm.RegexMatch.Groups.Count < 3 || string.IsNullOrWhiteSpace(pm.RegexMatch.Groups[1].Value) || string.IsNullOrWhiteSpace(pm.RegexMatch.Groups[2].Value))
|
|
||||||
{
|
|
||||||
examPaper.Errors.Add(new ParseError(ParseErrorType.RegexMatchIssue,
|
|
||||||
$"Option match at index {i} does not have enough regex groups or valid label/text groups (Group 1/2). Skipping this option.",
|
|
||||||
index: i, matchedText: pm.MatchedText));
|
|
||||||
currentContentStart = pm.EndIndex; // 尝试跳过此项
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Option newOption = new Option
|
|
||||||
{
|
|
||||||
Label = pm.RegexMatch.Groups[1].Value.Trim(),
|
|
||||||
Text = pm.RegexMatch.Groups[2].Value.Trim()
|
|
||||||
};
|
|
||||||
currentQuestion.Options.Add(newOption);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// 结构性问题:找到孤立的选项,记录错误但继续
|
|
||||||
examPaper.Errors.Add(new ParseError(ParseErrorType.Structural,
|
|
||||||
$"Found isolated Option at index {i}. Options must belong to a question. Ignoring this option.",
|
|
||||||
index: i, matchedText: pm.MatchedText));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
examPaper.Errors.Add(new ParseError(ParseErrorType.UnexpectedError,
|
|
||||||
$"An unexpected error occurred during processing Option at index {i}.",
|
|
||||||
index: i, matchedText: pm.MatchedText, innerException: ex));
|
|
||||||
// 这里不需要 `continue`,因为即使出错也可能只是该选项的问题,不影响后续处理
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
currentContentStart = pm.EndIndex; // 更新当前内容起点
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// 捕获任何在处理单个 PotentialMatch 过程中未被更具体 catch 块捕获的意外错误
|
|
||||||
examPaper.Errors.Add(new ParseError(ParseErrorType.UnexpectedError,
|
|
||||||
$"An unexpected error occurred during main loop processing of PotentialMatch at index {i}.",
|
$"An unexpected error occurred during main loop processing of PotentialMatch at index {i}.",
|
||||||
index: i, matchedText: pm.MatchedText, innerException: ex));
|
index: i, matchedText: pm.MatchedText, innerException: ex));
|
||||||
currentContentStart = Math.Max(currentContentStart, pm.EndIndex); // 尝试跳过当前匹配项,继续下一项
|
currentContentStart = Math.Max(currentContentStart, pm.EndIndex);
|
||||||
// 这里不 `continue` 是因为外层循环会推进 `i`,但确保 `currentContentStart` 更新以避免无限循环
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 处理所有匹配项之后的剩余内容 ---
|
|
||||||
if (currentContentStart < fullExamText.Length)
|
if (currentContentStart < fullExamText.Length)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -542,169 +297,183 @@ namespace TechHelper.Client.Exam
|
|||||||
string remainingText = fullExamText.Substring(currentContentStart).Trim();
|
string remainingText = fullExamText.Substring(currentContentStart).Trim();
|
||||||
if (!string.IsNullOrWhiteSpace(remainingText))
|
if (!string.IsNullOrWhiteSpace(remainingText))
|
||||||
{
|
{
|
||||||
if (currentQuestion != null)
|
if (assignmentQuestionStack.Peek().Question != null)
|
||||||
{
|
{
|
||||||
ProcessQuestionContent(currentQuestion, remainingText,
|
ProcessQuestionContent(assignmentQuestionStack.Peek(), remainingText, assignment.Errors);
|
||||||
GetSubMatchesForRange(allPotentialMatches, currentContentStart, fullExamText.Length, examPaper.Errors),
|
|
||||||
examPaper.Errors);
|
|
||||||
}
|
|
||||||
else if (currentMajorQG != null)
|
|
||||||
{
|
|
||||||
currentMajorQG.Descript += (string.IsNullOrWhiteSpace(currentMajorQG.Descript) ? "" : "\n") + remainingText;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
examPaper.Description += (string.IsNullOrWhiteSpace(examPaper.Description) ? "" : "\n") + remainingText;
|
assignment.Description += (string.IsNullOrWhiteSpace(assignment.Description) ? "" : "\n") + remainingText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
examPaper.Errors.Add(new ParseError(ParseErrorType.UnexpectedError,
|
assignment.Errors.Add(new ParseError(ParseErrorType.UnexpectedError,
|
||||||
"An unexpected error occurred while processing remaining text after all potential matches.",
|
"An unexpected error occurred while processing remaining text after all potential matches.",
|
||||||
innerException: ex));
|
innerException: ex));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return examPaper;
|
return assignment;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private bool IsValidPotentialMatch(PotentialMatch pm, int index, int fullTextLength, int currentContentStart, List<ParseError> errors)
|
||||||
/// Extracts the exam title (simple implementation).
|
|
||||||
/// Logs errors to the provided error list instead of throwing.
|
|
||||||
/// </summary>
|
|
||||||
private string GetExamTitle(string examPaperText)
|
|
||||||
{
|
{
|
||||||
// 内部不再直接抛出异常,而是让外部的 try-catch 负责
|
if (pm.StartIndex < currentContentStart || pm.EndIndex > fullTextLength || pm.StartIndex > pm.EndIndex)
|
||||||
var firstLine = examPaperText.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
|
|
||||||
.FirstOrDefault(line => !string.IsNullOrWhiteSpace(line));
|
|
||||||
return firstLine ?? "未识别试卷标题";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a subset of the given PotentialMatch list within a specified range.
|
|
||||||
/// Logs errors to the provided error list instead of throwing.
|
|
||||||
/// </summary>
|
|
||||||
private List<PotentialMatch> GetSubMatchesForRange(List<PotentialMatch> allMatches, int start, int end, List<ParseError> errors)
|
|
||||||
{
|
|
||||||
// 输入验证,如果输入错误,记录错误并返回空列表
|
|
||||||
if (start < 0 || end < start)
|
|
||||||
{
|
{
|
||||||
errors.Add(new ParseError(ParseErrorType.Validation,
|
errors.Add(new ParseError(ParseErrorType.Validation,
|
||||||
$"Invalid range provided to GetSubMatchesForRange. Start: {start}, End: {end}.",
|
$"PotentialMatch at index {index} has invalid start/end indices. Start: {pm.StartIndex}, End: {pm.EndIndex}, CurrentContentStart: {currentContentStart}, FullTextLength: {fullTextLength}",
|
||||||
index: start)); // 使用 start 作为大概索引
|
index: index, matchedText: pm.MatchedText));
|
||||||
return new List<PotentialMatch>();
|
return false;
|
||||||
}
|
}
|
||||||
// allMatches 为 null 的情况已经在 BuildExamPaper 顶部处理,这里为了方法的健壮性可以再加一次检查
|
if (pm.RegexMatch == null || pm.PatternConfig == null)
|
||||||
if (allMatches == null)
|
|
||||||
{
|
{
|
||||||
return new List<PotentialMatch>();
|
errors.Add(new ParseError(ParseErrorType.Validation,
|
||||||
|
$"PotentialMatch at index {index} is missing RegexMatch or PatternConfig.",
|
||||||
|
index: index, matchedText: pm.MatchedText));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void HandleQuestionGroupMatch(PotentialMatch pm, int index, Stack<AssignmentQuestionEx> assignmentQuestionStack, List<ParseError> errors)
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return allMatches.Where(pm => pm.StartIndex >= start && pm.StartIndex < end).ToList();
|
while (assignmentQuestionStack.Count > 1 && pm.PatternConfig.Priority <= assignmentQuestionStack.Peek().Priority)
|
||||||
|
{
|
||||||
|
assignmentQuestionStack.Pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
string sequence = assignmentQuestionStack.Count > 0 ? assignmentQuestionStack.Peek().Sequence : string.Empty;
|
||||||
|
|
||||||
|
// 验证捕获组:Group 1 是编号,Group 2 是题目内容
|
||||||
|
if (pm.RegexMatch.Groups.Count < 3 || !pm.RegexMatch.Groups[1].Success || string.IsNullOrWhiteSpace(pm.RegexMatch.Groups[2].Value))
|
||||||
|
{
|
||||||
|
errors.Add(new ParseError(ParseErrorType.RegexMatchIssue,
|
||||||
|
$"Question/Group match at index {index} does not have enough regex groups (expected 3 for number and title) or a valid title group (Group 2). Skipping this group.",
|
||||||
|
index: index, matchedText: pm.MatchedText));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float score = 0;
|
||||||
|
// 尝试从 MatchedText 的末尾匹配分数
|
||||||
|
Match scoreMatch = _config.ScoreRegex.Match(pm.MatchedText);
|
||||||
|
if (scoreMatch.Success && scoreMatch.Groups.Count > 1 && scoreMatch.Groups[1].Success)
|
||||||
|
{
|
||||||
|
if (!float.TryParse(scoreMatch.Groups[1].Value, out score))
|
||||||
|
{
|
||||||
|
errors.Add(new ParseError(ParseErrorType.DataParsing,
|
||||||
|
$"Failed to parse score '{scoreMatch.Groups[1].Value}' for match at index {index}. Defaulting to 0.",
|
||||||
|
index: index, matchedText: pm.MatchedText));
|
||||||
|
}
|
||||||
|
// 从 MatchedText 中移除分数部分,使其只包含编号和标题
|
||||||
|
// 注意:这里修改的是pm.MatchedText,这不会影响原始文本,只是当前匹配项的“内容”
|
||||||
|
pm.MatchedText = pm.MatchedText.Substring(0, scoreMatch.Index).Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取标题,这里使用 Group 2 的值,它不包含分数
|
||||||
|
string title = pm.RegexMatch.Groups[2].Value.Trim();
|
||||||
|
|
||||||
|
string seq = pm.RegexMatch.Groups[1].Value.Trim();
|
||||||
|
seq = string.IsNullOrEmpty(seq) || string.IsNullOrEmpty(sequence) ? seq : " ." + seq;
|
||||||
|
|
||||||
|
AssignmentQuestionEx newAssignmentQuestion;
|
||||||
|
if (pm.PatternConfig.Type == AssignmentStructType.Struct)
|
||||||
|
{
|
||||||
|
newAssignmentQuestion = new AssignmentQuestionEx
|
||||||
|
{
|
||||||
|
Title = title,
|
||||||
|
Score = score,
|
||||||
|
Sequence = sequence + seq,
|
||||||
|
Priority = pm.PatternConfig.Priority,
|
||||||
|
Type = pm.PatternConfig.Type
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else // AssignmentStructType.Question 类型
|
||||||
|
{
|
||||||
|
newAssignmentQuestion = new AssignmentQuestionEx
|
||||||
|
{
|
||||||
|
Priority = pm.PatternConfig.Priority,
|
||||||
|
Type = pm.PatternConfig.Type,
|
||||||
|
Sequence = sequence + seq,
|
||||||
|
Score = score,
|
||||||
|
Question = new QuestionEx
|
||||||
|
{
|
||||||
|
Title = title,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
assignmentQuestionStack.Peek().ChildrenAssignmentQuestion.Add(newAssignmentQuestion);
|
||||||
|
assignmentQuestionStack.Push(newAssignmentQuestion);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
errors.Add(new ParseError(ParseErrorType.UnexpectedError,
|
errors.Add(new ParseError(ParseErrorType.UnexpectedError,
|
||||||
$"An unexpected error occurred getting sub-matches for range [{start}, {end}).",
|
$"An unexpected error occurred during processing a non-option match (type: {pm.PatternConfig.Type}) at index {index}.",
|
||||||
innerException: ex));
|
index: index, matchedText: pm.MatchedText, innerException: ex));
|
||||||
return new List<PotentialMatch>(); // 出错时返回空列表
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private void HandleOptionMatch(PotentialMatch pm, int index, AssignmentQuestionEx currentAssignmentQuestion, List<ParseError> errors)
|
||||||
/// Processes the content of a Question, mainly for parsing Options and identifying unstructured text.
|
|
||||||
/// Logs errors to the provided error list instead of throwing.
|
|
||||||
/// </summary>
|
|
||||||
private void ProcessQuestionContent(Question question, string contentText, List<PotentialMatch> potentialMatchesInScope, List<ParseError> errors)
|
|
||||||
{
|
{
|
||||||
// 参数验证,这些是内部方法的契约,如果违反则直接抛出,因为这意味着调用者有错
|
|
||||||
if (question == null) throw new ArgumentNullException(nameof(question), "Question cannot be null in ProcessQuestionContent.");
|
|
||||||
if (contentText == null) throw new ArgumentNullException(nameof(contentText), "Content text cannot be null in ProcessQuestionContent.");
|
|
||||||
if (potentialMatchesInScope == null) throw new ArgumentNullException(nameof(potentialMatchesInScope), "Potential matches in scope cannot be null.");
|
|
||||||
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
int lastOptionEndIndex = 0;
|
if (currentAssignmentQuestion.Question == null)
|
||||||
|
|
||||||
foreach (var pm in potentialMatchesInScope.OrderBy(p => p.StartIndex))
|
|
||||||
{
|
{
|
||||||
// 对每个匹配项的内部处理,记录错误但继续
|
errors.Add(new ParseError(ParseErrorType.Structural,
|
||||||
try
|
$"Found isolated Option at index {index}. Options must belong to a 'Question' type structure. Ignoring this option.",
|
||||||
{
|
index: index, matchedText: pm.MatchedText));
|
||||||
if (pm.Type == MatchType.Option)
|
return;
|
||||||
{
|
|
||||||
// 验证索引,记录错误但继续
|
|
||||||
if (pm.StartIndex < lastOptionEndIndex || pm.StartIndex > contentText.Length || pm.EndIndex > contentText.Length)
|
|
||||||
{
|
|
||||||
errors.Add(new ParseError(ParseErrorType.Validation,
|
|
||||||
$"Option match at index {pm.StartIndex} has invalid indices within content text. MatchedText: '{pm.MatchedText}'. Skipping.",
|
|
||||||
index: pm.StartIndex, matchedText: pm.MatchedText));
|
|
||||||
continue; // 跳过当前选项
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理选项前的文本
|
if (pm.RegexMatch.Groups.Count < 3 || !pm.RegexMatch.Groups[1].Success || string.IsNullOrWhiteSpace(pm.RegexMatch.Groups[2].Value))
|
||||||
if (pm.StartIndex > lastOptionEndIndex)
|
|
||||||
{
|
|
||||||
string textBeforeOption = contentText.Substring(lastOptionEndIndex, pm.StartIndex - lastOptionEndIndex).Trim();
|
|
||||||
if (!string.IsNullOrWhiteSpace(textBeforeOption))
|
|
||||||
{
|
|
||||||
question.Stem += (string.IsNullOrWhiteSpace(question.Stem) ? "" : "\n") + textBeforeOption;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegexMatch Groups 验证,记录错误但继续
|
|
||||||
if (pm.RegexMatch.Groups.Count < 3 || string.IsNullOrWhiteSpace(pm.RegexMatch.Groups[1].Value) || string.IsNullOrWhiteSpace(pm.RegexMatch.Groups[2].Value))
|
|
||||||
{
|
{
|
||||||
errors.Add(new ParseError(ParseErrorType.RegexMatchIssue,
|
errors.Add(new ParseError(ParseErrorType.RegexMatchIssue,
|
||||||
$"Option regex match '{pm.MatchedText}' does not have enough groups (expected 3) for label and text. Skipping option.",
|
$"Option match at index {index} does not have enough regex groups or valid label/text groups (Group 1/2). Skipping this option.",
|
||||||
index: pm.StartIndex, matchedText: pm.MatchedText));
|
index: index, matchedText: pm.MatchedText));
|
||||||
lastOptionEndIndex = pm.EndIndex; // 更新索引,避免卡死
|
return;
|
||||||
continue; // 跳过当前选项
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var newOption = new Option
|
Option newOption = new Option
|
||||||
{
|
{
|
||||||
Label = pm.RegexMatch.Groups[1].Value.Trim(),
|
Label = pm.RegexMatch.Groups[1].Value.Trim(),
|
||||||
Text = pm.RegexMatch.Groups[2].Value.Trim()
|
Text = pm.RegexMatch.Groups[2].Value.Trim()
|
||||||
};
|
};
|
||||||
question.Options.Add(newOption);
|
currentAssignmentQuestion.Question.Options.Add(newOption);
|
||||||
lastOptionEndIndex = pm.EndIndex;
|
|
||||||
}
|
|
||||||
// TODO: If there are SubQuestion types, they can be processed similarly here.
|
|
||||||
// 你可以在此处添加对子问题的处理逻辑,同样需要小心处理其内容和嵌套。
|
|
||||||
}
|
|
||||||
catch (Exception innerEx)
|
|
||||||
{
|
|
||||||
errors.Add(new ParseError(ParseErrorType.UnexpectedError,
|
|
||||||
$"An unexpected error occurred during processing a potential match ({pm.Type}) within question content.",
|
|
||||||
index: pm.StartIndex, matchedText: pm.MatchedText, innerException: innerEx));
|
|
||||||
lastOptionEndIndex = pm.EndIndex; // 尝试更新索引,避免无限循环
|
|
||||||
continue; // 尝试继续下一个匹配项
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理所有选项之后的剩余文本
|
|
||||||
if (lastOptionEndIndex < contentText.Length)
|
|
||||||
{
|
|
||||||
string remainingContent = contentText.Substring(lastOptionEndIndex).Trim();
|
|
||||||
if (!string.IsNullOrWhiteSpace(remainingContent))
|
|
||||||
{
|
|
||||||
question.Stem += (string.IsNullOrWhiteSpace(question.Stem) ? "" : "\n") + remainingContent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// 捕获 ProcessQuestionContent 整个方法内部的意外错误
|
|
||||||
errors.Add(new ParseError(ParseErrorType.UnexpectedError,
|
errors.Add(new ParseError(ParseErrorType.UnexpectedError,
|
||||||
$"An unexpected error occurred while processing content for Question '{question.Number}'.",
|
$"An unexpected error occurred during processing Option at index {index}.",
|
||||||
innerException: ex));
|
index: index, matchedText: pm.MatchedText, innerException: ex));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ProcessQuestionContent(AssignmentQuestionEx question, string contentText, List<ParseError> errors)
|
||||||
|
{
|
||||||
|
if (question?.Question == null)
|
||||||
|
{
|
||||||
|
errors.Add(new ParseError(ParseErrorType.Structural,
|
||||||
|
$"Attempted to process content for a non-question type AssignmentQuestionEx (Type: {question?.Type}). Content: '{contentText}'",
|
||||||
|
matchedText: contentText));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(contentText))
|
||||||
|
{
|
||||||
|
question.Question.Title += (string.IsNullOrWhiteSpace(question.Question.Title) ? "" : "\n") + contentText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetExamTitle(string examPaperText)
|
||||||
|
{
|
||||||
|
var firstLine = examPaperText.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.FirstOrDefault(line => !string.IsNullOrWhiteSpace(line));
|
||||||
|
return firstLine ?? "未识别试卷标题";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ExamParser
|
public class ExamParser
|
||||||
@@ -721,20 +490,16 @@ namespace TechHelper.Client.Exam
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 解析给定的试卷文本,返回结构化的 ExamPaper 对象。
|
/// 解析给定的试卷文本,返回结构化的 AssignmentEx 对象。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="examPaperText">完整的试卷文本</param>
|
/// <param name="examPaperText">完整的试卷文本</param>
|
||||||
/// <returns>解析后的 ExamPaper 对象</returns>
|
/// <returns>解析后的 AssignmentEx 对象</returns>
|
||||||
public ExamPaper ParseExamPaper(string examPaperText)
|
public AssignmentEx ParseExamPaper(string examPaperText)
|
||||||
{
|
{
|
||||||
// 1. 扫描:一次性扫描整个文本,收集所有潜在的匹配项
|
var assignment = new AssignmentEx();
|
||||||
// Scan 方法现在已经优化为不抛出 ArgumentNullException
|
List<PotentialMatch> allPotentialMatches = _scanner.Scan(examPaperText, assignment.Errors);
|
||||||
List<PotentialMatch> allPotentialMatches = _scanner.Scan(examPaperText);
|
assignment = _builder.BuildExam(examPaperText, allPotentialMatches);
|
||||||
|
return assignment;
|
||||||
// 2. 构建:根据扫描结果和原始文本,线性遍历并构建层级结构
|
|
||||||
// BuildExamPaper 现在会返回一个包含错误列表的 ExamPaper 对象
|
|
||||||
// 外部不再需要捕获内部解析异常,只需检查 ExamPaper.Errors 列表
|
|
||||||
return _builder.BuildExamPaper(examPaperText, allPotentialMatches);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,92 +0,0 @@
|
|||||||
using Entities.DTO;
|
|
||||||
|
|
||||||
namespace TechHelper.Client.Exam
|
|
||||||
{
|
|
||||||
public class ExamStruct
|
|
||||||
{
|
|
||||||
public string Title { get; set; }
|
|
||||||
public List<QuestionItem> Questions { get; set; } = new List<QuestionItem>();
|
|
||||||
|
|
||||||
|
|
||||||
public class QuestionItem
|
|
||||||
{
|
|
||||||
public string Sequence { get; set; } = string.Empty;
|
|
||||||
public string QuestionText { get; set; } = string.Empty;
|
|
||||||
public float Score { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Student
|
|
||||||
{
|
|
||||||
public Guid Id { get; set; } = Guid.NewGuid();
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class QuestionAnswerStatus
|
|
||||||
{
|
|
||||||
public string QuestionSequence { get; set; } = string.Empty; // 题目序号,例如 "1.1"
|
|
||||||
public string QuestionText { get; set; } = string.Empty; // 题目文本
|
|
||||||
public float QuestionScore { get; set; } // 题目分值
|
|
||||||
public Dictionary<Guid, bool> StudentCorrectStatus { get; set; } = new Dictionary<Guid, bool>();
|
|
||||||
// Key: Student.Id, Value: true 表示正确,false 表示错误
|
|
||||||
}
|
|
||||||
|
|
||||||
public class QuestionRowData
|
|
||||||
{
|
|
||||||
public ExamStruct.QuestionItem QuestionItem { get; set; } // 原始题目信息
|
|
||||||
public Dictionary<Guid, bool> StudentAnswers { get; set; } = new Dictionary<Guid, bool>();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static class ExamStructExtensions
|
|
||||||
{
|
|
||||||
public static ExamStruct GetStruct(this ExamDto dto)
|
|
||||||
{
|
|
||||||
if (dto == null)
|
|
||||||
{
|
|
||||||
return new ExamStruct { Title = "无效试卷", Questions = new List<ExamStruct.QuestionItem>() };
|
|
||||||
}
|
|
||||||
|
|
||||||
var examStruct = new ExamStruct
|
|
||||||
{
|
|
||||||
Title = dto.AssignmentTitle
|
|
||||||
};
|
|
||||||
|
|
||||||
GetSeqRecursive(dto.QuestionGroups, null, examStruct.Questions);
|
|
||||||
|
|
||||||
return examStruct;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 递归方法,用于生成所有题目和子题目的完整序号。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="currentGroup">当前正在处理的题目组。</param>
|
|
||||||
/// <param name="parentSequence">当前题目组的父级序号(例如:"1", "2.1")。如果为空,则表示顶级题目组。</param>
|
|
||||||
/// <param name="allQuestions">用于收集所有生成题目项的列表。</param>
|
|
||||||
private static void GetSeqRecursive(
|
|
||||||
QuestionGroupDto currentGroup,
|
|
||||||
string? parentSequence,
|
|
||||||
List<ExamStruct.QuestionItem> allQuestions)
|
|
||||||
{
|
|
||||||
string currentGroupSequence = parentSequence != null
|
|
||||||
? $"{parentSequence}.{currentGroup.Index}"
|
|
||||||
: currentGroup.Index.ToString();
|
|
||||||
|
|
||||||
foreach (var subQuestion in currentGroup.SubQuestions)
|
|
||||||
{
|
|
||||||
string fullSequence = $"{currentGroupSequence}.{subQuestion.Index}";
|
|
||||||
allQuestions.Add(new ExamStruct.QuestionItem
|
|
||||||
{
|
|
||||||
Sequence = fullSequence,
|
|
||||||
QuestionText = subQuestion.Stem ?? string.Empty,
|
|
||||||
Score = subQuestion.Score
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var subGroup in currentGroup.SubQuestionGroups)
|
|
||||||
{
|
|
||||||
GetSeqRecursive(subGroup, currentGroupSequence, allQuestions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,36 +0,0 @@
|
|||||||
using System.Xml.Serialization;
|
|
||||||
|
|
||||||
namespace TechHelper.Client.Exam.QuestionSimple
|
|
||||||
{
|
|
||||||
|
|
||||||
[XmlRoot("QG")]
|
|
||||||
public class QuestionGroup
|
|
||||||
{
|
|
||||||
[XmlElement("T")]
|
|
||||||
public string Title { get; set; }
|
|
||||||
[XmlElement("QR")]
|
|
||||||
public string QuestionReference { get; set; } = "";
|
|
||||||
[XmlArray("SQs")]
|
|
||||||
[XmlArrayItem("SQ")]
|
|
||||||
public List<SubQuestion> SubQuestions { get; set; } = new List<SubQuestion>();
|
|
||||||
[XmlArray("SQGs")]
|
|
||||||
[XmlArrayItem("QG")]
|
|
||||||
public List<QuestionGroup> SubQuestionGroups { get; set; } = new List<QuestionGroup>();
|
|
||||||
}
|
|
||||||
public class SubQuestion
|
|
||||||
{
|
|
||||||
[XmlElement("T")]
|
|
||||||
public string Stem { get; set; }
|
|
||||||
[XmlArray("Os")]
|
|
||||||
[XmlArrayItem("O")]
|
|
||||||
public List<Option> Options { get; set; } = new List<Option>();
|
|
||||||
[XmlElement("SA")]
|
|
||||||
public string SampleAnswer { get; set; } = "";
|
|
||||||
}
|
|
||||||
public class Option
|
|
||||||
{
|
|
||||||
[XmlAttribute("V")]
|
|
||||||
public string Value { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -9,11 +9,11 @@ namespace BlazorProducts.Client.HttpInterceptor
|
|||||||
public class HttpInterceptorHandlerService : DelegatingHandler
|
public class HttpInterceptorHandlerService : DelegatingHandler
|
||||||
{
|
{
|
||||||
private readonly NavigationManager _navManager;
|
private readonly NavigationManager _navManager;
|
||||||
private readonly RefreshTokenService2 _refreshTokenService;
|
private readonly IRefreshTokenService _refreshTokenService;
|
||||||
|
|
||||||
public HttpInterceptorHandlerService(
|
public HttpInterceptorHandlerService(
|
||||||
NavigationManager navManager,
|
NavigationManager navManager,
|
||||||
RefreshTokenService2 refreshTokenService)
|
IRefreshTokenService refreshTokenService)
|
||||||
{
|
{
|
||||||
_navManager = navManager;
|
_navManager = navManager;
|
||||||
_refreshTokenService = refreshTokenService;
|
_refreshTokenService = refreshTokenService;
|
||||||
@@ -28,7 +28,6 @@ namespace BlazorProducts.Client.HttpInterceptor
|
|||||||
var token = await _refreshTokenService.TryRefreshToken();
|
var token = await _refreshTokenService.TryRefreshToken();
|
||||||
if (!string.IsNullOrEmpty(token))
|
if (!string.IsNullOrEmpty(token))
|
||||||
{
|
{
|
||||||
|
|
||||||
request.Headers.Authorization = new AuthenticationHeaderValue("bearer", token);
|
request.Headers.Authorization = new AuthenticationHeaderValue("bearer", token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,8 +66,15 @@ namespace BlazorProducts.Client.HttpInterceptor
|
|||||||
// 400 Bad Request error. Often, you don't navigate for this; you display validation errors on the UI.
|
// 400 Bad Request error. Often, you don't navigate for this; you display validation errors on the UI.
|
||||||
break;
|
break;
|
||||||
case HttpStatusCode.Unauthorized:
|
case HttpStatusCode.Unauthorized:
|
||||||
// 401 Unauthorized error. Navigate to an unauthorized page or login page.
|
var token = await _refreshTokenService.TryRefreshToken();
|
||||||
_navManager.NavigateTo("/unauthorized"); // Or: _navManager.NavigateTo("/authentication/login");
|
if (!string.IsNullOrEmpty(token))
|
||||||
|
{
|
||||||
|
_navManager.NavigateTo(_navManager.Uri, forceLoad: true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_navManager.NavigateTo("/unauthorized");
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// For all other errors, navigate to a general error page.
|
// For all other errors, navigate to a general error page.
|
||||||
|
@@ -20,13 +20,12 @@ namespace TechHelper.Client.HttpRepository
|
|||||||
private readonly ILocalStorageService _localStorageService;
|
private readonly ILocalStorageService _localStorageService;
|
||||||
private readonly NavigationManager _navigationManager;
|
private readonly NavigationManager _navigationManager;
|
||||||
|
|
||||||
// 构造函数现在直接接收 HttpClient
|
public AuthenticationClientService(HttpClient client,
|
||||||
public AuthenticationClientService(HttpClient client, // <-- 修正点:直接注入 HttpClient
|
|
||||||
AuthenticationStateProvider authenticationStateProvider,
|
AuthenticationStateProvider authenticationStateProvider,
|
||||||
ILocalStorageService localStorageService,
|
ILocalStorageService localStorageService,
|
||||||
NavigationManager navigationManager)
|
NavigationManager navigationManager)
|
||||||
{
|
{
|
||||||
_client = client; // <-- 修正点:直接赋值
|
_client = client;
|
||||||
_localStorageService = localStorageService;
|
_localStorageService = localStorageService;
|
||||||
_stateProvider = authenticationStateProvider;
|
_stateProvider = authenticationStateProvider;
|
||||||
_navigationManager = navigationManager;
|
_navigationManager = navigationManager;
|
||||||
@@ -34,8 +33,6 @@ namespace TechHelper.Client.HttpRepository
|
|||||||
|
|
||||||
public async Task<AuthResponseDto> LoginAsync(UserForAuthenticationDto userForAuthenticationDto)
|
public async Task<AuthResponseDto> LoginAsync(UserForAuthenticationDto userForAuthenticationDto)
|
||||||
{
|
{
|
||||||
// 移除 using (_client = _clientFactory.CreateClient("Default"))
|
|
||||||
// _client 已经是注入的实例,直接使用它
|
|
||||||
var reponse = await _client.PostAsJsonAsync("account/login",
|
var reponse = await _client.PostAsJsonAsync("account/login",
|
||||||
userForAuthenticationDto);
|
userForAuthenticationDto);
|
||||||
|
|
||||||
@@ -71,7 +68,6 @@ namespace TechHelper.Client.HttpRepository
|
|||||||
|
|
||||||
public async Task<string> RefreshTokenAsync()
|
public async Task<string> RefreshTokenAsync()
|
||||||
{
|
{
|
||||||
// 移除 using (_client = _clientFactory.CreateClient("Default"))
|
|
||||||
var token = _localStorageService.GetItem<string>("authToken");
|
var token = _localStorageService.GetItem<string>("authToken");
|
||||||
var refreshToken = _localStorageService.GetItem<string>("refreshToken");
|
var refreshToken = _localStorageService.GetItem<string>("refreshToken");
|
||||||
|
|
||||||
@@ -96,7 +92,6 @@ namespace TechHelper.Client.HttpRepository
|
|||||||
|
|
||||||
public async Task<ResponseDto> RegisterUserAsync(UserForRegistrationDto userForRegistrationDto)
|
public async Task<ResponseDto> RegisterUserAsync(UserForRegistrationDto userForRegistrationDto)
|
||||||
{
|
{
|
||||||
// 移除 using (_client = _clientFactory.CreateClient("Default"))
|
|
||||||
userForRegistrationDto.ClientURI = Path.Combine(
|
userForRegistrationDto.ClientURI = Path.Combine(
|
||||||
_navigationManager.BaseUri, "emailconfirmation");
|
_navigationManager.BaseUri, "emailconfirmation");
|
||||||
|
|
||||||
@@ -167,6 +162,9 @@ namespace TechHelper.Client.HttpRepository
|
|||||||
((AuthStateProvider)_stateProvider).NotifyUserAuthentication(
|
((AuthStateProvider)_stateProvider).NotifyUserAuthentication(
|
||||||
result.Token);
|
result.Token);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(
|
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(
|
||||||
"bearer", result.Token);
|
"bearer", result.Token);
|
||||||
|
|
||||||
|
@@ -24,16 +24,15 @@ namespace TechHelper.Client.HttpRepository
|
|||||||
var authState = await _authenticationStateProvider.Value.GetAuthenticationStateAsync();
|
var authState = await _authenticationStateProvider.Value.GetAuthenticationStateAsync();
|
||||||
var user = authState.User;
|
var user = authState.User;
|
||||||
|
|
||||||
// 如果 user 或 claims 为空,表示用户未认证,直接返回空字符串
|
|
||||||
if (user?.Identity == null || !user.Identity.IsAuthenticated)
|
if (user?.Identity == null || !user.Identity.IsAuthenticated)
|
||||||
{
|
{
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
var expClaim = user.FindFirst(c => c.Type.Equals("exp"))?.Value; // 使用 ?. 防止空引用
|
var expClaim = user.FindFirst(c => c.Type.Equals("exp"))?.Value;
|
||||||
if (string.IsNullOrEmpty(expClaim))
|
if (string.IsNullOrEmpty(expClaim))
|
||||||
{
|
{
|
||||||
return string.Empty; // 没有过期时间声明,也直接返回
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
var expTime = DateTimeOffset.FromUnixTimeSeconds(
|
var expTime = DateTimeOffset.FromUnixTimeSeconds(
|
||||||
@@ -41,9 +40,11 @@ namespace TechHelper.Client.HttpRepository
|
|||||||
|
|
||||||
var diff = expTime - DateTime.UtcNow;
|
var diff = expTime - DateTime.UtcNow;
|
||||||
|
|
||||||
// 只有当令牌即将过期时才尝试刷新
|
|
||||||
|
var n = DateTime.UtcNow;
|
||||||
|
|
||||||
if (diff.TotalMinutes <= 2)
|
if (diff.TotalMinutes <= 2)
|
||||||
return await _authenticationClientService.Value.RefreshTokenAsync(); // 访问 .Value 来调用方法
|
return await _authenticationClientService.Value.RefreshTokenAsync();
|
||||||
|
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
@@ -1,46 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Components.Authorization;
|
|
||||||
|
|
||||||
namespace TechHelper.Client.HttpRepository
|
|
||||||
{
|
|
||||||
public class RefreshTokenService2
|
|
||||||
{
|
|
||||||
private readonly Lazy<AuthenticationStateProvider> _authenticationStateProvider;
|
|
||||||
private readonly Lazy<IAuthenticationClientService> _authenticationClientService;
|
|
||||||
|
|
||||||
public RefreshTokenService2(IServiceProvider serviceProvider)
|
|
||||||
{
|
|
||||||
_authenticationStateProvider = new Lazy<AuthenticationStateProvider>(
|
|
||||||
() => serviceProvider.GetRequiredService<AuthenticationStateProvider>());
|
|
||||||
|
|
||||||
_authenticationClientService = new Lazy<IAuthenticationClientService>(
|
|
||||||
() => serviceProvider.GetRequiredService<IAuthenticationClientService>());
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string> TryRefreshToken()
|
|
||||||
{
|
|
||||||
var authState = await _authenticationStateProvider.Value.GetAuthenticationStateAsync();
|
|
||||||
var user = authState.User;
|
|
||||||
|
|
||||||
if (user?.Identity == null || !user.Identity.IsAuthenticated)
|
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
var expClaim = user.FindFirst(c => c.Type.Equals("exp"))?.Value;
|
|
||||||
if (string.IsNullOrEmpty(expClaim))
|
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
var expTime = DateTimeOffset.FromUnixTimeSeconds(
|
|
||||||
Convert.ToInt64(expClaim));
|
|
||||||
|
|
||||||
var diff = expTime - DateTime.UtcNow;
|
|
||||||
|
|
||||||
if (diff.TotalMinutes <= 2)
|
|
||||||
return await _authenticationClientService.Value.RefreshTokenAsync();
|
|
||||||
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
0
TechHelper.Client/Pages/Author/Component.razor
Normal file
0
TechHelper.Client/Pages/Author/Component.razor
Normal file
@@ -1,48 +1,59 @@
|
|||||||
@page "/login"
|
@page "/login"
|
||||||
|
|
||||||
<MudText Typo="Typo.h2"> Login Account </MudText>
|
<div class="d-flex flex-column flex-grow-1 page-containerr">
|
||||||
|
|
||||||
<EditForm Model="@_userForAuth" OnValidSubmit="Logining" FormName="LoginingForm">
|
<div class="d-flex mud-paper-80-percent-centeredd w-75">
|
||||||
|
<MudPaper Class="d-flex flex-grow-1 ma-0 pa-0" style="background-color:transparent; min-height:100%" Elevation="0">
|
||||||
|
|
||||||
|
<MudGrid Class="d-flex flex-grow-1" style="background-color:transparent; min-height:100%">
|
||||||
|
<MudItem xs="12" sm="4" style="background-color:transparent">
|
||||||
|
<MudPaper Class="d-flex flex-column align-start justify-start mud-width-full h-100 pa-8" Elevation="0" Style="background-color:transparent">
|
||||||
|
<MudText Style="color:#ffffff" Typo="Typo.h4">TechHelper</MudText>
|
||||||
|
<MudText Style="color:#ffffff" Typo="Typo.body2">轻松管理,高效学习。</MudText>
|
||||||
|
<MudSpacer />
|
||||||
|
<MudText Style="color:#ffffff" Typo="Typo.h4">教育不是注满一桶水,</MudText>
|
||||||
|
<MudText Style="color:#ffffff" Typo="Typo.h4"> 而是点燃一把火。</MudText>
|
||||||
|
<MudImage Alt="Hello World" Fluid="true" Src="ref/UnFinish.png" />
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
|
||||||
|
<MudItem xs="12" sm="8" style="background-color:transparent">
|
||||||
|
|
||||||
|
<MudPaper Class="d-flex flex-row flex-grow-1 justify-center rounded-xl px-0 mud-height-full" >
|
||||||
|
|
||||||
|
<EditForm Model="@_userForAuth" OnValidSubmit="Logining" FormName="LoginingForm" class="w-100">
|
||||||
<DataAnnotationsValidator />
|
<DataAnnotationsValidator />
|
||||||
<MudGrid>
|
<MudPaper Class="d-flex flex-column flex-grow-1 rounded-xl px-15 justify-content-center pt-15 w-100 " Elevation="0" Outlined="false">
|
||||||
<MudItem xs="12" sm="7">
|
<MudText Typo="Typo.h5"> <b>登录账户</b> </MudText>
|
||||||
<MudCard>
|
|
||||||
<MudCardContent>
|
|
||||||
<MudTextField Label="Email" Class="mt-3"
|
<MudTextField Label="Email" Class="mt-3"
|
||||||
@bind-Value="_userForAuth.Email" For="@(() => _userForAuth.Email)" />
|
@bind-Value="_userForAuth.Email" For="@(() => _userForAuth.Email)" />
|
||||||
<MudTextField Label="Password" HelperText="Choose a strong password" Class="mt-3"
|
<MudTextField Label="Password" HelperText="Choose a strong password" Class="mt-3"
|
||||||
@bind-Value="_userForAuth.Password" For="@(() => _userForAuth.Password)" InputType="InputType.Password" />
|
@bind-Value="_userForAuth.Password" For="@(() => _userForAuth.Password)" InputType="InputType.Password" />
|
||||||
</MudCardContent>
|
|
||||||
<MudCardActions>
|
|
||||||
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary" Class="ml-auto">Register</MudButton>
|
|
||||||
</MudCardActions>
|
|
||||||
|
|
||||||
<div class="nav-item px-3">
|
|
||||||
<NavLink class="nav-link" href="forgotpassword">
|
<MudStack Row=true Class="align-content-center justify-content-start my-3">
|
||||||
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span>Forgot Password
|
<MudCheckBox @bind-Value="Basic_CheckBox2" Color="Color.Primary"></MudCheckBox>
|
||||||
</NavLink>
|
<MudText Typo="Typo.body2" Align=Align.Center Class="align-content-center"> 点击登录,即表示你同意我们的服务条款和隐私政策。 </MudText>
|
||||||
</div>
|
</MudStack>
|
||||||
</MudCard>
|
|
||||||
</MudItem>
|
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary" Class="w-100 mx-0 my-3 justify-content-center rounded-pill">LOGIN</MudButton>
|
||||||
<MudItem xs="12" sm="5">
|
|
||||||
<MudPaper Class="pa-4 mud-height-full">
|
|
||||||
<MudText Typo="Typo.subtitle2">Validation Summary</MudText>
|
|
||||||
@if (!ShowRegistrationErrors)
|
|
||||||
{
|
<MudText Typo="Typo.body2" Class="justify-content-center mx-auto mt-5" Color="Color.Dark">
|
||||||
<MudText Color="Color.Success">Success</MudText>
|
还没有账户?
|
||||||
}
|
<a href="/register" style="color: blue;">Sign in</a>
|
||||||
else
|
|
||||||
{
|
|
||||||
<MudText Color="@Color.Error">
|
|
||||||
<ValidationSummary />
|
|
||||||
</MudText>
|
</MudText>
|
||||||
}
|
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12">
|
</EditForm>
|
||||||
<MudText Typo="Typo.body2" Align="Align.Center">
|
|
||||||
Fill out the form correctly to see the success message.
|
</MudPaper>
|
||||||
</MudText>
|
|
||||||
</MudItem>
|
</MudItem>
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
</EditForm>
|
</MudPaper>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@@ -16,6 +16,8 @@ namespace TechHelper.Client.Pages.Author
|
|||||||
[Inject]
|
[Inject]
|
||||||
public NavigationManager NavigationManager { get; set; }
|
public NavigationManager NavigationManager { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
public bool Basic_CheckBox2 { get; set; } = true;
|
||||||
public bool ShowRegistrationErrors { get; set; }
|
public bool ShowRegistrationErrors { get; set; }
|
||||||
public string Error { get; set; }
|
public string Error { get; set; }
|
||||||
|
|
||||||
|
29
TechHelper.Client/Pages/Author/Login.razor.css
Normal file
29
TechHelper.Client/Pages/Author/Login.razor.css
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
.page-containerr {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%; /* <20>ӿڸ߶ȣ<DFB6>ȷ<EFBFBD><C8B7><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>㹻<EFBFBD>ĸ߶<C4B8><DFB6><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ԫ<EFBFBD><D4AA> */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column; /* <20><>ֱ<EFBFBD><D6B1><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ԫ<EFBFBD><D4AA> */
|
||||||
|
align-items: center; /* ˮƽ<CBAE><C6BD><EFBFBD><EFBFBD> */
|
||||||
|
justify-content: center; /* <20><>ֱ<EFBFBD><D6B1><EFBFBD><EFBFBD> */
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mud-paper-full-width {
|
||||||
|
width: 80%; /* <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD><D2BB>paper<65>Ŀ<EFBFBD><C4BF><EFBFBD> */
|
||||||
|
height: auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mud-paper-80-percent-centeredd {
|
||||||
|
width: 80%;
|
||||||
|
min-height: 80%;
|
||||||
|
padding: 0px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: #959dff;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 40px;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
@@ -1,18 +1,37 @@
|
|||||||
@page "/register"
|
@page "/register"
|
||||||
@using System.ComponentModel.DataAnnotations
|
@using System.ComponentModel.DataAnnotations
|
||||||
|
@using Microsoft.AspNetCore.Components
|
||||||
@using Entities.Contracts
|
@using Entities.Contracts
|
||||||
|
@using System.Globalization;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
|
|
||||||
|
<div class="d-flex flex-grow-1 page-container">
|
||||||
|
|
||||||
<MudText Typo="Typo.h2"> Create Account </MudText>
|
<div class="d-flex mud-paper-80-percent-centered w-75">
|
||||||
|
<MudPaper Class="d-flex flex-grow-1 ma-0 pa-0" style="background-color:transparent; min-height:100%" Elevation="0">
|
||||||
|
|
||||||
<EditForm Model="@_userForRegistration" OnValidSubmit="Register" FormName="RegistrationForm">
|
<MudGrid Class="d-flex flex-grow-1" style="background-color:transparent; min-height:100%">
|
||||||
|
<MudItem xs="12" sm="4" style="background-color:transparent">
|
||||||
|
<MudPaper Class="d-flex flex-column align-start justify-start mud-width-full h-100 pa-8" Elevation="0" Style="background-color:transparent">
|
||||||
|
<MudText Style="color:#ffffff" Typo="Typo.h4">TechHelper</MudText>
|
||||||
|
<MudText Style="color:#ffffff" Typo="Typo.body2">快速注册,开始你的管理之旅。</MudText>
|
||||||
|
<MudSpacer />
|
||||||
|
<MudText Style="color:#ffffff" Typo="Typo.h4">学而不思则罔,</MudText>
|
||||||
|
<MudText Style="color:#ffffff" Typo="Typo.h4"> 思而不学则殆。</MudText>
|
||||||
|
<MudImage Alt="Hello World" Fluid="true" Src="ref/UnFinish.png" />
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
|
||||||
|
<MudItem xs="12" sm="8" style="background-color:transparent">
|
||||||
|
|
||||||
|
<MudPaper Class="d-flex flex-row flex-grow-1 justify-center rounded-xl px-20 mud-height-full">
|
||||||
|
<EditForm Model="@_userForRegistration" OnValidSubmit="Register" FormName="RegistrationForm" class="w-100">
|
||||||
<DataAnnotationsValidator />
|
<DataAnnotationsValidator />
|
||||||
<MudGrid>
|
<MudPaper Class="d-flex flex-column flex-grow-1 rounded-xl px-5 justify-content-center pt-15 w-100 " Elevation="0" Outlined="false">
|
||||||
<MudItem xs="12" sm="7">
|
<MudText Typo="Typo.h5"> <b>注册账户</b> </MudText>
|
||||||
<MudCard>
|
|
||||||
<MudCardContent>
|
|
||||||
<MudTextField Label="Name" HelperText="Max. 8 characters"
|
<MudTextField Label="Name" HelperText="Max. 8 characters"
|
||||||
@bind-Value="_userForRegistration.Name" For="@(() => _userForRegistration.Email)" />
|
@bind-Value="_userForRegistration.Name" For="@(() => _userForRegistration.Email)" />
|
||||||
<MudTextField Label="Email" Class="mt-3"
|
<MudTextField Label="Email" Class="mt-3"
|
||||||
@@ -21,76 +40,57 @@
|
|||||||
@bind-Value="_userForRegistration.Password" For="@(() => _userForRegistration.Password)" InputType="InputType.Password" />
|
@bind-Value="_userForRegistration.Password" For="@(() => _userForRegistration.Password)" InputType="InputType.Password" />
|
||||||
<MudTextField Label="Password" HelperText="Repeat the password" Class="mt-3"
|
<MudTextField Label="Password" HelperText="Repeat the password" Class="mt-3"
|
||||||
@bind-Value="_userForRegistration.ConfirmPassword" For="@(() => _userForRegistration.ConfirmPassword)" InputType="InputType.Password" />
|
@bind-Value="_userForRegistration.ConfirmPassword" For="@(() => _userForRegistration.ConfirmPassword)" InputType="InputType.Password" />
|
||||||
<MudRadioGroup T="UserRoles" Label="Roles" @bind-Value="_userForRegistration.Roles">
|
<MudChipSet T="UserRoles" @bind-SelectedValue="_userForRegistration.Roles" CheckMark SelectionMode="SelectionMode.SingleSelection" Class="w-100">
|
||||||
@foreach (UserRoles item in Enum.GetValues(typeof(UserRoles)))
|
<MudChip Text="Student" Color="Color.Primary" Value="@UserRoles.Student">Student</MudChip>
|
||||||
{
|
<MudChip Text="Teacher" Color="Color.Secondary" Value="@UserRoles.Teacher">Teacher</MudChip>
|
||||||
if (item != UserRoles.Administrator)
|
</MudChipSet>
|
||||||
{
|
|
||||||
<MudRadio Value="@item">@item.ToString()</MudRadio>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</MudRadioGroup>
|
|
||||||
<MudStack Row="true">
|
<MudStack Row="true">
|
||||||
|
<MudSelect T="GradeEnum" Value="grade" Label="Select Grade" AdornmentColor="Color.Secondary" ValueChanged="HandleSelectedValuesChanged">
|
||||||
|
@foreach (GradeEnum item in Enum.GetValues(typeof(GradeEnum)))
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@item">@item</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
|
||||||
|
|
||||||
|
<MudSelect T="byte" Value="selectclass" Label="Select Class" AdornmentColor="Color.Secondary" ValueChanged="HandleListSelectedValuesChanged">
|
||||||
|
@foreach (byte item in Classes)
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@item">@item</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
|
||||||
|
|
||||||
<MudTextField Label="Class"
|
|
||||||
HelperText="Enter a class number between 1 and 14."
|
|
||||||
Class="mt-3"
|
|
||||||
@bind-Value="_userForRegistration.Class"
|
|
||||||
For="@(() => _userForRegistration.Class)"
|
|
||||||
InputType="InputType.Number"
|
|
||||||
Required="true"
|
|
||||||
RequiredError="Class is required." />
|
|
||||||
|
|
||||||
<MudTextField Label="Grade"
|
|
||||||
HelperText="Enter a grade number between 1 and 6."
|
|
||||||
Class="mt-3"
|
|
||||||
@bind-Value="_userForRegistration.Grade"
|
|
||||||
For="@(() => _userForRegistration.Grade)"
|
|
||||||
InputType="InputType.Number"
|
|
||||||
Required="true"
|
|
||||||
RequiredError="Grade is required." />
|
|
||||||
</MudStack>
|
</MudStack>
|
||||||
|
|
||||||
<MudTextField Label="Phone Number"
|
|
||||||
HelperText="Enter your phone number (optional, 7-20 digits)."
|
|
||||||
Class="mt-3"
|
|
||||||
@bind-Value="_userForRegistration.PhoneNumber"
|
|
||||||
For="@(() => _userForRegistration.PhoneNumber)"
|
|
||||||
InputType="InputType.Telephone" /> <MudTextField Label="Home Address"
|
|
||||||
HelperText="Enter your home address (optional, max 200 characters)."
|
|
||||||
Class="mt-3"
|
|
||||||
@bind-Value="_userForRegistration.HomeAddress"
|
|
||||||
For="@(() => _userForRegistration.HomeAddress)"
|
|
||||||
Lines="3" />
|
|
||||||
</MudCardContent>
|
|
||||||
<MudCardActions>
|
|
||||||
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary" Class="ml-auto">Register</MudButton>
|
|
||||||
</MudCardActions>
|
|
||||||
</MudCard>
|
|
||||||
|
|
||||||
</MudItem>
|
<MudStack Row=true Class="align-content-center justify-content-start my-3">
|
||||||
<MudItem xs="12" sm="5">
|
<MudCheckBox @bind-Value="Basic_CheckBox2" Color="Color.Primary"></MudCheckBox>
|
||||||
<MudPaper Class="pa-4 mud-height-full">
|
<MudText Typo="Typo.body2" Align=Align.Center Class="align-content-center"> 点击注册,即表示你同意我们的服务条款和隐私政策。 </MudText>
|
||||||
<MudText Typo="Typo.subtitle2">Validation Summary</MudText>
|
</MudStack>
|
||||||
@if (success)
|
|
||||||
{
|
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary" Class="w-100 mx-0 my-3 justify-content-center rounded-pill">LOGIN</MudButton>
|
||||||
<MudText Color="Color.Success">Success</MudText>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<MudText Color="@Color.Error">
|
<MudText Typo="Typo.body2" Class="justify-content-center mx-auto mt-5" Color="Color.Dark">
|
||||||
<ValidationSummary />
|
已有账户?
|
||||||
|
<a href="/login" style="color: blue;">Sign up</a>
|
||||||
</MudText>
|
</MudText>
|
||||||
}
|
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudText Typo="Typo.body2" Align="Align.Center">
|
|
||||||
Fill out the form correctly to see the success message.
|
|
||||||
</MudText>
|
|
||||||
</MudItem>
|
|
||||||
</MudGrid>
|
|
||||||
|
|
||||||
</EditForm>
|
</EditForm>
|
||||||
|
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@@ -5,6 +5,7 @@ using MudBlazor;
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using TechHelper.Features;
|
using TechHelper.Features;
|
||||||
using Entities.Contracts;
|
using Entities.Contracts;
|
||||||
|
using TechHelper.Client.Services;
|
||||||
|
|
||||||
namespace TechHelper.Client.Pages.Author
|
namespace TechHelper.Client.Pages.Author
|
||||||
{
|
{
|
||||||
@@ -12,14 +13,19 @@ namespace TechHelper.Client.Pages.Author
|
|||||||
{
|
{
|
||||||
|
|
||||||
private UserForRegistrationDto _userForRegistration = new UserForRegistrationDto();
|
private UserForRegistrationDto _userForRegistration = new UserForRegistrationDto();
|
||||||
|
private GradeEnum grade = GradeEnum.Unknown;
|
||||||
[Inject]
|
[Inject]
|
||||||
public IAuthenticationClientService AuthenticationService { get; set; }
|
public IAuthenticationClientService AuthenticationService { get; set; }
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
public NavigationManager NavigationManager { get; set; }
|
public NavigationManager NavigationManager { get; set; }
|
||||||
|
[Inject]
|
||||||
|
public IClassServices ClassServices { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
public bool Basic_CheckBox2 { get; set; } = true;
|
||||||
|
public byte selectclass { get; set; } = 0;
|
||||||
|
public List<byte> Classes { get; set; } = new List<byte>();
|
||||||
|
|
||||||
public bool ShowRegistrationErrors { get; set; }
|
public bool ShowRegistrationErrors { get; set; }
|
||||||
public string[] Errors { get; set; }
|
public string[] Errors { get; set; }
|
||||||
@@ -51,6 +57,25 @@ namespace TechHelper.Client.Pages.Author
|
|||||||
[Inject]
|
[Inject]
|
||||||
public IEmailSender emailSender { get; set; }
|
public IEmailSender emailSender { get; set; }
|
||||||
|
|
||||||
|
private async void HandleSelectedValuesChanged(GradeEnum selectedValues)
|
||||||
|
{
|
||||||
|
grade = selectedValues;
|
||||||
|
Snackbar.Add("<22><><EFBFBD>ȴ<EFBFBD>,<2C><><EFBFBD>ڻ<EFBFBD>ȡ<EFBFBD><C8A1><EFBFBD>꼶<EFBFBD>༶", Severity.Info);
|
||||||
|
var result = await ClassServices.GetGradeClasses((byte)selectedValues);
|
||||||
|
if (result.Status)
|
||||||
|
{
|
||||||
|
Classes = result.Result as List<byte> ?? new List<byte>();
|
||||||
|
Snackbar.Add("<22><>ȡ<EFBFBD>ɹ<EFBFBD>", Severity.Success);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Snackbar.Add("<22><>ȡʧ<C8A1><CAA7>", Severity.Error);
|
||||||
|
}
|
||||||
|
private void HandleListSelectedValuesChanged(byte selectedValues)
|
||||||
|
{
|
||||||
|
selectclass = selectedValues;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public async void SendEmail()
|
public async void SendEmail()
|
||||||
{
|
{
|
||||||
string eamilTo = "1928360026@qq.com";
|
string eamilTo = "1928360026@qq.com";
|
||||||
|
29
TechHelper.Client/Pages/Author/Registration.razor.css
Normal file
29
TechHelper.Client/Pages/Author/Registration.razor.css
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
.page-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%; /* <20>ӿڸ߶ȣ<DFB6>ȷ<EFBFBD><C8B7><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>㹻<EFBFBD>ĸ߶<C4B8><DFB6><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ԫ<EFBFBD><D4AA> */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column; /* <20><>ֱ<EFBFBD><D6B1><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ԫ<EFBFBD><D4AA> */
|
||||||
|
align-items: center; /* ˮƽ<CBAE><C6BD><EFBFBD><EFBFBD> */
|
||||||
|
justify-content: center; /* <20><>ֱ<EFBFBD><D6B1><EFBFBD><EFBFBD> */
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mud-paper-full-width {
|
||||||
|
width: 80%; /* <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD><D2BB>paper<65>Ŀ<EFBFBD><C4BF><EFBFBD> */
|
||||||
|
height: auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mud-paper-80-percent-centered {
|
||||||
|
width: 80%;
|
||||||
|
min-height: 80%;
|
||||||
|
padding: 0px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: #959dff;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 40px;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
34
TechHelper.Client/Pages/Author/RestoreUserRole.razor
Normal file
34
TechHelper.Client/Pages/Author/RestoreUserRole.razor
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
@using TechHelper.Client.Services
|
||||||
|
<MudPaper Height="50" Width="50" Outlined="true">
|
||||||
|
|
||||||
|
<MudButton Variant="Variant.Filled" OnClick="Restore"> Restore Role </MudButton>
|
||||||
|
|
||||||
|
</MudPaper>
|
||||||
|
@code {
|
||||||
|
[Inject]
|
||||||
|
public IAuthenticationClientService Authentication { get; set; }
|
||||||
|
[Inject]
|
||||||
|
public IUserServices UserServices { get; set; }
|
||||||
|
[Inject]
|
||||||
|
public ISnackbar Snackbar { get; set; }
|
||||||
|
private async Task Restore()
|
||||||
|
{
|
||||||
|
var result = await UserServices.RestoreUserInfo();
|
||||||
|
if (result.Status)
|
||||||
|
{
|
||||||
|
|
||||||
|
Snackbar.Add("更新成功", Severity.Success);
|
||||||
|
|
||||||
|
}
|
||||||
|
else
|
||||||
|
Snackbar.Add("更新失败", Severity.Error);
|
||||||
|
var token = await Authentication.RefreshTokenAsync();
|
||||||
|
if (token != null)
|
||||||
|
Snackbar.Add("刷新令牌成功", Severity.Success);
|
||||||
|
else
|
||||||
|
Snackbar.Add("刷新令牌失败,你可以手动刷新", Severity.Warning);
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
143
TechHelper.Client/Pages/Common/Exam/AssignmentInfoCard.razor
Normal file
143
TechHelper.Client/Pages/Common/Exam/AssignmentInfoCard.razor
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
<MudPaper Class="rounded-xl w-100 px-10 ma-3 pt-5" Elevation="5" Height="170px" Style="background-color:#6bc6be">
|
||||||
|
|
||||||
|
<MudGrid Class="h-100">
|
||||||
|
|
||||||
|
<MudItem xs="12" sm="2" Class="h-100 pa-1 mt-1">
|
||||||
|
<MudStack Class="h-100">
|
||||||
|
<MudText Style="color:white"> 期中测试BETA版本 </MudText>
|
||||||
|
<MudText Style="color:white" Typo="Typo.h3"><b> 75 </b></MudText>
|
||||||
|
|
||||||
|
<MudPaper Elevation=0 Class="h-100 w-100" Style="background-color:transparent">
|
||||||
|
<MudStack Class="h-100" Row=true>
|
||||||
|
<MudPaper Elevation=0 Height="100%" Width="100%" Style="background-color:transparent">
|
||||||
|
<MudPaper Elevation=0 Height="50%" Style="background-color:transparent">
|
||||||
|
<MudText Style="color:#9ed5f7" Typo="Typo.body2">
|
||||||
|
TotalNumber:
|
||||||
|
<span style="color: #fefefe;">15</span>
|
||||||
|
</MudText>
|
||||||
|
</MudPaper>
|
||||||
|
<MudPaper Elevation=0 Height="50%" Style="background-color:transparent">
|
||||||
|
<MudText Style="color:#9ed5f7" Typo="Typo.body2">
|
||||||
|
TotalScore:
|
||||||
|
<span style="color: #fefefe;">15</span>
|
||||||
|
</MudText>
|
||||||
|
</MudPaper>
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
|
<MudPaper Elevation=0 Height="100%" Width="100%" Style="background-color:transparent">
|
||||||
|
<MudPaper Elevation=0 Height="50%" Style="background-color:transparent">
|
||||||
|
<MudText Style="color:#9ed5f7" Typo="Typo.body2">
|
||||||
|
中位数:
|
||||||
|
<span style="color: #fefefe;">15</span>
|
||||||
|
</MudText>
|
||||||
|
</MudPaper>
|
||||||
|
<MudPaper Elevation=0 Height="50%" Style="background-color:transparent">
|
||||||
|
<MudText Style="color:#9ed5f7" Typo="Typo.body2">
|
||||||
|
方差:
|
||||||
|
<span style="color: #fefefe;">15</span>
|
||||||
|
</MudText>
|
||||||
|
</MudPaper>
|
||||||
|
</MudPaper>
|
||||||
|
</MudStack>
|
||||||
|
</MudPaper>
|
||||||
|
</MudStack>
|
||||||
|
</MudItem>
|
||||||
|
|
||||||
|
|
||||||
|
<MudItem xs="12" sm="9">
|
||||||
|
<MudPaper Style="background-color:transparent" Class="w-100 mt-n3" Height="100%" Elevation="0">
|
||||||
|
<MudChart ChartType="ChartType.Line" Class="pt-0" ChartSeries="@Series" XAxisLabels="@XAxisLabels" CanHideSeries
|
||||||
|
Height="150px" Width="100%" AxisChartOptions="_axisChartOptions" ChartOptions="options">
|
||||||
|
<CustomGraphics>
|
||||||
|
<style>
|
||||||
|
.heavy {
|
||||||
|
font: normal 12px helvetica;
|
||||||
|
fill: rgb(255,255,255);
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<text x="60" y="15" class="heavy"> 成绩的整体分布情况 </text>
|
||||||
|
</CustomGraphics>
|
||||||
|
</MudChart>
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="1">
|
||||||
|
<MudChipSet T="string" SelectedValuesChanged="HandleSelectedValuesChanged" SelectedValues="@_selected" SelectionMode="SelectionMode.MultiSelection" CheckMark="true">
|
||||||
|
<MudChip Size="Size.Small" Text="类型错误数量分布" Variant="Variant.Text" Color="Color.Default">类型分布</MudChip>
|
||||||
|
<MudChip Size="Size.Small" Text="类型错误成绩分布" Variant="Variant.Text" Color="Color.Primary">课时分布</MudChip>
|
||||||
|
<MudChip Size="Size.Small" Text="pink" Variant="Variant.Text" Color="Color.Secondary">成绩趋势</MudChip>
|
||||||
|
<MudChip Size="Size.Small" Text="blue" Variant="Variant.Text" Color="Color.Info">分值区间</MudChip>
|
||||||
|
<MudChip Size="Size.Small" Text="green" Variant="Variant.Text" Color="Color.Success">Success</MudChip>
|
||||||
|
<MudChip Size="Size.Small" Text="orange" Variant="Variant.Text" Color="Color.Warning">Warning</MudChip>
|
||||||
|
<MudChip Size="Size.Small" Text="red" Variant="Variant.Text" Color="Color.Error">Error</MudChip>
|
||||||
|
<MudChip Size="Size.Small" Text="black" Variant="Variant.Text" Color="Color.Dark">Dark</MudChip>
|
||||||
|
</MudChipSet>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
|
|
||||||
|
@code {
|
||||||
|
public double[] data = { 25, 77, 28, 5 };
|
||||||
|
public string[] labels = { "Oil", "Coal", "Gas", "Biomass" };
|
||||||
|
private AxisChartOptions _axisChartOptions = new AxisChartOptions
|
||||||
|
{
|
||||||
|
};
|
||||||
|
private ChartOptions options = new ChartOptions
|
||||||
|
{
|
||||||
|
InterpolationOption = InterpolationOption.NaturalSpline,
|
||||||
|
YAxisFormat = "c2",
|
||||||
|
ShowLegend = false,
|
||||||
|
YAxisLines = false,
|
||||||
|
XAxisLines = false,
|
||||||
|
XAxisLabelPosition = XAxisLabelPosition.None,
|
||||||
|
YAxisLabelPosition = YAxisLabelPosition.None,
|
||||||
|
YAxisTicks = 100,
|
||||||
|
ShowLabels = false,
|
||||||
|
ShowLegendLabels = false
|
||||||
|
|
||||||
|
};
|
||||||
|
public List<ChartSeries> Series = new List<ChartSeries>()
|
||||||
|
{
|
||||||
|
new ChartSeries() { Name = "类型错误数量分布", Data = new double[] { 35, 41, 35, 51, 49, 62, 69, 91, 148 } },
|
||||||
|
new ChartSeries() { Name = "类型错误成绩分布", Data = new double[] { 55, 21, 45, 11, 45, 23, 11, 56, 13 } },
|
||||||
|
};
|
||||||
|
public string[] XAxisLabels = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep" };
|
||||||
|
|
||||||
|
Random random = new Random();
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
options.InterpolationOption = InterpolationOption.NaturalSpline;
|
||||||
|
options.YAxisFormat = "c2";
|
||||||
|
options.ShowLegend = false;
|
||||||
|
options.YAxisLines = false;
|
||||||
|
options.XAxisLines = false;
|
||||||
|
options.XAxisLabelPosition = XAxisLabelPosition.None;
|
||||||
|
options.YAxisLabelPosition = YAxisLabelPosition.None;
|
||||||
|
options.ShowLabels = false;
|
||||||
|
options.ShowLegendLabels = false;
|
||||||
|
options.LineStrokeWidth = 1;
|
||||||
|
_axisChartOptions.MatchBoundsToSize = true;
|
||||||
|
|
||||||
|
Series[0].LineDisplayType = LineDisplayType.Area;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private IReadOnlyCollection<string> _selected;
|
||||||
|
|
||||||
|
private void HandleSelectedValuesChanged(IReadOnlyCollection<string> selected)
|
||||||
|
{
|
||||||
|
Series.ForEach(x => x.Visible = false);
|
||||||
|
|
||||||
|
foreach(var item in selected)
|
||||||
|
{
|
||||||
|
var sv = Series.FirstOrDefault(predicate: x => x.Name == item);
|
||||||
|
if(sv != null)
|
||||||
|
{
|
||||||
|
sv.Visible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,6 @@
|
|||||||
|
<MudPaper Class="rounded-xl w-100 px-10 ma-3 pt-5">
|
||||||
|
|
||||||
|
<MudText> Name </MudText>
|
||||||
|
<MudText> 错误数量 </MudText>
|
||||||
|
<MudText> 分数 </MudText>
|
||||||
|
</MudPaper>
|
@@ -0,0 +1,6 @@
|
|||||||
|
<MudPaper Class="rounded-xl w-100 px-10 ma-3 pt-5">
|
||||||
|
|
||||||
|
<MudText> Name </MudText>
|
||||||
|
<MudText> 平均数 </MudText>
|
||||||
|
<MudText> 中为数 </MudText>
|
||||||
|
</MudPaper>
|
67
TechHelper.Client/Pages/Common/PublishExamDialog.razor
Normal file
67
TechHelper.Client/Pages/Common/PublishExamDialog.razor
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
@using Entities.DTO
|
||||||
|
@inject ISnackbar Snackbar
|
||||||
|
@using Entities.Contracts
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<MudDialog Class="rounded-xl pa-2" Style="background-color: #dedede">
|
||||||
|
<TitleContent>
|
||||||
|
<MudText Typo="Typo.h6">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.EditAttributes" Class="mr-3 mb-n1" />
|
||||||
|
<b> 发布! </b>
|
||||||
|
</MudText>
|
||||||
|
</TitleContent>
|
||||||
|
<DialogContent>
|
||||||
|
<MudPaper Elevation="0" Class="rounded-xl pa-1 " Style="background-color: transparent">
|
||||||
|
<MudPaper Elevation="0" Class="rounded-xl pa-2 ma-2">
|
||||||
|
<MudTextField @bind-Value="Exam.Name" Label="Title" Variant="Variant.Text" Margin="Margin.Dense" AutoFocus="true" />
|
||||||
|
<MudTextField @bind-Value="Exam.TotalQuestions" Label="TotalQuestions" Variant="Variant.Text" Adornment="Adornment.End" AdornmentText="." Margin="Margin.Dense" AutoFocus="true" />
|
||||||
|
<MudTextField @bind-Value="Exam.Score" Label="Score" Variant="Variant.Text" Adornment="Adornment.End" AdornmentText="." Margin="Margin.Dense" AutoFocus="true" />
|
||||||
|
</MudPaper>
|
||||||
|
<MudPaper Elevation="0" Class="rounded-xl pa-2 ma-2">
|
||||||
|
|
||||||
|
<MudChipSet T="SubjectAreaEnum" @bind-SelectedValue="@Exam.SubjectArea" CheckMark=true SelectionMode="SelectionMode.SingleSelection" Size="Size.Small">
|
||||||
|
<MudChip Text="@SubjectAreaEnum.Literature.ToString()" Color="Color.Primary" Value="@SubjectAreaEnum.Literature"> @SubjectAreaEnum.Literature</MudChip>
|
||||||
|
<MudChip Text="@SubjectAreaEnum.Mathematics.ToString()" Color="Color.Secondary" Value="@SubjectAreaEnum.Mathematics"> @SubjectAreaEnum.Mathematics</MudChip>
|
||||||
|
<MudChip Text="@SubjectAreaEnum.English.ToString()" Color="Color.Info" Value="@SubjectAreaEnum.English"> @SubjectAreaEnum.English</MudChip>
|
||||||
|
<MudChip Text="@SubjectAreaEnum.ComputerScience.ToString()" Color="Color.Success" Value="@SubjectAreaEnum.ComputerScience"> @SubjectAreaEnum.ComputerScience</MudChip>
|
||||||
|
</MudChipSet>
|
||||||
|
|
||||||
|
|
||||||
|
</MudPaper>
|
||||||
|
<MudPaper Elevation="0" Class="rounded-xl pa-2 ma-2">
|
||||||
|
|
||||||
|
<MudChipSet T="ExamType" @bind-SelectedValue="@Exam.ExamType" CheckMark=true SelectionMode="SelectionMode.SingleSelection" Size="Size.Small">
|
||||||
|
<MudChip Text="@ExamType.DailyTest.ToString()" Color="Color.Primary" Value="@ExamType.DailyTest"> @ExamType.DailyTest</MudChip>
|
||||||
|
<MudChip Text="@ExamType.WeeklyExam.ToString()" Color="Color.Secondary" Value="@ExamType.WeeklyExam"> @ExamType.WeeklyExam</MudChip>
|
||||||
|
<MudChip Text="@ExamType.MonthlyExam.ToString()" Color="Color.Info" Value="@ExamType.MonthlyExam"> @ExamType.MonthlyExam</MudChip>
|
||||||
|
<MudChip Text="@ExamType.MidtermExam.ToString()" Color="Color.Success" Value="@ExamType.MidtermExam"> @ExamType.MidtermExam</MudChip>
|
||||||
|
<MudChip Text="@ExamType.FinalExam.ToString()" Color="Color.Warning" Value="@ExamType.FinalExam"> @ExamType.FinalExam</MudChip>
|
||||||
|
<MudChip Text="@ExamType.AITest.ToString()" Color="Color.Error" Value="@ExamType.AITest"> @ExamType.AITest</MudChip>
|
||||||
|
</MudChipSet>
|
||||||
|
</MudPaper>
|
||||||
|
</MudPaper>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<MudButton OnClick="Cancel">Cancel</MudButton>
|
||||||
|
<MudButton Color="Color.Error" OnClick="Confirm">确认</MudButton>
|
||||||
|
</DialogActions>
|
||||||
|
</MudDialog>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[CascadingParameter]
|
||||||
|
private IMudDialogInstance MudDialog { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public AssignmentDto Exam { get; set; } = new AssignmentDto();
|
||||||
|
|
||||||
|
|
||||||
|
public SubjectAreaEnum SubjectArea { get; set; }
|
||||||
|
private void Cancel() => MudDialog.Cancel();
|
||||||
|
|
||||||
|
private void Confirm()
|
||||||
|
{
|
||||||
|
Snackbar.Add("属性已更新", Severity.Success);
|
||||||
|
MudDialog.Close(DialogResult.Ok(Exam));
|
||||||
|
}
|
||||||
|
}
|
34
TechHelper.Client/Pages/Common/QuestionCardDialog.razor
Normal file
34
TechHelper.Client/Pages/Common/QuestionCardDialog.razor
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
@using Entities.DTO
|
||||||
|
@inject ISnackbar Snackbar
|
||||||
|
|
||||||
|
<MudDialog Class="rounded-xl" Style="background-color: #dedede" >
|
||||||
|
<TitleContent>
|
||||||
|
<MudText Typo="Typo.h6">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.EditAttributes" Class="mr-3 mb-n1" />
|
||||||
|
<b> 编辑属性 </b>
|
||||||
|
</MudText>
|
||||||
|
</TitleContent>
|
||||||
|
<DialogContent>
|
||||||
|
<TechHelper.Client.Pages.Exam.AssignmentQuestionEdit AssignmentQuestion="Questions"/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<MudButton OnClick="Cancel">Cancel</MudButton>
|
||||||
|
<MudButton Color="Color.Error" OnClick="Confirm">确认</MudButton>
|
||||||
|
</DialogActions>
|
||||||
|
</MudDialog>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[CascadingParameter]
|
||||||
|
private IMudDialogInstance MudDialog { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public AssignmentQuestionDto Questions { get; set; } = new AssignmentQuestionDto();
|
||||||
|
|
||||||
|
private void Cancel() => MudDialog.Cancel();
|
||||||
|
|
||||||
|
private void Confirm()
|
||||||
|
{
|
||||||
|
Snackbar.Add("属性已更新", Severity.Success);
|
||||||
|
MudDialog.Close(DialogResult.Ok(Questions));
|
||||||
|
}
|
||||||
|
}
|
22
TechHelper.Client/Pages/Common/SimpleCard.razor
Normal file
22
TechHelper.Client/Pages/Common/SimpleCard.razor
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<MudPaper Elevation=5 Class="w-100 rounded-xl" Height="@Height" Style="@Style">
|
||||||
|
<MudPaper Elevation=0 Class="w-100 pa-2 align-content-center" Height="20%" Style="background-color:transparent"> @TitleContent </MudPaper>
|
||||||
|
<MudPaper Elevation=0 Class="w-100 pa-2" Style="background-color:transparent" Height="60%"> @BodyContent </MudPaper>
|
||||||
|
<MudPaper Elevation=0 Class="w-100 pa-2 align-content-center" Style="background-color:transparent" Height="20%"> @FooterContent </MudPaper>
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public string Style { get; set; }
|
||||||
|
[Parameter]
|
||||||
|
public string Height { get; set; } = "200px";
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public RenderFragment TitleContent { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public RenderFragment BodyContent { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public RenderFragment FooterContent { get; set; }
|
||||||
|
}
|
86
TechHelper.Client/Pages/Common/TextEditorDialog.razor
Normal file
86
TechHelper.Client/Pages/Common/TextEditorDialog.razor
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
@using Entities.DTO
|
||||||
|
@inject ISnackbar Snackbar
|
||||||
|
|
||||||
|
<MudDialog Class="rounded-xl" Style="background-color: #dedede" >
|
||||||
|
<TitleContent>
|
||||||
|
<MudText Typo="Typo.h6">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.EditAttributes" Class="mr-3 mb-n1" />
|
||||||
|
<b> 编辑属性 </b>
|
||||||
|
</MudText>
|
||||||
|
</TitleContent>
|
||||||
|
<DialogContent>
|
||||||
|
<BlazoredTextEditor @ref="@TextEditor">
|
||||||
|
<ToolbarContent>
|
||||||
|
<select class="ql-header">
|
||||||
|
<option selected=""></option>
|
||||||
|
<option value="1"></option>
|
||||||
|
<option value="2"></option>
|
||||||
|
<option value="3"></option>
|
||||||
|
<option value="4"></option>
|
||||||
|
<option value="5"></option>
|
||||||
|
</select>
|
||||||
|
<span class="ql-formats">
|
||||||
|
<button class="ql-bold"></button>
|
||||||
|
<button class="ql-italic"></button>
|
||||||
|
<button class="ql-underline"></button>
|
||||||
|
<button class="ql-strike"></button>
|
||||||
|
</span>
|
||||||
|
<span class="ql-formats">
|
||||||
|
<select class="ql-color"></select>
|
||||||
|
<select class="ql-background"></select>
|
||||||
|
</span>
|
||||||
|
<span class="ql-formats">
|
||||||
|
<button class="ql-list" value="ordered"></button>
|
||||||
|
<button class="ql-list" value="bullet"></button>
|
||||||
|
</span>
|
||||||
|
<span class="ql-formats">
|
||||||
|
<button class="ql-link"></button>
|
||||||
|
</span>
|
||||||
|
</ToolbarContent>
|
||||||
|
<EditorContent>
|
||||||
|
</EditorContent>
|
||||||
|
</BlazoredTextEditor>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<MudButton OnClick="Cancel">Cancel</MudButton>
|
||||||
|
<MudButton Color="Color.Error" OnClick="Confirm">确认</MudButton>
|
||||||
|
</DialogActions>
|
||||||
|
</MudDialog>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[CascadingParameter]
|
||||||
|
private IMudDialogInstance MudDialog { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public BlazoredTextEditor TextEditor { get; set; } = new BlazoredTextEditor();
|
||||||
|
[Parameter]
|
||||||
|
public string EditorText { get; set; } = "{}";
|
||||||
|
|
||||||
|
private void HandleClick()
|
||||||
|
{
|
||||||
|
TextEditor.InsertText(EditorText);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Cancel() => MudDialog.Cancel();
|
||||||
|
|
||||||
|
|
||||||
|
protected async override Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await DelayInsert();
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private async Task DelayInsert()
|
||||||
|
{
|
||||||
|
await Task.Delay(100);
|
||||||
|
HandleClick();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Confirm()
|
||||||
|
{
|
||||||
|
Snackbar.Add("属性已更新", Severity.Success);
|
||||||
|
EditorText = await TextEditor.GetText();
|
||||||
|
MudDialog.Close(DialogResult.Ok(TextEditor));
|
||||||
|
}
|
||||||
|
}
|
10
TechHelper.Client/Pages/Exam/AssignmentDetailView.razor
Normal file
10
TechHelper.Client/Pages/Exam/AssignmentDetailView.razor
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<MudText> QuestionNum </MudText>
|
||||||
|
<MudText> QuestionTypeDis </MudText>
|
||||||
|
<MudText> ErrorQuestionTypeDis </MudText>
|
||||||
|
<MudText> ErrorLessonDis </MudText>
|
||||||
|
<MudText> ErrorIndexDis </MudText>
|
||||||
|
<MudText> ErrorNum </MudText>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
|
||||||
|
}
|
66
TechHelper.Client/Pages/Exam/AssignmentQuestionEdit.razor
Normal file
66
TechHelper.Client/Pages/Exam/AssignmentQuestionEdit.razor
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
@using Entities.DTO
|
||||||
|
@using Entities.Contracts
|
||||||
|
@using TechHelper.Client.Exam
|
||||||
|
@using TechHelper.Client.Pages.Exam.QuestionCard
|
||||||
|
|
||||||
|
|
||||||
|
<MudPaper Elevation="0" Class="rounded-xl" Style="background-color: transparent">
|
||||||
|
@* <MudText>@AssignmentQuestion.Id</MudText> *@
|
||||||
|
<MudPaper Class="ma-4 pa-5 rounded-xl">
|
||||||
|
<MudText Class="mt-3" Typo="Typo.button"><b>包裹器属性</b></MudText>
|
||||||
|
<MudTextField @bind-Value="AssignmentQuestion.Title" Label="Title" Variant="Variant.Text" Margin="Margin.Dense" AutoFocus="true" />
|
||||||
|
<MudTextField @bind-Value="AssignmentQuestion.Index" Label="Index" Variant="Variant.Text" Adornment="Adornment.End" AdornmentText="." Margin="Margin.Dense" AutoFocus="true" />
|
||||||
|
<MudTextField @bind-Value="AssignmentQuestion.Score" Label="Score" Variant="Variant.Text" Adornment="Adornment.End" AdornmentText="." Margin="Margin.Dense" AutoFocus="true" />
|
||||||
|
<MudChipSet T="AssignmentStructType" SelectedValue="AssignmentQuestion.StructType" CheckMark SelectionMode="SelectionMode.SingleSelection" SelectedValueChanged="HandleSelectedValueChanged">
|
||||||
|
<MudChip Text="pink" Color="Color.Secondary" Value="@AssignmentStructType.Struct">@AssignmentStructType.Struct</MudChip>
|
||||||
|
<MudChip Text="purple" Color="Color.Primary" Value="@AssignmentStructType.Group">@AssignmentStructType.Group</MudChip>
|
||||||
|
<MudChip Text="blue" Color="Color.Info" Value="@AssignmentStructType.Question">@AssignmentStructType.Question</MudChip>
|
||||||
|
<MudChip Text="green" Color="Color.Warning" Value="@AssignmentStructType.SubQuestion">@AssignmentStructType.SubQuestion</MudChip>
|
||||||
|
<MudChip Text="orange" Color="Color.Error" Value="@AssignmentStructType.Option">@AssignmentStructType.Option</MudChip>
|
||||||
|
</MudChipSet>
|
||||||
|
</MudPaper>
|
||||||
|
@if (AssignmentQuestion.Question != null)
|
||||||
|
{
|
||||||
|
<QuestionEdit Question="AssignmentQuestion.Question" />
|
||||||
|
}
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public AssignmentQuestionDto AssignmentQuestion { get; set; } = new AssignmentQuestionDto();
|
||||||
|
public QuestionDto TempQuesdto;
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
base.OnInitialized();
|
||||||
|
if (AssignmentQuestion.Question != null)
|
||||||
|
{
|
||||||
|
TempQuesdto = AssignmentQuestion.Question;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleSelectedValueChanged(AssignmentStructType type)
|
||||||
|
{
|
||||||
|
AssignmentQuestion.StructType = type;
|
||||||
|
if (type != AssignmentStructType.Question && AssignmentQuestion.Question != null)
|
||||||
|
{
|
||||||
|
AssignmentQuestion.Title = AssignmentQuestion.Question.Title;
|
||||||
|
AssignmentQuestion.Question = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == AssignmentStructType.Question && AssignmentQuestion.Question == null)
|
||||||
|
{
|
||||||
|
if (TempQuesdto != null)
|
||||||
|
{
|
||||||
|
AssignmentQuestion.Question = TempQuesdto;
|
||||||
|
if (AssignmentQuestion.Title == AssignmentQuestion.Question.Title)
|
||||||
|
{
|
||||||
|
AssignmentQuestion.Title = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
AssignmentQuestion.Question = new QuestionDto { };
|
||||||
|
}
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
@@ -1,9 +1,10 @@
|
|||||||
@using Entities.DTO
|
@using Entities.DTO
|
||||||
@using TechHelper.Client.Exam
|
@using TechHelper.Client.Exam
|
||||||
|
@using TechHelper.Client.Services
|
||||||
@page "/exam/check/{ExamID}"
|
@page "/exam/check/{ExamID}"
|
||||||
|
|
||||||
|
|
||||||
<MudText Typo="Typo.h4" Class="mb-4">试卷批改预览: @ExamDto.AssignmentTitle</MudText>
|
<MudText Typo="Typo.h4" Class="mb-4">试卷批改预览: @Assignment.Title</MudText>
|
||||||
<MudDivider Class="my-4" />
|
<MudDivider Class="my-4" />
|
||||||
|
|
||||||
@if (_isLoading)
|
@if (_isLoading)
|
||||||
@@ -13,14 +14,14 @@
|
|||||||
}
|
}
|
||||||
else if (_questionsForTable.Any() && _students.Any())
|
else if (_questionsForTable.Any() && _students.Any())
|
||||||
{
|
{
|
||||||
<MudTable @ref="_table" T="QuestionRowData" Items="@_questionsForTable" Hover="true" Breakpoint="Breakpoint.Sm" Class="mud-elevation-2" Dense="true">
|
<MudTable @ref="_table" T="QuestionRowData" Items="@_questionsForTable" Hover="true" Breakpoint="Breakpoint.Sm" Striped="true" Class="mud-elevation-2" Dense="true">
|
||||||
<HeaderContent>
|
<HeaderContent>
|
||||||
<MudTh Style="width:100px;">序号</MudTh>
|
<MudTh Style="width:100px;">序号</MudTh>
|
||||||
<MudTh Style="width:80px; text-align:center;">分值</MudTh>
|
<MudTh Style="width:80px; text-align:center;">分值</MudTh>
|
||||||
@foreach (var student in _students)
|
@foreach (var student in _students)
|
||||||
{
|
{
|
||||||
<MudTh Style="width:120px; text-align:center;">
|
<MudTh Style="width:120px; text-align:center;">
|
||||||
@student.Name
|
@student.DisplayName
|
||||||
<MudTooltip Text="点击以切换此学生所有题目的对错">
|
<MudTooltip Text="点击以切换此学生所有题目的对错">
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Info" Size="Size.Small" Class="ml-1"
|
<MudIconButton Icon="@Icons.Material.Filled.Info" Size="Size.Small" Class="ml-1"
|
||||||
@onclick="() => ToggleStudentAllAnswers(student.Id)" />
|
@onclick="() => ToggleStudentAllAnswers(student.Id)" />
|
||||||
@@ -33,7 +34,7 @@ else if (_questionsForTable.Any() && _students.Any())
|
|||||||
<MudTd DataLabel="分值" Style="text-align:center;">@context.QuestionItem.Score</MudTd>
|
<MudTd DataLabel="分值" Style="text-align:center;">@context.QuestionItem.Score</MudTd>
|
||||||
@foreach (var student in _students)
|
@foreach (var student in _students)
|
||||||
{
|
{
|
||||||
<MudTd DataLabel="@student.Name" Style="text-align:center;">
|
<MudTd DataLabel="@student.DisplayName" Style="text-align:center;">
|
||||||
@if (context.StudentAnswers.ContainsKey(student.Id))
|
@if (context.StudentAnswers.ContainsKey(student.Id))
|
||||||
{
|
{
|
||||||
<MudCheckBox @bind-Value="context.StudentAnswers[student.Id]" Size="Size.Small" Color="Color.Primary"></MudCheckBox>
|
<MudCheckBox @bind-Value="context.StudentAnswers[student.Id]" Size="Size.Small" Color="Color.Primary"></MudCheckBox>
|
||||||
@@ -55,7 +56,7 @@ else if (_questionsForTable.Any() && _students.Any())
|
|||||||
@foreach (var student in _students)
|
@foreach (var student in _students)
|
||||||
{
|
{
|
||||||
<MudText Typo="Typo.subtitle1">
|
<MudText Typo="Typo.subtitle1">
|
||||||
@student.Name: <MudText Typo="Typo.h5" Color="Color.Primary" Class="d-inline-block ml-2">@GetStudentTotalScore(student.Id)</MudText>
|
@student.DisplayName: <MudText Typo="Typo.h5" Color="Color.Primary" Class="d-inline-block ml-2">@GetStudentTotalScore(student.Id)</MudText>
|
||||||
</MudText>
|
</MudText>
|
||||||
}
|
}
|
||||||
<MudButton Variant="Variant.Filled" Color="Color.Success" Class="mt-4" @onclick="SubmitGrading">
|
<MudButton Variant="Variant.Filled" Color="Color.Success" Class="mt-4" @onclick="SubmitGrading">
|
||||||
@@ -73,36 +74,58 @@ else
|
|||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string ExamId { get; set; } // 从路由获取的试卷ID
|
public string ExamId { get; set; }
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
public IExamService ExamService { get; set; } // 注入试卷服务
|
public IExamService ExamService { get; set; }
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
private ISnackbar Snackbar { get; set; } // 注入 Snackbar 用于消息提示
|
private ISnackbar Snackbar { get; set; }
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
private NavigationManager Navigation { get; set; } // 注入导航管理器
|
private NavigationManager Navigation { get; set; }
|
||||||
|
|
||||||
private MudTable<QuestionRowData> _table = new(); // MudTable 实例引用
|
private MudTable<QuestionRowData> _table = new();
|
||||||
private ExamDto ExamDto { get; set; } = new ExamDto(); // 原始试卷数据
|
private AssignmentDto Assignment { get; set; } = new AssignmentDto();
|
||||||
private ExamStruct _examStruct = new ExamStruct(); // 处理后的试卷结构,包含带序号的题目
|
private AssignmentCheckData _examStruct = new AssignmentCheckData();
|
||||||
|
|
||||||
private List<Student> _students = new List<Student>(); // 临时生成的学生列表
|
private List<StudentDto> _students = new List<StudentDto>();
|
||||||
private List<QuestionRowData> _questionsForTable = new List<QuestionRowData>(); // 用于 MudTable 的数据源
|
private List<QuestionRowData> _questionsForTable = new List<QuestionRowData>();
|
||||||
|
|
||||||
private bool _isLoading = true; // 加载状态
|
private bool _isLoading = true;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
public IClassServices ClassServices { get; set; }
|
||||||
|
|
||||||
// 在组件初始化时加载数据
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
await LoadExamData();
|
await LoadExamData();
|
||||||
GenerateTemporaryStudentsAndAnswers(); // 生成学生和初始作答数据
|
|
||||||
|
var result = await ClassServices.GetClassStudents();
|
||||||
|
if (!result.Status) Snackbar.Add($"获取学生失败, {result.Message}", Severity.Error);
|
||||||
|
_students = result.Result as List<StudentDto> ?? new List<StudentDto>();
|
||||||
|
BuildTable();
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载试卷数据的方法
|
private void BuildTable()
|
||||||
|
{
|
||||||
|
_questionsForTable = _examStruct.Questions.Select(q =>
|
||||||
|
{
|
||||||
|
var rowData = new QuestionRowData
|
||||||
|
{
|
||||||
|
QuestionItem = q,
|
||||||
|
StudentAnswers = new Dictionary<Guid, bool>()
|
||||||
|
};
|
||||||
|
foreach (var student in _students)
|
||||||
|
{
|
||||||
|
rowData.StudentAnswers[student.Id] = false;
|
||||||
|
}
|
||||||
|
return rowData;
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task LoadExamData()
|
private async Task LoadExamData()
|
||||||
{
|
{
|
||||||
if (Guid.TryParse(ExamId, out Guid parsedExamId))
|
if (Guid.TryParse(ExamId, out Guid parsedExamId))
|
||||||
@@ -112,13 +135,13 @@ else
|
|||||||
var result = await ExamService.GetExam(parsedExamId);
|
var result = await ExamService.GetExam(parsedExamId);
|
||||||
if (result.Status)
|
if (result.Status)
|
||||||
{
|
{
|
||||||
ExamDto = result.Result as ExamDto ?? new ExamDto();
|
Assignment = result.Result as AssignmentDto ?? new AssignmentDto();
|
||||||
_examStruct = ExamDto.GetStruct(); // 将 ExamDto 转换为 ExamStruct
|
_examStruct = Assignment.GetStruct();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Snackbar?.Add($"获取试卷失败: {result.Message}", Severity.Error);
|
Snackbar?.Add($"获取试卷失败: {result.Message}", Severity.Error);
|
||||||
Navigation.NavigateTo("/exam/manager"); // 导航回管理页
|
Navigation.NavigateTo("/exam/manager");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -136,44 +159,8 @@ else
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成临时学生和作答数据
|
|
||||||
private void GenerateTemporaryStudentsAndAnswers()
|
|
||||||
{
|
|
||||||
_students = new List<Student>();
|
|
||||||
// 生成 40 个学生
|
|
||||||
for (int i = 1; i <= 40; i++)
|
|
||||||
{
|
|
||||||
_students.Add(new Student { Name = $"学生{i}" });
|
|
||||||
}
|
|
||||||
|
|
||||||
_questionsForTable = _examStruct.Questions.Select(qItem =>
|
|
||||||
{
|
|
||||||
var rowData = new QuestionRowData
|
|
||||||
{
|
|
||||||
QuestionItem = qItem,
|
|
||||||
StudentAnswers = new Dictionary<Guid, bool>()
|
|
||||||
};
|
|
||||||
|
|
||||||
// 为每个学生随机生成初始的对错状态
|
|
||||||
var random = new Random();
|
|
||||||
foreach (var student in _students)
|
|
||||||
{
|
|
||||||
// 模拟随机对错,50%的概率
|
|
||||||
rowData.StudentAnswers[student.Id] = random.Next(0, 2) == 1;
|
|
||||||
}
|
|
||||||
return rowData;
|
|
||||||
}).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 当某个学生的某个题目的作答状态改变时触发
|
|
||||||
private void OnAnswerChanged(string questionSequence, Guid studentId, bool isCorrect)
|
|
||||||
{
|
|
||||||
// 可以在这里添加额外的逻辑,例如记录更改
|
|
||||||
Console.WriteLine($"题目 {questionSequence}, 学生 {studentId} 的答案变为: {isCorrect}");
|
|
||||||
// 由于是 @bind-Checked,数据模型已经自动更新,这里只是日志
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算某个学生的总分
|
|
||||||
private float GetStudentTotalScore(Guid studentId)
|
private float GetStudentTotalScore(Guid studentId)
|
||||||
{
|
{
|
||||||
float totalScore = 0;
|
float totalScore = 0;
|
||||||
@@ -187,7 +174,6 @@ else
|
|||||||
return totalScore;
|
return totalScore;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换某个学生所有题目的对错状态 (用于快速批改)
|
|
||||||
private void ToggleStudentAllAnswers(Guid studentId)
|
private void ToggleStudentAllAnswers(Guid studentId)
|
||||||
{
|
{
|
||||||
bool allCorrect = _questionsForTable.All(row => row.StudentAnswers.ContainsKey(studentId) && row.StudentAnswers[studentId]);
|
bool allCorrect = _questionsForTable.All(row => row.StudentAnswers.ContainsKey(studentId) && row.StudentAnswers[studentId]);
|
||||||
@@ -196,28 +182,58 @@ else
|
|||||||
{
|
{
|
||||||
if (row.StudentAnswers.ContainsKey(studentId))
|
if (row.StudentAnswers.ContainsKey(studentId))
|
||||||
{
|
{
|
||||||
row.StudentAnswers[studentId] = !allCorrect; // 全部取反
|
row.StudentAnswers[studentId] = !allCorrect;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
StateHasChanged(); // 手动通知 Blazor 刷新 UI
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提交批改结果(模拟)
|
|
||||||
private void SubmitGrading()
|
private void SubmitGrading()
|
||||||
{
|
{
|
||||||
Console.WriteLine("--- 提交批改结果 ---");
|
|
||||||
|
List<SubmissionDto> submissionDto = new List<SubmissionDto>();
|
||||||
|
|
||||||
|
|
||||||
foreach (var student in _students)
|
foreach (var student in _students)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"学生: {student.Name}, 总分: {GetStudentTotalScore(student.Id)}");
|
var newSubmission = new SubmissionDto();
|
||||||
|
newSubmission.StudentId = student.Id;
|
||||||
|
newSubmission.AssignmentId = Assignment.Id;
|
||||||
|
newSubmission.SubmissionTime = DateTime.Now;
|
||||||
|
newSubmission.Status = Entities.Contracts.SubmissionStatus.Graded;
|
||||||
|
|
||||||
|
|
||||||
foreach (var row in _questionsForTable)
|
foreach (var row in _questionsForTable)
|
||||||
{
|
{
|
||||||
|
if (row.QuestionItem.AssignmentQuestionDto.StructType == Entities.Contracts.AssignmentStructType.Struct) continue;
|
||||||
if (row.StudentAnswers.TryGetValue(student.Id, out bool isCorrect))
|
if (row.StudentAnswers.TryGetValue(student.Id, out bool isCorrect))
|
||||||
{
|
{
|
||||||
Console.WriteLine($" - 题目 {row.QuestionItem.Sequence}: {(isCorrect ? "正确" : "错误")}");
|
newSubmission.SubmissionDetails.Add(new SubmissionDetailDto
|
||||||
|
{
|
||||||
|
IsCorrect = isCorrect,
|
||||||
|
StudentId = student.Id,
|
||||||
|
AssignmentQuestionId = row.QuestionItem.AssignmentQuestionDto.Id,
|
||||||
|
PointsAwarded = isCorrect ? row.QuestionItem.AssignmentQuestionDto.Score : 0
|
||||||
|
});
|
||||||
|
|
||||||
|
newSubmission.OverallGrade += isCorrect ? row.QuestionItem.AssignmentQuestionDto.Score : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
submissionDto.Add(newSubmission);
|
||||||
}
|
}
|
||||||
Snackbar?.Add("批改结果已提交(模拟)", Severity.Success);
|
|
||||||
// 实际应用中,这里会将 _questionsForTable 和 _students 的数据发送到后端API
|
submissionDto.ForEach(async s =>
|
||||||
|
{
|
||||||
|
Snackbar?.Add($"正在提交: {_students.FirstOrDefault(std => std.Id == s.StudentId)?.DisplayName} 的试卷", Severity.Info);
|
||||||
|
var submidResult = await ExamService.SubmissionAssignment(s);
|
||||||
|
if (submidResult.Status)
|
||||||
|
Snackbar?.Add($"批改结果已提交 {_students.FirstOrDefault(st => st.Id == s.StudentId)?.DisplayName}", Severity.Success);
|
||||||
|
else
|
||||||
|
Snackbar?.Add("批改结果提交失败", Severity.Error);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,4 +1,8 @@
|
|||||||
@page "/exam/create"
|
@page "/exam/create"
|
||||||
|
@using AutoMapper
|
||||||
|
@using TechHelper.Client.Pages.Common
|
||||||
|
@using TechHelper.Client.Pages.Exam.ExamView
|
||||||
|
@using TechHelper.Client.Services
|
||||||
@using Blazored.TextEditor
|
@using Blazored.TextEditor
|
||||||
@using Entities.DTO
|
@using Entities.DTO
|
||||||
@using TechHelper.Client.Exam
|
@using TechHelper.Client.Exam
|
||||||
@@ -7,10 +11,19 @@
|
|||||||
@using Microsoft.AspNetCore.Components
|
@using Microsoft.AspNetCore.Components
|
||||||
@using System.Globalization;
|
@using System.Globalization;
|
||||||
@using TechHelper.Client.Pages.Editor
|
@using TechHelper.Client.Pages.Editor
|
||||||
|
@inject IDialogService DialogService
|
||||||
|
|
||||||
<MudPaper Elevation="5" Class="d-flex overflow-hidden flex-grow-1" Style="overflow:hidden; position:relative;height:100%">
|
<MudPaper Elevation="5" Class="d-flex overflow-hidden flex-grow-1 pa-1 rounded-xl " Style="overflow:hidden; position:relative;height:100%">
|
||||||
<MudDrawerContainer Class="mud-height-full flex-grow-1" Style="height:100%">
|
<MudDrawerContainer Class="mud-height-full flex-grow-1" Style="height:100%">
|
||||||
<MudDrawer @bind-Open="@_open" Elevation="0" Variant="@DrawerVariant.Persistent" Color="Color.Primary" Anchor="Anchor.End" OverlayAutoClose="true">
|
<MudDrawer @bind-Open="@_open" Elevation="0" Variant="@DrawerVariant.Persistent" Color="Color.Primary" Anchor="Anchor.End" OverlayAutoClose="true">
|
||||||
|
|
||||||
|
@if (_edit)
|
||||||
|
{
|
||||||
|
<AssignmentQuestionEdit AssignmentQuestion="selectedAssignmentQuestion" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
|
||||||
<MudDrawerHeader>
|
<MudDrawerHeader>
|
||||||
<MudText Typo="Typo.h6"> 配置 </MudText>
|
<MudText Typo="Typo.h6"> 配置 </MudText>
|
||||||
</MudDrawerHeader>
|
</MudDrawerHeader>
|
||||||
@@ -19,11 +32,165 @@
|
|||||||
<ParseRoleConfig />
|
<ParseRoleConfig />
|
||||||
<MudButton Color="Color.Success"> ParseExam </MudButton>
|
<MudButton Color="Color.Success"> ParseExam </MudButton>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudDrawer>
|
}
|
||||||
<MudStack Row="true" Class="flex-grow-1" Style="height:100%">
|
|
||||||
<ExamView Class="overflow-auto" ParsedExam="ExamContent"></ExamView>
|
|
||||||
|
|
||||||
<MudPaper Class="ma-2">
|
|
||||||
|
</MudDrawer>
|
||||||
|
<MudStack Class="flex-grow-1 h-100">
|
||||||
|
|
||||||
|
<MudPaper Class="rounded-xl ma-1 pa-2" Style="background-color:#fefefefe">
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Secondary" Class="rounded-xl" Size="Size.Small" OnClick="OpenEditor">文本编辑器</MudButton>
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Secondary" Class="rounded-xl" Size="Size.Small" OnClick="ParseExam">载入</MudButton>
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Secondary" Class="rounded-xl" Size="Size.Small" OnClick="OpenPublish">发布</MudButton>
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Secondary" Class="rounded-xl" Size="Size.Small" OnClick="OpenPublish">指派</MudButton>
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
|
|
||||||
|
<ExamView Class="overflow-auto ma-1 pa-2 rounded-xl" ClickedStruct="HandleClickedStruct" ParsedExam="ExamContent"></ExamView>
|
||||||
|
|
||||||
|
<MudPaper MaxWidth="300">
|
||||||
|
|
||||||
|
@if (_parsedExam.Errors.Any())
|
||||||
|
{
|
||||||
|
foreach (var item in _parsedExam.Errors)
|
||||||
|
{
|
||||||
|
<MudText> @item.Message </MudText>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</MudPaper>
|
||||||
|
</MudStack>
|
||||||
|
</MudDrawerContainer>
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
|
||||||
|
[CascadingParameter]
|
||||||
|
private Task<AuthenticationState> authenticationStateTask { get; set; }
|
||||||
|
|
||||||
|
private AssignmentQuestionDto selectedAssignmentQuestion = new AssignmentQuestionDto();
|
||||||
|
private IReadOnlyCollection<string> _selected;
|
||||||
|
private bool _open = false;
|
||||||
|
private bool _edit = false;
|
||||||
|
|
||||||
|
private void ToggleDrawer()
|
||||||
|
{
|
||||||
|
_open = !_open;
|
||||||
|
_edit = false;
|
||||||
|
}
|
||||||
|
private BlazoredTextEditor _textEditor = new BlazoredTextEditor();
|
||||||
|
private AssignmentEx _parsedExam = new AssignmentEx();
|
||||||
|
private AssignmentDto ExamContent = new AssignmentDto();
|
||||||
|
|
||||||
|
private ExamParserConfig _examParserConfig { get; set; } = new ExamParserConfig();
|
||||||
|
private string EditorText = "";
|
||||||
|
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
public IMapper Mapper { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
private async void OpenEditor()
|
||||||
|
{
|
||||||
|
var parameters = new DialogParameters<TextEditorDialog> { { x => x.TextEditor, _textEditor } };
|
||||||
|
parameters.Add("EditorText", EditorText);
|
||||||
|
|
||||||
|
var dialog = await DialogService.ShowAsync<TextEditorDialog>("TextEditor", parameters);
|
||||||
|
var result = await dialog.Result;
|
||||||
|
if (!result.Canceled)
|
||||||
|
{
|
||||||
|
_textEditor = result.Data as BlazoredTextEditor ?? new BlazoredTextEditor();
|
||||||
|
await ParseExam();
|
||||||
|
}
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
private async void OpenPublish()
|
||||||
|
{
|
||||||
|
var parameters = new DialogParameters<PublishExamDialog> { { x => x.Exam, ExamContent } };
|
||||||
|
|
||||||
|
var dialog = await DialogService.ShowAsync<PublishExamDialog>("PublishExam", parameters);
|
||||||
|
var result = await dialog.Result;
|
||||||
|
if (!result.Canceled)
|
||||||
|
{
|
||||||
|
await Publish();
|
||||||
|
}
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void HandleClickedStruct(AssignmentQuestionDto dto)
|
||||||
|
{
|
||||||
|
// _open = true;
|
||||||
|
// _edit = true;
|
||||||
|
// StateHasChanged();
|
||||||
|
|
||||||
|
var parameters = new DialogParameters<QuestionCardDialog> { { x => x.Questions, dto } };
|
||||||
|
|
||||||
|
var dialog = await DialogService.ShowAsync<QuestionCardDialog>("Edit_Question", parameters);
|
||||||
|
var result = await dialog.Result;
|
||||||
|
if (!result.Canceled)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ParseExam()
|
||||||
|
{
|
||||||
|
|
||||||
|
var plainText = await _textEditor.GetText();
|
||||||
|
EditorText = plainText;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(plainText))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var exampar = new ExamParser(_examParserConfig);
|
||||||
|
_parsedExam = exampar.ParseExamPaper(plainText);
|
||||||
|
Snackbar.Add("试卷解析成功。", Severity.Success);
|
||||||
|
Snackbar.Add($"{_parsedExam.Errors}。", Severity.Success);
|
||||||
|
StateHasChanged();
|
||||||
|
ExamContent = Mapper.Map<AssignmentDto>(_parsedExam);
|
||||||
|
ExamContent.SeqIndex();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error parsing exam paper: {ex.Message}");
|
||||||
|
Console.WriteLine($"Stack Trace: {ex.StackTrace}");
|
||||||
|
|
||||||
|
Snackbar.Add($"解析试卷时发生错误:{ex.Message}", Severity.Error);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Snackbar.Add("试卷文本为空,无法解析。", Severity.Warning);
|
||||||
|
}
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
public IExamService examService { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
public async Task Publish()
|
||||||
|
{
|
||||||
|
var apiRespon = await examService.SaveParsedExam(ExamContent);
|
||||||
|
Snackbar.Add(apiRespon.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- #region name -->
|
||||||
|
@* <MudButtonGroup Vertical="true" Color="Color.Primary" Variant="Variant.Filled">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Settings" OnClick="@ToggleDrawer" Color="Color.Secondary" />
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.TransitEnterexit" OnClick="@ParseExam" Color="Color.Secondary" />
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Save" OnClick="@ToggleDrawer" Color="Color.Secondary" />
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Publish" OnClick="@Publish" Color="Color.Secondary" />
|
||||||
|
</MudButtonGroup> *@
|
||||||
|
@* <MudPaper Class="ma-2 h-100">
|
||||||
<MudPaper Elevation="0" Style="height:calc(100% - 80px)">
|
<MudPaper Elevation="0" Style="height:calc(100% - 80px)">
|
||||||
<BlazoredTextEditor @ref="@_textEditor">
|
<BlazoredTextEditor @ref="@_textEditor">
|
||||||
<ToolbarContent>
|
<ToolbarContent>
|
||||||
@@ -57,77 +224,6 @@
|
|||||||
</EditorContent>
|
</EditorContent>
|
||||||
</BlazoredTextEditor>
|
</BlazoredTextEditor>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
</MudPaper>
|
</MudPaper> *@
|
||||||
|
|
||||||
<MudButtonGroup Vertical="true" Color="Color.Primary" Variant="Variant.Filled">
|
<!-- #endregion -->
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Settings" OnClick="@ToggleDrawer" Color="Color.Secondary" />
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.TransitEnterexit" OnClick="@ParseExam" Color="Color.Secondary" />
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Save" OnClick="@ToggleDrawer" Color="Color.Secondary" />
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Publish" OnClick="@Publish" Color="Color.Secondary" />
|
|
||||||
</MudButtonGroup>
|
|
||||||
</MudStack>
|
|
||||||
</MudDrawerContainer>
|
|
||||||
</MudPaper>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
|
|
||||||
[CascadingParameter]
|
|
||||||
private Task<AuthenticationState> authenticationStateTask { get; set; }
|
|
||||||
|
|
||||||
private bool _open = false;
|
|
||||||
|
|
||||||
private void ToggleDrawer()
|
|
||||||
{
|
|
||||||
_open = !_open;
|
|
||||||
}
|
|
||||||
private BlazoredTextEditor _textEditor = new BlazoredTextEditor();
|
|
||||||
private ExamPaper _parsedExam = new ExamPaper();
|
|
||||||
private ExamDto ExamContent = new ExamDto();
|
|
||||||
|
|
||||||
private ExamParserConfig _examParserConfig { get; set; } = new ExamParserConfig();
|
|
||||||
|
|
||||||
private async Task ParseExam()
|
|
||||||
{
|
|
||||||
|
|
||||||
var plainText = await _textEditor.GetText();
|
|
||||||
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(plainText))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var exampar = new ExamParser(_examParserConfig);
|
|
||||||
_parsedExam = exampar.ParseExamPaper(plainText);
|
|
||||||
Snackbar.Add("试卷解析成功。", Severity.Success);
|
|
||||||
Snackbar.Add($"{_parsedExam.Errors}。", Severity.Success);
|
|
||||||
ExamContent = _parsedExam.ConvertToExamDTO();
|
|
||||||
ExamContent.SeqIndex();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Error parsing exam paper: {ex.Message}");
|
|
||||||
Console.WriteLine($"Stack Trace: {ex.StackTrace}");
|
|
||||||
|
|
||||||
Snackbar.Add($"解析试卷时发生错误:{ex.Message}", Severity.Error);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Snackbar.Add("试卷文本为空,无法解析。", Severity.Warning);
|
|
||||||
}
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Inject]
|
|
||||||
public IExamService examService { get; set; }
|
|
||||||
|
|
||||||
|
|
||||||
public async Task Publish()
|
|
||||||
{
|
|
||||||
ExamContent.CreaterEmail = authenticationStateTask.Result.User.Identity.Name;
|
|
||||||
var apiRespon = await examService.SaveParsedExam(ExamContent);
|
|
||||||
Snackbar.Add(apiRespon.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,5 +1,8 @@
|
|||||||
@page "/exam/edit/{ExamId}"
|
@page "/exam/edit/{ExamId}"
|
||||||
@using Entities.DTO
|
@using Entities.DTO
|
||||||
|
@using TechHelper.Client.Pages.Exam.ExamView
|
||||||
|
@using TechHelper.Client.Services
|
||||||
|
@using Entities.DTO
|
||||||
@using TechHelper.Client.Exam
|
@using TechHelper.Client.Exam
|
||||||
|
|
||||||
<ExamView ParsedExam="@ExamDto"/>
|
<ExamView ParsedExam="@ExamDto"/>
|
||||||
@@ -17,7 +20,7 @@
|
|||||||
[Inject]
|
[Inject]
|
||||||
private ISnackbar Snackbar { get; set; }
|
private ISnackbar Snackbar { get; set; }
|
||||||
|
|
||||||
private ExamDto ExamDto { get; set; }
|
private AssignmentDto ExamDto { get; set; }
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
@@ -28,7 +31,7 @@
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await ExamService.GetExam(parsedExamId);
|
var result = await ExamService.GetExam(parsedExamId);
|
||||||
if (result.Status) ExamDto = result.Result as ExamDto ?? new ExamDto();
|
if (result.Status) ExamDto = result.Result as AssignmentDto ?? new AssignmentDto();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@@ -1,48 +0,0 @@
|
|||||||
@using Entities.DTO
|
|
||||||
@using TechHelper.Client.Exam
|
|
||||||
|
|
||||||
|
|
||||||
<MudPaper Elevation=@Elevation Class=@Class>
|
|
||||||
|
|
||||||
@foreach (var majorQG in MajorQGList)
|
|
||||||
{
|
|
||||||
<MudStack Row="true">
|
|
||||||
<MudText Typo="Typo.h6">@majorQG.Title</MudText>
|
|
||||||
@if (majorQG.Score > 0)
|
|
||||||
{
|
|
||||||
<MudText Typo="Typo.body2"><b>总分:</b> @majorQG.Score 分</MudText>
|
|
||||||
}
|
|
||||||
</MudStack>
|
|
||||||
|
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(majorQG.Descript))
|
|
||||||
{
|
|
||||||
<MudText Typo="Typo.body2">@((MarkupString)majorQG.Descript.Replace("\n", "<br />"))</MudText>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (majorQG.SubQuestions.Any())
|
|
||||||
{
|
|
||||||
@foreach (var question in majorQG.SubQuestions)
|
|
||||||
{
|
|
||||||
<QuestionCard Question="question" Elevation=@Elevation Class="my-2 pa-1" />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (majorQG.SubQuestionGroups.Any())
|
|
||||||
{
|
|
||||||
<ExamGroupView MajorQGList="majorQG.SubQuestionGroups" Elevation="1" />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</MudPaper>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
[Parameter]
|
|
||||||
public List<QuestionGroupDto> MajorQGList { get; set; } = new List<QuestionGroupDto>();
|
|
||||||
|
|
||||||
[Parameter]
|
|
||||||
public string Class { get; set; } = "my-2 pa-1";
|
|
||||||
|
|
||||||
[Parameter]
|
|
||||||
public int Elevation { get; set; } = 0;
|
|
||||||
}
|
|
@@ -1,9 +1,11 @@
|
|||||||
@using Entities.DTO
|
@using Entities.DTO
|
||||||
@using Microsoft.AspNetCore.Authorization
|
@using Microsoft.AspNetCore.Authorization
|
||||||
@using TechHelper.Client.Exam
|
@using TechHelper.Client.Exam
|
||||||
|
@using TechHelper.Client.Pages.Common.Exam
|
||||||
|
|
||||||
@page "/exam/manage"
|
@page "/exam/manage"
|
||||||
|
@using Entities.DTO
|
||||||
|
@using TechHelper.Client.Services
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
|
|
||||||
|
|
||||||
@@ -19,7 +21,8 @@ else
|
|||||||
<MudPaper Class="d-flex flex-wrap flex-grow-0 gap-4" Height="100%" Width="100%">
|
<MudPaper Class="d-flex flex-wrap flex-grow-0 gap-4" Height="100%" Width="100%">
|
||||||
@foreach (var item in examDtos)
|
@foreach (var item in examDtos)
|
||||||
{
|
{
|
||||||
<ExamPreview examDto="item" Width="256px" Height="256px"> </ExamPreview>
|
@* <ExamPreview AssignmentDto="item" Width="256px" Height="256px"> </ExamPreview> *@
|
||||||
|
<AssignmentInfoCard></AssignmentInfoCard>
|
||||||
}
|
}
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|
||||||
@@ -33,7 +36,7 @@ else
|
|||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
private Task<AuthenticationState> authenticationStateTask { get; set; }
|
private Task<AuthenticationState> authenticationStateTask { get; set; }
|
||||||
|
|
||||||
private List<ExamDto> examDtos = new List<ExamDto>();
|
private List<AssignmentDto> examDtos = new List<AssignmentDto>();
|
||||||
|
|
||||||
private bool isloding = true;
|
private bool isloding = true;
|
||||||
|
|
||||||
@@ -47,10 +50,17 @@ else
|
|||||||
{
|
{
|
||||||
isloding = true;
|
isloding = true;
|
||||||
Snackbar.Add("正在加载", Severity.Info);
|
Snackbar.Add("正在加载", Severity.Info);
|
||||||
var result = await ExamService.GetAllExam(authenticationStateTask.Result.User.Identity.Name);
|
var result = await ExamService.GetAllExam();
|
||||||
examDtos = result.Result as List<ExamDto> ?? new List<ExamDto>();
|
if (result.Status)
|
||||||
isloding = false;
|
{
|
||||||
|
examDtos = result.Result as List<AssignmentDto> ?? new List<AssignmentDto>();
|
||||||
Snackbar.Add("加载成功", Severity.Info);
|
Snackbar.Add("加载成功", Severity.Info);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Snackbar.Add($"加载失败 {result.Message}", Severity.Error);
|
||||||
|
}
|
||||||
|
isloding = false;
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
<MudPaper Class="overflow-hidden " Style="background-color:pink" Width="@Width" Height="@Height" MaxHeight="@MaxHeight" MaxWidth="@MaxWidth">
|
<MudPaper Class="overflow-hidden " Style="background-color:pink" Width="@Width" Height="@Height" MaxHeight="@MaxHeight" MaxWidth="@MaxWidth">
|
||||||
<MudPaper Class="d-flex flex-column flex-grow-1 justify-content-between" Height="100%" Style="background-color:green">
|
<MudPaper Class="d-flex flex-column flex-grow-1 justify-content-between" Height="100%" Style="background-color:green">
|
||||||
|
|
||||||
<MudText Typo="Typo.body2"> @examDto.AssignmentTitle </MudText>
|
<MudText Typo="Typo.body2"> @AssignmentDto.Title </MudText>
|
||||||
|
|
||||||
|
|
||||||
<MudPaper>
|
<MudPaper>
|
||||||
@@ -12,7 +12,11 @@
|
|||||||
|
|
||||||
<MudButton OnClick="ExamClick"> 详情 </MudButton>
|
<MudButton OnClick="ExamClick"> 详情 </MudButton>
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" aria-label="delete" />
|
<MudIconButton Icon="@Icons.Material.Filled.Delete" aria-label="delete" />
|
||||||
<MudIconButton Icon="@Icons.Custom.Brands.GitHub" OnClick="CheckExam" Color="Color.Primary" aria-label="github" />
|
@if (bteacher)
|
||||||
|
{
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Check" OnClick="CheckExam" Color="Color.Primary" aria-label="github" />
|
||||||
|
}
|
||||||
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Favorite" Color="Color.Secondary" aria-label="add to favorite" />
|
<MudIconButton Icon="@Icons.Material.Filled.Favorite" Color="Color.Secondary" aria-label="add to favorite" />
|
||||||
</MudButtonGroup>
|
</MudButtonGroup>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
@@ -21,12 +25,16 @@
|
|||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
[CascadingParameter]
|
||||||
|
private Task<AuthenticationState> authenticationStateTask { get; set; }
|
||||||
|
|
||||||
|
private bool bteacher = false;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
public NavigationManager navigationManager { get; set; }
|
public NavigationManager navigationManager { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public ExamDto examDto { get; set; }
|
public AssignmentDto AssignmentDto { get; set; }
|
||||||
|
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
@@ -45,13 +53,19 @@
|
|||||||
public string? MaxHeight { get; set; } = "64";
|
public string? MaxHeight { get; set; } = "64";
|
||||||
|
|
||||||
|
|
||||||
|
protected override Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
bteacher = authenticationStateTask.Result.User.IsInRole("Teacher");
|
||||||
|
return base.OnInitializedAsync();
|
||||||
|
}
|
||||||
|
|
||||||
private void ExamClick()
|
private void ExamClick()
|
||||||
{
|
{
|
||||||
navigationManager.NavigateTo($"exam/edit/{examDto.AssignmentId}");
|
navigationManager.NavigateTo($"exam/edit/{AssignmentDto.Id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CheckExam()
|
private void CheckExam()
|
||||||
{
|
{
|
||||||
navigationManager.NavigateTo($"exam/check/{examDto.AssignmentId}");
|
navigationManager.NavigateTo($"exam/check/{AssignmentDto.Id}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
87
TechHelper.Client/Pages/Exam/ExamView/ExamStructView.razor
Normal file
87
TechHelper.Client/Pages/Exam/ExamView/ExamStructView.razor
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
@using Entities.Contracts
|
||||||
|
@using Entities.DTO
|
||||||
|
@using TechHelper.Client.Exam
|
||||||
|
@using TechHelper.Client.Pages.Exam.QuestionCard
|
||||||
|
|
||||||
|
<MudPaper @onclick:stopPropagation Style="background-color:transparent" Elevation="0">
|
||||||
|
<MudPaper Elevation=@Elevation Class=@Class @onclick="HandleClick" Style="@Style">
|
||||||
|
|
||||||
|
<MudStack Row="true" Class="justify-content-between align-content-center" Style="background-color: transparent">
|
||||||
|
<MudText Class="justify-content-lg-start" Typo="Typo.h6">@ExamStruct.Title</MudText>
|
||||||
|
<MudStack Row="true" Class="align-content-center">
|
||||||
|
<MudText Class="ma-auto" Align="Align.Center" Typo="Typo.body2"> Num: @ExamStruct.ChildrenAssignmentQuestion.Count</MudText>
|
||||||
|
<MudText Class="ma-auto" Align="Align.Center" Typo="Typo.body2"><b>总分:</b> @ExamStruct.Score 分</MudText>
|
||||||
|
<MudToggleIconButton @bind-Toggled="ExamStruct.BCorrect"
|
||||||
|
Icon="@Icons.Material.Filled.Close"
|
||||||
|
Color="@Color.Error"
|
||||||
|
ToggledIcon="@Icons.Material.Filled.Check"
|
||||||
|
ToggledColor="@Color.Success"
|
||||||
|
title="@(ExamStruct.BCorrect ? "On" : "Off")" />
|
||||||
|
|
||||||
|
<MudIconButton Color="Color.Tertiary" Icon="@Icons.Material.Filled.ExpandLess" Size="Size.Small" />
|
||||||
|
<MudIconButton Color="Color.Tertiary" Icon="@Icons.Material.Filled.ExpandMore" Size="Size.Small" />
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Delete" aria-label="delete" Size="Size.Small" />
|
||||||
|
<MudChip T="string" Color="Color.Info" Class="justify-content-end">@ExamStruct.StructType</MudChip>
|
||||||
|
</MudStack>
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
@if (ExamStruct.Question != null)
|
||||||
|
{
|
||||||
|
<QuestionCard Question="ExamStruct.Question" Index="ExamStruct.Index" Elevation=0 Class="my-2 pa-1 rounded-xl" />
|
||||||
|
}
|
||||||
|
|
||||||
|
@foreach (var examStruct in ExamStruct.ChildrenAssignmentQuestion)
|
||||||
|
{
|
||||||
|
<ExamStructView ExamStruct="examStruct" ClickedStruct="HandleChildStructClick" Elevation=@(examStruct.Question != null
|
||||||
|
&& examStruct.ChildrenAssignmentQuestion.Count == 0 ? 0 : 0) Class="@($"my-2 pa-1 rounded-xl {(examStruct.StructType != AssignmentStructType.Question ? "my-5" : "my-1")}")"
|
||||||
|
Style=@(examStruct.StructType switch
|
||||||
|
{
|
||||||
|
AssignmentStructType.Question => "background-color: #ececec",
|
||||||
|
AssignmentStructType.Group => "background-color: #ffffff",
|
||||||
|
AssignmentStructType.Struct => "background-color: #cccccccc",
|
||||||
|
AssignmentStructType.SubQuestion => "background-color: #ffffff",
|
||||||
|
AssignmentStructType.Option => "background-color: #ffffff",
|
||||||
|
_ => "background-color: transparent"
|
||||||
|
}) />
|
||||||
|
}
|
||||||
|
|
||||||
|
</MudPaper>
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
|
|
||||||
|
@* Style=@(examStruct.StructType switch
|
||||||
|
{
|
||||||
|
AssignmentStructType.Question => "background-color: #ffffff",
|
||||||
|
AssignmentStructType.Composite => "background-color: #ececec",
|
||||||
|
AssignmentStructType.Struct => "background-color: #dcdcdc",
|
||||||
|
AssignmentStructType.SubQuestion => "background-color: #ffffff",
|
||||||
|
AssignmentStructType.Option => "background-color: #dddddd",
|
||||||
|
_ => "background-color: transparent"
|
||||||
|
}) *@
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public AssignmentQuestionDto ExamStruct { get; set; } = new AssignmentQuestionDto();
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<AssignmentQuestionDto> ClickedStruct { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string Class { get; set; } = "my-2 pa-1";
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public int Elevation { get; set; } = 0;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string Style { get; set; } = "background-color : #eeeeee";
|
||||||
|
|
||||||
|
private async void HandleClick()
|
||||||
|
{
|
||||||
|
await ClickedStruct.InvokeAsync(ExamStruct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void HandleChildStructClick(AssignmentQuestionDto clickedChildExamStruct)
|
||||||
|
{
|
||||||
|
await ClickedStruct.InvokeAsync(clickedChildExamStruct);
|
||||||
|
}
|
||||||
|
}
|
@@ -4,11 +4,11 @@
|
|||||||
@if (ParsedExam != null)
|
@if (ParsedExam != null)
|
||||||
{
|
{
|
||||||
|
|
||||||
<MudPaper Height="@Height" Class="@Class" Style="@Style" Width="@Width">
|
<MudPaper Height="@Height" Class="@Class" Style="@Style" Width="@Width" Elevation="5">
|
||||||
<MudText Class="d-flex justify-content-center" Typo="Typo.h6"> @ParsedExam.AssignmentTitle </MudText>
|
<MudText Class="d-flex justify-content-center" Typo="Typo.button"> <b> @ParsedExam.Title </b></MudText>
|
||||||
<MudText Typo="Typo.body1"> @ParsedExam.Description </MudText>
|
<MudText Typo="Typo.body1"> @ParsedExam.Description </MudText>
|
||||||
|
|
||||||
<ExamGroupView MajorQGList="@ParsedExam.QuestionGroups.SubQuestionGroups" Elevation="1" Class="ma-0 pa-2" />
|
<ExamStructView ExamStruct="@ParsedExam.ExamStruct" Elevation="0" ClickedStruct="HandleClickedStruct" Class="ma-0 pa-2 rounded-xl" />
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,11 @@ else
|
|||||||
@code {
|
@code {
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public ExamDto ParsedExam { get; set; } = new ExamDto();
|
public AssignmentDto ParsedExam { get; set; } = new AssignmentDto();
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<AssignmentQuestionDto> ClickedStruct { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string Height { get; set; } = "100%";
|
public string Height { get; set; } = "100%";
|
||||||
[Parameter]
|
[Parameter]
|
||||||
@@ -33,4 +37,10 @@ else
|
|||||||
public string Class { get; set; } = "";
|
public string Class { get; set; } = "";
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string Style { get; set; } = "";
|
public string Style { get; set; } = "";
|
||||||
|
|
||||||
|
|
||||||
|
private void HandleClickedStruct(AssignmentQuestionDto dto)
|
||||||
|
{
|
||||||
|
ClickedStruct.InvokeAsync(dto);
|
||||||
|
}
|
||||||
}
|
}
|
@@ -1,49 +1,23 @@
|
|||||||
@using TechHelper.Client.Exam
|
@using TechHelper.Client.Exam
|
||||||
|
@using Entities.Contracts // Assuming SubjectAreaEnum is defined here, adjust if not
|
||||||
|
|
||||||
<MudPaper Outlined="true" Class="mt-2">
|
<MudPaper Outlined="true" Class="mt-2">
|
||||||
|
|
||||||
<MudRadioGroup @bind-Value="_examParser">
|
<MudText Typo="Typo.h6" Class="mb-4">Current Parsing Rules</MudText>
|
||||||
@foreach (ExamParserEnum item in Enum.GetValues(typeof(ExamParserEnum)))
|
|
||||||
{
|
|
||||||
<MudRadio T="ExamParserEnum" Value="@item">@item</MudRadio>
|
|
||||||
}
|
|
||||||
</MudRadioGroup>
|
|
||||||
|
|
||||||
<MudTextField @bind-Value="_ParserConfig" Label="正则表达式模式" Variant="Variant.Outlined" FullWidth="true" Class="mb-2" />
|
@* Display Question Patterns *@
|
||||||
<MudNumericField Label="优先级" @bind-Value="_Priority" Variant="Variant.Outlined" Min="1" Max="100" />
|
|
||||||
<MudButton OnClick="AddPattern" Variant="Variant.Filled" Color="Color.Primary" Class="mt-2">添加模式</MudButton>
|
|
||||||
|
|
||||||
|
|
||||||
<MudText Typo="Typo.subtitle1" Class="mb-2">所有已配置模式:</MudText>
|
|
||||||
|
|
||||||
|
|
||||||
@if (ExamParserConfig.MajorQuestionGroupPatterns.Any())
|
|
||||||
{
|
|
||||||
<MudExpansionPanel Text="大题组模式详情" Class="mb-2">
|
|
||||||
<MudStack>
|
|
||||||
@foreach (var config in ExamParserConfig.MajorQuestionGroupPatterns)
|
|
||||||
{
|
|
||||||
<MudChip T="string">
|
|
||||||
**模式:** <code>@config.Pattern</code>, **优先级:** @config.Priority
|
|
||||||
</MudChip>
|
|
||||||
}
|
|
||||||
</MudStack>
|
|
||||||
</MudExpansionPanel>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<MudText Typo="Typo.body2" Class="mb-2">暂无大题组模式。</MudText>
|
|
||||||
}
|
|
||||||
|
|
||||||
@* 题目模式详情 *@
|
|
||||||
@if (ExamParserConfig.QuestionPatterns.Any())
|
@if (ExamParserConfig.QuestionPatterns.Any())
|
||||||
{
|
{
|
||||||
<MudExpansionPanel Text="题目模式详情" Class="mb-2">
|
<MudExpansionPanel Text="Question Patterns" Class="mb-2" IsInitiallyExpanded="true">
|
||||||
<MudStack>
|
<MudStack Spacing="1">
|
||||||
@foreach (var config in ExamParserConfig.QuestionPatterns)
|
@foreach (var config in ExamParserConfig.QuestionPatterns)
|
||||||
{
|
{
|
||||||
<MudChip T="string">
|
<MudChip T="string" Color="Color.Info" Variant="Variant.Outlined" Class="d-flex justify-content-between align-items-center">
|
||||||
**模式:** <code>@config.Pattern</code>, **优先级:** @config.Priority
|
<div class="d-flex flex-column align-items-start">
|
||||||
|
<MudText Typo="Typo.body2">**Type:** @config.Type</MudText>
|
||||||
|
<MudText Typo="Typo.body2">**Pattern:** <code>@config.Pattern</code></MudText>
|
||||||
|
</div>
|
||||||
|
<MudText Typo="Typo.body2">**Priority:** @config.Priority</MudText>
|
||||||
</MudChip>
|
</MudChip>
|
||||||
}
|
}
|
||||||
</MudStack>
|
</MudStack>
|
||||||
@@ -51,17 +25,22 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<MudText Typo="Typo.body2" Class="mb-2">暂无题目模式。</MudText>
|
<MudText Typo="Typo.body2" Class="mb-2">No question patterns configured.</MudText>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@* Display Option Patterns *@
|
||||||
@if (ExamParserConfig.OptionPatterns.Any())
|
@if (ExamParserConfig.OptionPatterns.Any())
|
||||||
{
|
{
|
||||||
<MudExpansionPanel Text="选项模式详情" Class="mb-2">
|
<MudExpansionPanel Text="Option Patterns" Class="mb-2" IsInitiallyExpanded="true">
|
||||||
<MudStack>
|
<MudStack Spacing="1">
|
||||||
@foreach (var config in ExamParserConfig.OptionPatterns)
|
@foreach (var config in ExamParserConfig.OptionPatterns)
|
||||||
{
|
{
|
||||||
<MudChip T="string">
|
<MudChip T="string" Color="Color.Warning" Variant="Variant.Outlined" Class="d-flex justify-content-between align-items-center">
|
||||||
**模式:** <code>@config.Pattern</code>, **优先级:** @config.Priority
|
<div class="d-flex flex-column align-items-start">
|
||||||
|
<MudText Typo="Typo.body2">**Type:** @config.Type</MudText>
|
||||||
|
<MudText Typo="Typo.body2">**Pattern:** <code>@config.Pattern</code></MudText>
|
||||||
|
</div>
|
||||||
|
<MudText Typo="Typo.body2">**Priority:** @config.Priority</MudText>
|
||||||
</MudChip>
|
</MudChip>
|
||||||
}
|
}
|
||||||
</MudStack>
|
</MudStack>
|
||||||
@@ -69,59 +48,15 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<MudText Typo="Typo.body2" Class="mb-2">暂无选项模式。</MudText>
|
<MudText Typo="Typo.body2" Class="mb-2">No option patterns configured.</MudText>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
<MudButton Variant="Variant.Filled" Color="Color.Secondary" OnClick="ResetPatterns">重置默认规则</MudButton>
|
|
||||||
|
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|
||||||
|
|
||||||
public ExamParserEnum _examParser { get; set; } = ExamParserEnum.MajorQuestionGroupPatterns;
|
|
||||||
private string _ParserConfig;
|
|
||||||
private int _Priority = 1;
|
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public ExamParserConfig ExamParserConfig { get; set; } = new ExamParserConfig();
|
public ExamParserConfig ExamParserConfig { get; set; } = new ExamParserConfig();
|
||||||
|
|
||||||
[Inject]
|
// No other properties or methods are needed as the component is now purely for display.
|
||||||
public ISnackbar Snackbar { get; set; }
|
|
||||||
|
|
||||||
private void AddPattern()
|
|
||||||
{
|
|
||||||
|
|
||||||
switch ((ExamParserEnum)_examParser)
|
|
||||||
{
|
|
||||||
case ExamParserEnum.MajorQuestionGroupPatterns:
|
|
||||||
ExamParserConfig.MajorQuestionGroupPatterns.Add(new RegexPatternConfig(_ParserConfig, _Priority));
|
|
||||||
Snackbar.Add($"已添加大题组模式: {_ParserConfig}, 优先级: {_Priority}", Severity.Success);
|
|
||||||
break;
|
|
||||||
case ExamParserEnum.QuestionPatterns:
|
|
||||||
ExamParserConfig.QuestionPatterns.Add(new RegexPatternConfig(_ParserConfig, _Priority));
|
|
||||||
Snackbar.Add($"已添加题目模式: {_ParserConfig}, 优先级: {_Priority}", Severity.Success);
|
|
||||||
break;
|
|
||||||
case ExamParserEnum.OptionPatterns:
|
|
||||||
ExamParserConfig.OptionPatterns.Add(new RegexPatternConfig(_ParserConfig, _Priority));
|
|
||||||
Snackbar.Add($"已添加选项模式: {_ParserConfig}, 优先级: {_Priority}", Severity.Success);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
Snackbar.Add("请选择要添加的模式类型。");
|
|
||||||
break;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
StateHasChanged();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void ResetPatterns()
|
|
||||||
{
|
|
||||||
ExamParserConfig = new ExamParserConfig();
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
}
|
}
|
@@ -1,36 +0,0 @@
|
|||||||
@using Entities.DTO
|
|
||||||
@using TechHelper.Client.Exam
|
|
||||||
|
|
||||||
<MudPaper Class=@Class Elevation=@Elevation Outlined="false">
|
|
||||||
|
|
||||||
<MudText Typo="Typo.subtitle1">
|
|
||||||
<b>@Question.Index</b> @((MarkupString)Question.Stem.Replace("\n", "<br />"))
|
|
||||||
@if (Question.Score > 0)
|
|
||||||
{
|
|
||||||
<MudText Typo="Typo.body2" Class="d-inline ml-2">(@Question.Score 分)</MudText>
|
|
||||||
}
|
|
||||||
</MudText>
|
|
||||||
|
|
||||||
@if (Question.Options.Any())
|
|
||||||
{
|
|
||||||
<div class="mt-2">
|
|
||||||
@foreach (var option in Question.Options)
|
|
||||||
{
|
|
||||||
var tempOption = option;
|
|
||||||
<p>@((MarkupString)(tempOption.Value.Replace("\n", "<br />")))</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
</MudPaper>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
[Parameter]
|
|
||||||
public SubQuestionDto Question { get; set; } = new SubQuestionDto();
|
|
||||||
|
|
||||||
[Parameter]
|
|
||||||
public string Class { get; set; }
|
|
||||||
|
|
||||||
[Parameter]
|
|
||||||
public int Elevation { get; set; }
|
|
||||||
}
|
|
46
TechHelper.Client/Pages/Exam/QuestionCard/QuestionCard.razor
Normal file
46
TechHelper.Client/Pages/Exam/QuestionCard/QuestionCard.razor
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
@using Entities.Contracts
|
||||||
|
@using Entities.DTO
|
||||||
|
@using TechHelper.Client.Exam
|
||||||
|
|
||||||
|
<MudPaper Class=@Class Elevation=@Elevation Outlined="false">
|
||||||
|
|
||||||
|
<MudText Typo="Typo.subtitle1">
|
||||||
|
<b>@Index</b> @((MarkupString)Question.Title.Replace("\n", "<br />"))
|
||||||
|
@if (Score > 0)
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.body2" Class="d-inline ml-2">(@Score 分)</MudText>
|
||||||
|
}
|
||||||
|
</MudText>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(Question.Options))
|
||||||
|
{
|
||||||
|
<div class="mt-2">
|
||||||
|
@foreach (var option in Question.Options.ParseOptionsFromText())
|
||||||
|
{
|
||||||
|
var tempOption = option;
|
||||||
|
<p>@((MarkupString)(tempOption.Replace("\n", "<br />")))</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public QuestionDto Question { get; set; } = new QuestionDto();
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public AssignmentStructType Type { get; set; } = AssignmentStructType.Question;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public byte Index { get; set; } = 0;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public byte Score { get; set; } = 0;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string Class { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public int Elevation { get; set; }
|
||||||
|
}
|
18
TechHelper.Client/Pages/Exam/QuestionCard/QuestionEdit.razor
Normal file
18
TechHelper.Client/Pages/Exam/QuestionCard/QuestionEdit.razor
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
@using Entities.DTO
|
||||||
|
@using TechHelper.Client.Exam
|
||||||
|
|
||||||
|
|
||||||
|
<MudPaper Elevation="1" Class="ma-4 pa-5 rounded-xl">
|
||||||
|
@* <MudText>@Question.Id</MudText> *@
|
||||||
|
<MudText Class="mt-3" Typo="Typo.button"><b>问题属性</b></MudText>
|
||||||
|
<MudTextField @bind-Value="Question.Title" Label="Title" Variant="Variant.Text" Margin="Margin.Dense" AutoGrow="true" />
|
||||||
|
<MudTextField @bind-Value="Question.Answer" Label="Answer" Variant="Variant.Text" Adornment="Adornment.End" AdornmentText="." Margin="Margin.Dense" AutoGrow="true" />
|
||||||
|
<MudTextField @bind-Value="Question.Options" Label="Options" Variant="Variant.Text" Adornment="Adornment.End" AdornmentText="." Margin="Margin.Dense" AutoGrow="true" />
|
||||||
|
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public QuestionDto Question { get; set; } = new QuestionDto();
|
||||||
|
}
|
9
TechHelper.Client/Pages/Exam/QuestionDetailView.razor
Normal file
9
TechHelper.Client/Pages/Exam/QuestionDetailView.razor
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<MudText> Title </MudText>
|
||||||
|
<MudText> Error Person Num </MudText>
|
||||||
|
<MudText> List Error Person </MudText>
|
||||||
|
<MudText> Lesson </MudText>
|
||||||
|
<MudText> Keypoint </MudText>
|
||||||
|
<MudText> Answer </MudText>
|
||||||
|
@code {
|
||||||
|
|
||||||
|
}
|
@@ -1,118 +1,23 @@
|
|||||||
@page "/"
|
@page "/"
|
||||||
@using Microsoft.AspNetCore.Authorization
|
@using Microsoft.AspNetCore.Authorization
|
||||||
|
@using TechHelper.Client.Pages.Common.Exam
|
||||||
|
|
||||||
<AuthorizeView Roles="Administrator">
|
<AuthorizeView Roles="Student">
|
||||||
|
<Authorized>
|
||||||
<MudText> Hello @context.User.Identity.Name</MudText>
|
<TechHelper.Client.Pages.Student.HomePage />
|
||||||
@foreach (var item in context.User.Claims)
|
</Authorized>
|
||||||
{
|
|
||||||
<MudPaper class="ma-2 pa-2">
|
|
||||||
<MudText> @item.Value </MudText>
|
|
||||||
<MudText> @item.Issuer </MudText>
|
|
||||||
<MudText> @item.Subject </MudText>
|
|
||||||
<MudText> @item.Properties </MudText>
|
|
||||||
<MudText> @item.ValueType </MudText>
|
|
||||||
</MudPaper>
|
|
||||||
}
|
|
||||||
|
|
||||||
Welcome to your new app.
|
|
||||||
</AuthorizeView>
|
</AuthorizeView>
|
||||||
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
<AssignmentInfoCard></AssignmentInfoCard>
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
@code {
|
||||||
<MudText>Hello </MudText>
|
[CascadingParameter]
|
||||||
<MudText>Hello </MudText>
|
private Task<AuthenticationState> authenticationStateTask { get; set; }
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
protected override Task OnInitializedAsync()
|
||||||
<MudText>Hello </MudText>
|
{
|
||||||
<MudText>Hello </MudText>
|
return base.OnInitializedAsync();
|
||||||
<MudText>Hello </MudText>
|
Console.WriteLine(authenticationStateTask.Result.User.IsInRole("Student"));
|
||||||
<MudText>Hello </MudText>
|
}
|
||||||
<MudText>Hello </MudText>
|
}
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
||||||
<MudText>Hello </MudText>
|
|
@@ -56,3 +56,4 @@ else
|
|||||||
<MudText Class="ma-3 pa-3"> 班级 : @authenticationStateTask.Result.User.FindFirst("Class")?.Value.ToString() </MudText>
|
<MudText Class="ma-3 pa-3"> 班级 : @authenticationStateTask.Result.User.FindFirst("Class")?.Value.ToString() </MudText>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
}
|
}
|
||||||
|
<RestoreUserRole></RestoreUserRole>
|
@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Components.Authorization;
|
|||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
using System.Collections.Frozen;
|
using System.Collections.Frozen;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Security.Claims;
|
||||||
using TechHelper.Client.HttpRepository;
|
using TechHelper.Client.HttpRepository;
|
||||||
using TechHelper.Client.Services;
|
using TechHelper.Client.Services;
|
||||||
|
|
||||||
|
@@ -3,10 +3,7 @@
|
|||||||
@using System.ComponentModel.DataAnnotations
|
@using System.ComponentModel.DataAnnotations
|
||||||
@using Microsoft.AspNetCore.Identity
|
@using Microsoft.AspNetCore.Identity
|
||||||
|
|
||||||
|
<MudPaper Class="flex-grow-1">
|
||||||
<PageTitle>Profile</PageTitle>
|
|
||||||
|
|
||||||
<h3>Profile</h3>
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
@@ -27,6 +24,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</MudPaper>
|
||||||
@code {
|
@code {
|
||||||
|
|
||||||
private string? username;
|
private string? username;
|
||||||
|
141
TechHelper.Client/Pages/Student/HomePage.razor
Normal file
141
TechHelper.Client/Pages/Student/HomePage.razor
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<MudPaper Class="flex-grow-1 w-100 h-100 ma-auto">
|
||||||
|
<MudGrid Class="w-100 h-100">
|
||||||
|
<MudItem xs="12" sm="4">
|
||||||
|
<MudPaper Style="background-color:transparent" Class="w-100 justify-content-center">
|
||||||
|
<MudChart ChartType="ChartType.Donut" Width="200px" Height="200px" InputData="@data" InputLabels="@labels" Class="ma-auto">
|
||||||
|
<CustomGraphics>
|
||||||
|
<text class="donut-inner-text" x="50%" y="35%" dominant-baseline="middle" text-anchor="middle" fill="black" font-family="Helvetica" font-size="20">Total</text>
|
||||||
|
<text class="donut-inner-text" x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" fill="black" font-family="Helvetica" font-size="50">@data.Sum().ToString()</text>
|
||||||
|
</CustomGraphics>
|
||||||
|
</MudChart>
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
|
<MudPaper Style="background-color:transparent" Class="w-100 pa-5">
|
||||||
|
<TechHelper.Client.Pages.Common.SimpleCard Style="background-color:#ff4081">
|
||||||
|
<BodyContent>
|
||||||
|
<MudText>BodyContent</MudText>
|
||||||
|
</BodyContent>
|
||||||
|
<TitleContent>
|
||||||
|
<MudText>TitleContent</MudText>
|
||||||
|
</TitleContent>
|
||||||
|
<FooterContent>
|
||||||
|
<MudText>FooterContent</MudText>
|
||||||
|
</FooterContent>
|
||||||
|
</TechHelper.Client.Pages.Common.SimpleCard>
|
||||||
|
|
||||||
|
<TechHelper.Client.Pages.Common.SimpleCard Style="background-color:#1ec8a5">
|
||||||
|
<BodyContent>
|
||||||
|
<MudText>BodyContent</MudText>
|
||||||
|
</BodyContent>
|
||||||
|
<TitleContent>
|
||||||
|
<MudText>TitleContent</MudText>
|
||||||
|
</TitleContent>
|
||||||
|
<FooterContent>
|
||||||
|
<MudText>FooterContent</MudText>
|
||||||
|
</FooterContent>
|
||||||
|
</TechHelper.Client.Pages.Common.SimpleCard>
|
||||||
|
|
||||||
|
<TechHelper.Client.Pages.Common.SimpleCard Style="background-color:#4680ff">
|
||||||
|
<TitleContent>
|
||||||
|
<MudText Typo="Typo.body1">TitleContent</MudText>
|
||||||
|
</TitleContent>
|
||||||
|
<BodyContent>
|
||||||
|
<MudText Typo="Typo.button"><b>BodyContent</b></MudText>
|
||||||
|
</BodyContent>
|
||||||
|
<FooterContent>
|
||||||
|
<MudText Typo="Typo.body2">leran about this curson</MudText>
|
||||||
|
</FooterContent>
|
||||||
|
</TechHelper.Client.Pages.Common.SimpleCard>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
|
||||||
|
<MudItem xs="12" sm="8">
|
||||||
|
<MudPaper Style="background-color:transparent" Class="w-100 h-100">
|
||||||
|
|
||||||
|
<TechHelper.Client.Pages.Common.SimpleCard Style="background-color:#c2bef8" Height="350px">
|
||||||
|
<TitleContent>
|
||||||
|
<MudText Typo="Typo.button"><b>Visits Summary:</b></MudText>
|
||||||
|
</TitleContent>
|
||||||
|
<BodyContent>
|
||||||
|
<MudChart ChartType="ChartType.Line" LegendPosition="Position.Left" Class="pt-55" ChartSeries="@Series" XAxisLabels="@XAxisLabels" Height="110%" Width="100%" AxisChartOptions="_axisChartOptions" ChartOptions="options"></MudChart>
|
||||||
|
</BodyContent>
|
||||||
|
<FooterContent>
|
||||||
|
<MudText Typo="Typo.body2">leran about this curson</MudText>
|
||||||
|
</FooterContent>
|
||||||
|
</TechHelper.Client.Pages.Common.SimpleCard>
|
||||||
|
|
||||||
|
|
||||||
|
@* <TechHelper.Client.Pages.Common.SimpleCard Style="background-color:#c2bef8" Height="100%">
|
||||||
|
<TitleContent>
|
||||||
|
<MudText Typo="Typo.button"><b>Visits Summary:</b></MudText>
|
||||||
|
</TitleContent>
|
||||||
|
<BodyContent>
|
||||||
|
<MudDataGrid Items="@Elements" Filterable="true" FilterMode="@_filterMode" FilterCaseSensitivity="@_caseSensitivity">
|
||||||
|
<Columns>
|
||||||
|
<PropertyColumn Property="x => x.Number" Title="Nr" Filterable="false" />
|
||||||
|
<PropertyColumn Property="x => x.Sign" />
|
||||||
|
<PropertyColumn Property="x => x.Name" />
|
||||||
|
<PropertyColumn Property="x => x.Position" Filterable="false" />
|
||||||
|
<PropertyColumn Property="x => x.Molar" Title="Molar mass" />
|
||||||
|
<PropertyColumn Property="x => x.Group" Title="Category" />
|
||||||
|
</Columns>
|
||||||
|
<PagerContent>
|
||||||
|
<MudDataGridPager T="Element" />
|
||||||
|
</PagerContent>
|
||||||
|
</MudDataGrid>
|
||||||
|
</BodyContent>
|
||||||
|
<FooterContent>
|
||||||
|
<MudText Typo="Typo.body2">leran about this curson</MudText>
|
||||||
|
</FooterContent>
|
||||||
|
</TechHelper.Client.Pages.Common.SimpleCard> *@
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@code {
|
||||||
|
public double[] data = { 25, 77, 28, 5 };
|
||||||
|
public string[] labels = { "Oil", "Coal", "Gas", "Biomass" };
|
||||||
|
private AxisChartOptions _axisChartOptions = new AxisChartOptions();
|
||||||
|
private ChartOptions options = new ChartOptions();
|
||||||
|
public List<ChartSeries> Series = new List<ChartSeries>()
|
||||||
|
{
|
||||||
|
new ChartSeries() { Name = "Series 1", Data = new double[] { 90, 79, 72, 69, 62, 62, 55, 65, 70 } },
|
||||||
|
new ChartSeries() { Name = "Series 2", Data = new double[] { 35, 41, 35, 51, 49, 62, 69, 91, 148 } },
|
||||||
|
};
|
||||||
|
public string[] XAxisLabels = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep" };
|
||||||
|
|
||||||
|
Random random = new Random();
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
options.InterpolationOption = InterpolationOption.NaturalSpline;
|
||||||
|
options.YAxisFormat = "c2";
|
||||||
|
_axisChartOptions.MatchBoundsToSize = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RandomizeData()
|
||||||
|
{
|
||||||
|
foreach (var series in Series)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < series.Data.Length - 1; i++)
|
||||||
|
{
|
||||||
|
series.Data[i] = random.NextDouble() * 100 + 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnClickMenu(InterpolationOption interpolationOption)
|
||||||
|
{
|
||||||
|
options.InterpolationOption = interpolationOption;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
28
TechHelper.Client/Pages/Teacher/StudentsView.razor
Normal file
28
TechHelper.Client/Pages/Teacher/StudentsView.razor
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
@using Entities.Contracts
|
||||||
|
@using Entities.DTO
|
||||||
|
@using TechHelper.Client.Services
|
||||||
|
<h3>StudentsView</h3>
|
||||||
|
|
||||||
|
|
||||||
|
@foreach(var cs in ClassStudents)
|
||||||
|
{
|
||||||
|
<MudText> @cs.DisplayName </MudText>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[CascadingParameter]
|
||||||
|
private Task<AuthenticationState> authenticationStateTask { get; set; }
|
||||||
|
|
||||||
|
private List<StudentDto> ClassStudents { get; set; } = new List<StudentDto>();
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
public IClassServices ClassServices { get; set; }
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
var result = await ClassServices.GetClassStudents();
|
||||||
|
ClassStudents = result.Result as List<StudentDto> ?? new List<StudentDto>();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
@@ -13,8 +13,8 @@ using TechHelper.Client.Services;
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
|
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
|
||||||
using TechHelper.Client.AI;
|
using TechHelper.Client.AI;
|
||||||
using TechHelper.Client.Exam;
|
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using TechHelper.Context;
|
||||||
|
|
||||||
|
|
||||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||||
@@ -27,12 +27,16 @@ builder.Services.AddOidcAuthentication(options =>
|
|||||||
builder.Configuration.Bind("Local", options.ProviderOptions);
|
builder.Configuration.Bind("Local", options.ProviderOptions);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
builder.Services.AddAutoMapper(typeof(AutoMapperProFile).Assembly);
|
||||||
|
|
||||||
|
|
||||||
builder.Services.Configure<ApiConfiguration>(builder.Configuration.GetSection("ApiConfiguration"));
|
builder.Services.Configure<ApiConfiguration>(builder.Configuration.GetSection("ApiConfiguration"));
|
||||||
builder.Services.AddAuthorizationCore();
|
builder.Services.AddAuthorizationCore();
|
||||||
builder.Services.AddCascadingAuthenticationState();
|
builder.Services.AddCascadingAuthenticationState();
|
||||||
builder.Services.AddLocalStorageServices();
|
builder.Services.AddLocalStorageServices();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
builder.Services.AddScoped<IAuthenticationClientService, AuthenticationClientService>();
|
builder.Services.AddScoped<IAuthenticationClientService, AuthenticationClientService>();
|
||||||
builder.Services.AddScoped<IExamService, ExamService>();
|
builder.Services.AddScoped<IExamService, ExamService>();
|
||||||
builder.Services.AddScoped<AuthenticationStateProvider, AuthStateProvider>();
|
builder.Services.AddScoped<AuthenticationStateProvider, AuthStateProvider>();
|
||||||
@@ -40,9 +44,8 @@ builder.Services.AddScoped<IRefreshTokenService, RefreshTokenService>();
|
|||||||
builder.Services.AddScoped<IClassServices, ClasssServices>();
|
builder.Services.AddScoped<IClassServices, ClasssServices>();
|
||||||
builder.Services.AddScoped<IEmailSender, QEmailSender>();
|
builder.Services.AddScoped<IEmailSender, QEmailSender>();
|
||||||
builder.Services.AddScoped<HttpInterceptorHandlerService>();
|
builder.Services.AddScoped<HttpInterceptorHandlerService>();
|
||||||
builder.Services.AddScoped<RefreshTokenService2>();
|
|
||||||
builder.Services.AddScoped<IAIService, AiService>();
|
builder.Services.AddScoped<IAIService, AiService>();
|
||||||
|
builder.Services.AddScoped<IUserServices, UserServices>();
|
||||||
builder.Services.AddHttpClient("WebApiClient", client =>
|
builder.Services.AddHttpClient("WebApiClient", client =>
|
||||||
{
|
{
|
||||||
var baseAddress = builder.Configuration.GetSection("ApiConfiguration:BaseAddress").Value;
|
var baseAddress = builder.Configuration.GetSection("ApiConfiguration:BaseAddress").Value;
|
||||||
|
@@ -14,7 +14,7 @@
|
|||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": true,
|
"launchBrowser": true,
|
||||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||||
"applicationUrl": "https://localhost:7047;http://localhost:5190",
|
"applicationUrl": "https://localhost:7047",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
}
|
}
|
||||||
@@ -28,4 +28,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,15 +1,21 @@
|
|||||||
using Entities.DTO;
|
using Entities.Contracts;
|
||||||
|
using Entities.DTO;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
|
using TechHelper.Client.HttpRepository;
|
||||||
|
using TechHelper.Services;
|
||||||
|
|
||||||
namespace TechHelper.Client.Services
|
namespace TechHelper.Client.Services
|
||||||
{
|
{
|
||||||
public class ClasssServices : IClassServices
|
public class ClasssServices : IClassServices
|
||||||
{
|
{
|
||||||
private readonly HttpClient _client;
|
private readonly HttpClient _client;
|
||||||
|
private readonly IAuthenticationClientService _authenticationClientService;
|
||||||
|
|
||||||
public ClasssServices(HttpClient client)
|
public ClasssServices(HttpClient client, IAuthenticationClientService authenticationClientService)
|
||||||
{
|
{
|
||||||
_client = client;
|
_client = client;
|
||||||
|
_authenticationClientService = authenticationClientService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<ResponseDto> CreateClass(UserRegistrationToClassDto userClass)
|
public Task<ResponseDto> CreateClass(UserRegistrationToClassDto userClass)
|
||||||
@@ -17,6 +23,37 @@ namespace TechHelper.Client.Services
|
|||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<ApiResponse> GetClassStudents()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _client.PostAsJsonAsync("class/getClassStudents","");
|
||||||
|
var content = await result.Content.ReadAsStringAsync();
|
||||||
|
var users = JsonConvert.DeserializeObject<List<StudentDto>>(content);
|
||||||
|
return ApiResponse.Success(result: users);
|
||||||
|
}
|
||||||
|
catch(Exception ex)
|
||||||
|
{
|
||||||
|
return ApiResponse.Error($"获取失败,{ex.Message}, InnerException: {ex.InnerException}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ApiResponse> GetGradeClasses(byte grade)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _client.PostAsJsonAsync("class/GetGradeClasses", grade);
|
||||||
|
var content = await result.Content.ReadAsStringAsync();
|
||||||
|
Console.WriteLine($"服务器返回的原始内容是: {content}");
|
||||||
|
var users = JsonConvert.DeserializeObject<List<byte>>(content);
|
||||||
|
return ApiResponse.Success(result: users);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return ApiResponse.Error($"获取失败,{ex.Message}, InnerException: {ex.InnerException}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<ResponseDto> UserRegister(UserRegistrationToClassDto userRegistrationToClassDto)
|
public async Task<ResponseDto> UserRegister(UserRegistrationToClassDto userRegistrationToClassDto)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -25,6 +62,8 @@ namespace TechHelper.Client.Services
|
|||||||
userRegistrationToClassDto);
|
userRegistrationToClassDto);
|
||||||
var data = await result.Content.ReadAsStringAsync();
|
var data = await result.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
await _authenticationClientService.RefreshTokenAsync();
|
||||||
|
|
||||||
return new ResponseDto
|
return new ResponseDto
|
||||||
{
|
{
|
||||||
IsSuccessfulRegistration = result.IsSuccessStatusCode,
|
IsSuccessfulRegistration = result.IsSuccessStatusCode,
|
||||||
|
@@ -3,19 +3,19 @@ using TechHelper.Client.AI;
|
|||||||
using TechHelper.Services;
|
using TechHelper.Services;
|
||||||
using Entities.DTO;
|
using Entities.DTO;
|
||||||
using System.Net.Http.Json; // 用于 PostAsJsonAsync
|
using System.Net.Http.Json; // 用于 PostAsJsonAsync
|
||||||
using Newtonsoft.Json; // 用于 JSON 反序列化
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace TechHelper.Client.Exam
|
namespace TechHelper.Client.Services
|
||||||
{
|
{
|
||||||
public class ExamService : IExamService
|
public class ExamService : IExamService
|
||||||
{
|
{
|
||||||
private readonly IAIService _aIService; // 遵循命名规范,字段前加下划线
|
private readonly IAIService _aIService;
|
||||||
private readonly HttpClient _client; // 直接注入 HttpClient
|
private readonly HttpClient _client;
|
||||||
|
|
||||||
public ExamService(IAIService aIService, HttpClient client) // 修正点:直接注入 HttpClient
|
public ExamService(IAIService aIService, HttpClient client)
|
||||||
{
|
{
|
||||||
_aIService = aIService;
|
_aIService = aIService;
|
||||||
_client = client; // 赋值注入的 HttpClient 实例
|
_client = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ApiResponse ConvertToXML<T>(string xmlContent)
|
public ApiResponse ConvertToXML<T>(string xmlContent)
|
||||||
@@ -86,15 +86,14 @@ namespace TechHelper.Client.Exam
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ApiResponse> GetAllExam(string user)
|
public async Task<ApiResponse> GetAllExam()
|
||||||
{
|
{
|
||||||
// 直接使用注入的 _client 实例
|
var response = await _client.GetAsync($"exam/getAllPreview");
|
||||||
var response = await _client.GetAsync($"exam/getAllPreview?user={user}");
|
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode)
|
if (response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var content = await response.Content.ReadAsStringAsync();
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
var result = JsonConvert.DeserializeObject<List<ExamDto>>(content);
|
var result = JsonConvert.DeserializeObject<List<AssignmentDto>>(content);
|
||||||
return ApiResponse.Success(result: result);
|
return ApiResponse.Success(result: result);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -105,14 +104,33 @@ namespace TechHelper.Client.Exam
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ApiResponse> GetExam(Guid guid)
|
public async Task<ApiResponse> GetAllSubmission()
|
||||||
{
|
{
|
||||||
|
try
|
||||||
var response = await _client.GetAsync($"exam/get?id={guid}");
|
{
|
||||||
|
var response = await _client.GetAsync($"exam/getAllSubmission");
|
||||||
if (response.IsSuccessStatusCode)
|
if (response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var content = await response.Content.ReadAsStringAsync();
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
var exam = JsonConvert.DeserializeObject<ExamDto>(content);
|
var exam = JsonConvert.DeserializeObject<AssignmentDto>(content);
|
||||||
|
return ApiResponse.Success();
|
||||||
|
}
|
||||||
|
return ApiResponse.Error(message: "获取失败");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return ApiResponse.Error(message: $"内部错误{ex.Message}, InerEx{ex.InnerException}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ApiResponse> GetExam(Guid guid)
|
||||||
|
{
|
||||||
|
|
||||||
|
var response = await _client.GetAsync($"exam/{guid}");
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
var exam = JsonConvert.DeserializeObject<AssignmentDto>(content);
|
||||||
return ApiResponse.Success(result: exam);
|
return ApiResponse.Success(result: exam);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -144,12 +162,12 @@ namespace TechHelper.Client.Exam
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ApiResponse> SaveParsedExam(ExamDto examDto)
|
public async Task<ApiResponse> SaveParsedExam(AssignmentDto assiDto)
|
||||||
{
|
{
|
||||||
// 直接使用注入的 _client 实例
|
// 直接使用注入的 _client 实例
|
||||||
var response = await _client.PostAsJsonAsync("exam/add", examDto);
|
var response = await _client.PostAsJsonAsync("exam/add", assiDto);
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode) // 检查是否是成功的状态码,例如 200 OK, 201 Created 等
|
if (response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
return ApiResponse.Success(message: "试题保存成功。");
|
return ApiResponse.Success(message: "试题保存成功。");
|
||||||
}
|
}
|
||||||
@@ -159,5 +177,18 @@ namespace TechHelper.Client.Exam
|
|||||||
return ApiResponse.Error(message: $"保存试题失败: {response.StatusCode} - {errorContent}");
|
return ApiResponse.Error(message: $"保存试题失败: {response.StatusCode} - {errorContent}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<ApiResponse> SubmissionAssignment(SubmissionDto submission)
|
||||||
|
{
|
||||||
|
var response = await _client.PostAsJsonAsync("exam/submission", submission);
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
return ApiResponse.Success("提交成功");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return ApiResponse.Error("提交失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -8,5 +8,7 @@ namespace TechHelper.Client.Services
|
|||||||
{
|
{
|
||||||
public Task<ResponseDto> UserRegister(UserRegistrationToClassDto userRegistrationToClassDto);
|
public Task<ResponseDto> UserRegister(UserRegistrationToClassDto userRegistrationToClassDto);
|
||||||
public Task<ResponseDto> CreateClass(UserRegistrationToClassDto userClass);
|
public Task<ResponseDto> CreateClass(UserRegistrationToClassDto userClass);
|
||||||
|
public Task<ApiResponse> GetClassStudents();
|
||||||
|
public Task<ApiResponse> GetGradeClasses(byte grade);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,17 +1,19 @@
|
|||||||
using Entities.DTO;
|
using Entities.DTO;
|
||||||
using TechHelper.Services;
|
using TechHelper.Services;
|
||||||
|
|
||||||
namespace TechHelper.Client.Exam
|
namespace TechHelper.Client.Services
|
||||||
{
|
{
|
||||||
public interface IExamService
|
public interface IExamService
|
||||||
{
|
{
|
||||||
public Task<ApiResponse> FormatExam(string examContent);
|
public Task<ApiResponse> FormatExam(string examContent);
|
||||||
public Task<ApiResponse> DividExam(string examContent);
|
public Task<ApiResponse> DividExam(string examContent);
|
||||||
public Task<ApiResponse> SaveParsedExam(ExamDto examDto);
|
public Task<ApiResponse> SaveParsedExam(AssignmentDto assiDto);
|
||||||
public Task<ApiResponse> ParseSingleQuestionGroup(string examContent);
|
public Task<ApiResponse> ParseSingleQuestionGroup(string examContent);
|
||||||
public ApiResponse ConvertToXML<T>(string xmlContent);
|
public ApiResponse ConvertToXML<T>(string xmlContent);
|
||||||
|
|
||||||
public Task<ApiResponse> GetAllExam(string user);
|
public Task<ApiResponse> GetAllExam();
|
||||||
public Task<ApiResponse> GetExam(Guid guid);
|
public Task<ApiResponse> GetExam(Guid guid);
|
||||||
|
public Task<ApiResponse> SubmissionAssignment(SubmissionDto submission);
|
||||||
|
public Task<ApiResponse> GetAllSubmission();
|
||||||
}
|
}
|
||||||
}
|
}
|
9
TechHelper.Client/Services/IUserServices.cs
Normal file
9
TechHelper.Client/Services/IUserServices.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using TechHelper.Services;
|
||||||
|
|
||||||
|
namespace TechHelper.Client.Services
|
||||||
|
{
|
||||||
|
public interface IUserServices
|
||||||
|
{
|
||||||
|
public Task<ApiResponse> RestoreUserInfo();
|
||||||
|
}
|
||||||
|
}
|
24
TechHelper.Client/Services/UserServices.cs
Normal file
24
TechHelper.Client/Services/UserServices.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using TechHelper.Services;
|
||||||
|
|
||||||
|
namespace TechHelper.Client.Services
|
||||||
|
{
|
||||||
|
public class UserServices : IUserServices
|
||||||
|
{
|
||||||
|
private readonly HttpClient _client;
|
||||||
|
|
||||||
|
public UserServices(HttpClient httpClient)
|
||||||
|
{
|
||||||
|
_client = httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ApiResponse> RestoreUserInfo()
|
||||||
|
{
|
||||||
|
var result = await _client.GetAsync("user/restoreUserRole");
|
||||||
|
if (result.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
return ApiResponse.Success();
|
||||||
|
}
|
||||||
|
return ApiResponse.Error();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -5,8 +5,6 @@
|
|||||||
<MudPaper Class="d-flex flex-row flex-grow-1 overflow-hidden" Height="100%">
|
<MudPaper Class="d-flex flex-row flex-grow-1 overflow-hidden" Height="100%">
|
||||||
|
|
||||||
<MudPaper Width="200px">
|
<MudPaper Width="200px">
|
||||||
<h1>Manage your account</h1>
|
|
||||||
<h2>Change your account settings</h2>
|
|
||||||
<MudDivider Class="flex-grow-0" />
|
<MudDivider Class="flex-grow-0" />
|
||||||
<ExamNavMenu />
|
<ExamNavMenu />
|
||||||
|
|
||||||
|
@@ -3,11 +3,8 @@
|
|||||||
|
|
||||||
|
|
||||||
<MudPaper Class="d-flex flex-column flex-grow-1">
|
<MudPaper Class="d-flex flex-column flex-grow-1">
|
||||||
|
|
||||||
<h1>Manage your account</h1>
|
|
||||||
<h2>Change your account settings</h2>
|
|
||||||
<MudDivider Class="flex-grow-0" />
|
<MudDivider Class="flex-grow-0" />
|
||||||
<MudStack Row="true">
|
<MudStack Row="true" Class="d-flex flex-grow-1">
|
||||||
<ManageNavMenu />
|
<ManageNavMenu />
|
||||||
@Body
|
@Body
|
||||||
</MudStack>
|
</MudStack>
|
||||||
|
@@ -4,6 +4,7 @@
|
|||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -22,6 +23,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
||||||
<PackageReference Include="Blazor.LocalStorage.WebAssembly" Version="8.0.0" />
|
<PackageReference Include="Blazor.LocalStorage.WebAssembly" Version="8.0.0" />
|
||||||
<PackageReference Include="Blazored.TextEditor" Version="1.1.3" />
|
<PackageReference Include="Blazored.TextEditor" Version="1.1.3" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.12" />
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.12" />
|
||||||
@@ -38,4 +40,8 @@
|
|||||||
<ProjectReference Include="..\Entities\Entities.csproj" />
|
<ProjectReference Include="..\Entities\Entities.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Pages\Exam\TemplateCard\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@@ -4,6 +4,6 @@
|
|||||||
"ClientId": "33333333-3333-3333-33333333333333333"
|
"ClientId": "33333333-3333-3333-33333333333333333"
|
||||||
},
|
},
|
||||||
"ApiConfiguration": {
|
"ApiConfiguration": {
|
||||||
"BaseAddress": "http://localhost:5099"
|
"BaseAddress": "http://localhost:8080"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
BIN
TechHelper.Client/wwwroot/ref/Border.png
Normal file
BIN
TechHelper.Client/wwwroot/ref/Border.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
BIN
TechHelper.Client/wwwroot/ref/File.png
Normal file
BIN
TechHelper.Client/wwwroot/ref/File.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.2 KiB |
BIN
TechHelper.Client/wwwroot/ref/Login.png
Normal file
BIN
TechHelper.Client/wwwroot/ref/Login.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 99 KiB |
BIN
TechHelper.Client/wwwroot/ref/UnFinish.png
Normal file
BIN
TechHelper.Client/wwwroot/ref/UnFinish.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 83 KiB |
BIN
TechHelper.Client/wwwroot/ref/Working.png
Normal file
BIN
TechHelper.Client/wwwroot/ref/Working.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 73 KiB |
@@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Identity;
|
|||||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Entities.Contracts;
|
using Entities.Contracts;
|
||||||
using TechHelper.Server.Context.Configuration;
|
|
||||||
|
|
||||||
namespace TechHelper.Context
|
namespace TechHelper.Context
|
||||||
{
|
{
|
||||||
@@ -14,7 +13,6 @@ namespace TechHelper.Context
|
|||||||
|
|
||||||
public DbSet<AssignmentClass> AssignmentClasses { get; set; }
|
public DbSet<AssignmentClass> AssignmentClasses { get; set; }
|
||||||
public DbSet<Assignment> Assignments { get; set; }
|
public DbSet<Assignment> Assignments { get; set; }
|
||||||
public DbSet<AssignmentQuestion> AssignmentGroups { get; set; }
|
|
||||||
public DbSet<AssignmentQuestion> AssignmentQuestions { get; set; }
|
public DbSet<AssignmentQuestion> AssignmentQuestions { get; set; }
|
||||||
public DbSet<Class> Classes { get; set; }
|
public DbSet<Class> Classes { get; set; }
|
||||||
public DbSet<ClassTeacher> ClassStudents { get; set; }
|
public DbSet<ClassTeacher> ClassStudents { get; set; }
|
||||||
@@ -22,6 +20,7 @@ namespace TechHelper.Context
|
|||||||
public DbSet<Question> Questions { get; set; }
|
public DbSet<Question> Questions { get; set; }
|
||||||
public DbSet<Submission> Submissions { get; set; }
|
public DbSet<Submission> Submissions { get; set; }
|
||||||
public DbSet<SubmissionDetail> SubmissionDetails { get; set; }
|
public DbSet<SubmissionDetail> SubmissionDetails { get; set; }
|
||||||
|
public DbSet<QuestionContext> QuestionContexts { get; set; }
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder builder)
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
{
|
{
|
||||||
@@ -29,14 +28,12 @@ namespace TechHelper.Context
|
|||||||
builder.ApplyConfiguration(new RoleConfiguration());
|
builder.ApplyConfiguration(new RoleConfiguration());
|
||||||
builder.ApplyConfiguration(new AssignmentConfiguration());
|
builder.ApplyConfiguration(new AssignmentConfiguration());
|
||||||
builder.ApplyConfiguration(new AssignmentClassConfiguration());
|
builder.ApplyConfiguration(new AssignmentClassConfiguration());
|
||||||
builder.ApplyConfiguration(new AssignmentGroupConfiguration());
|
|
||||||
builder.ApplyConfiguration(new AssignmentQuestionConfiguration());
|
builder.ApplyConfiguration(new AssignmentQuestionConfiguration());
|
||||||
builder.ApplyConfiguration(new ClassConfiguration());
|
builder.ApplyConfiguration(new ClassConfiguration());
|
||||||
builder.ApplyConfiguration(new ClassStudentConfiguration());
|
builder.ApplyConfiguration(new ClassStudentConfiguration());
|
||||||
builder.ApplyConfiguration(new ClassTeacherConfiguration());
|
builder.ApplyConfiguration(new ClassTeacherConfiguration());
|
||||||
builder.ApplyConfiguration(new QuestionConfiguration());
|
builder.ApplyConfiguration(new QuestionConfiguration());
|
||||||
builder.ApplyConfiguration(new SubmissionConfiguration());
|
builder.ApplyConfiguration(new SubmissionConfiguration());
|
||||||
builder.ApplyConfiguration(new QuestionGroupConfiguration());
|
|
||||||
builder.ApplyConfiguration(new SubmissionDetailConfiguration());
|
builder.ApplyConfiguration(new SubmissionDetailConfiguration());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -23,73 +23,37 @@ namespace TechHelper.Context
|
|||||||
public AutoMapperProFile()
|
public AutoMapperProFile()
|
||||||
{
|
{
|
||||||
CreateMap<UserForRegistrationDto, User>()
|
CreateMap<UserForRegistrationDto, User>()
|
||||||
.ForMember(dest => dest.Id, opt => opt.Ignore()) // ID由IdentityUser生成
|
.ForMember(dest => dest.Id, opt => opt.Ignore())
|
||||||
.ForMember(dest => dest.UserName, opt => opt.MapFrom(src => src.Name)) // 或者 MapFrom(src => src.Name)
|
.ForMember(dest => dest.UserName, opt => opt.MapFrom(src => src.Name))
|
||||||
.ForMember(dest => dest.Email, opt => opt.MapFrom(src => src.Email))
|
.ForMember(dest => dest.Email, opt => opt.MapFrom(src => src.Email))
|
||||||
.ForMember(dest => dest.PhoneNumber, opt => opt.MapFrom(src => src.PhoneNumber))
|
.ForMember(dest => dest.PhoneNumber, opt => opt.MapFrom(src => src.PhoneNumber))
|
||||||
.ForMember(dest => dest.Address, opt => opt.MapFrom(src => src.HomeAddress)) // 映射到 IdentityUser 的 Address 属性
|
.ForMember(dest => dest.Address, opt => opt.MapFrom(src => src.HomeAddress))
|
||||||
.ForMember(dest => dest.PasswordHash, opt => opt.Ignore()) // 密码哈希由 UserManager 处理
|
.ForMember(dest => dest.PasswordHash, opt => opt.Ignore())
|
||||||
.ForMember(dest => dest.EmailConfirmed, opt => opt.Ignore()); // 邮箱确认状态由服务层处理
|
.ForMember(dest => dest.EmailConfirmed, opt => opt.Ignore());
|
||||||
|
|
||||||
CreateMap<ClassDto, Class>()
|
CreateMap<ClassDto, Class>()
|
||||||
.ForMember(d => d.Number, o => o.MapFrom(src => src.Class)).ReverseMap();
|
.ForMember(d => d.Number, o => o.MapFrom(src => src.Class))
|
||||||
|
.ReverseMap();
|
||||||
CreateMap<SubQuestionDto, Question>()
|
|
||||||
.ForMember(dest => dest.Id, opt => opt.Ignore())
|
|
||||||
.ForMember(dest => dest.QuestionText, opt => opt.MapFrom(src => src.Stem))
|
|
||||||
.ForMember(dest => dest.CorrectAnswer, opt => opt.MapFrom(src => src.SampleAnswer))
|
|
||||||
.ForMember(dest => dest.QuestionType, opt => opt.MapFrom(src => EnumMappingHelpers.ParseEnumSafe(src.QuestionType, QuestionType.Unknown)))
|
|
||||||
.ForMember(dest => dest.DifficultyLevel, opt => opt.MapFrom(src => EnumMappingHelpers.ParseEnumSafe(src.DifficultyLevel, DifficultyLevel.easy)))
|
|
||||||
.ForMember(dest => dest.SubjectArea, opt => opt.MapFrom(src => EnumMappingHelpers.ParseEnumSafe(src.DifficultyLevel, SubjectAreaEnum.Unknown)))
|
|
||||||
.ForMember(dest => dest.CreatedBy, opt => opt.Ignore())
|
|
||||||
.ForMember(dest => dest.CreatedAt, opt => opt.Ignore())
|
|
||||||
.ForMember(dest => dest.UpdatedAt, opt => opt.Ignore())
|
|
||||||
.ForMember(dest => dest.IsDeleted, opt => opt.Ignore());
|
|
||||||
|
|
||||||
// 2. Question -> SubQuestionDto (查看时)
|
|
||||||
CreateMap<Question, SubQuestionDto>()
|
|
||||||
.ForMember(dest => dest.Stem, opt => opt.MapFrom(src => src.QuestionText))
|
|
||||||
.ForMember(dest => dest.Score, opt => opt.Ignore()) // Question 实体没有 Score 字段,需要从 AssignmentQuestion 获取
|
|
||||||
.ForMember(dest => dest.SampleAnswer, opt => opt.MapFrom(src => src.CorrectAnswer))
|
|
||||||
.ForMember(dest => dest.QuestionType, opt => opt.MapFrom(src => src.QuestionType.ToString()))
|
|
||||||
.ForMember(dest => dest.DifficultyLevel, opt => opt.MapFrom(src => src.DifficultyLevel.ToString()))
|
|
||||||
.ForMember(dest => dest.Options, opt => opt.Ignore()); // Options 需要单独处理
|
|
||||||
|
|
||||||
CreateMap<Assignment, ExamDto>()
|
|
||||||
.ForMember(dest => dest.AssignmentTitle, opt => opt.MapFrom(src => src.Title))
|
|
||||||
.ForMember(dest => dest.Description, opt => opt.MapFrom(src => src.Description))
|
|
||||||
.ForMember(dest => dest.AssignmentId, opt => opt.MapFrom(src => src.Id))
|
|
||||||
.ForMember(dest => dest.QuestionGroups, opt => opt.MapFrom(src =>
|
|
||||||
src.AssignmentGroups.FirstOrDefault(ag => ag.ParentGroup == null)))
|
|
||||||
.ForMember(dest => dest.SubjectArea, opt => opt.MapFrom(src => src.SubjectArea.ToString()));
|
|
||||||
|
|
||||||
CreateMap<AssignmentGroup, QuestionGroupDto>()
|
|
||||||
.ForMember(dest => dest.SubQuestionGroups, opt => opt.MapFrom(src => src.ChildAssignmentGroups))
|
|
||||||
.ForMember(dest => dest.SubQuestions, opt => opt.MapFrom(src => src.AssignmentQuestions));
|
|
||||||
|
|
||||||
CreateMap<AssignmentQuestion, SubQuestionDto>()
|
|
||||||
.ForMember(dest => dest.Stem, opt => opt.MapFrom(src => src.Question.QuestionText))
|
|
||||||
.ForMember(dest => dest.SampleAnswer, opt => opt.MapFrom(src => src.Question.CorrectAnswer))
|
|
||||||
.ForMember(dest => dest.QuestionType, opt => opt.MapFrom(src => src.Question.QuestionType.ToString()))
|
|
||||||
.ForMember(dest => dest.DifficultyLevel, opt => opt.MapFrom(src => src.Question.DifficultyLevel.ToString()));
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
CreateMap<QuestionGroupDto, AssignmentGroup>()
|
// Assignment
|
||||||
.ForMember(dest => dest.ChildAssignmentGroups, opt => opt.MapFrom(src => src.SubQuestionGroups))
|
CreateMap<AssignmentDto, Assignment>().ReverseMap();
|
||||||
.ForMember(dest => dest.AssignmentQuestions, opt => opt.MapFrom(src => src.SubQuestions));
|
|
||||||
|
|
||||||
CreateMap<SubQuestionDto, AssignmentQuestion>()
|
CreateMap<AssignmentQuestionDto, AssignmentQuestion>().ReverseMap();
|
||||||
.ForMember(dest => dest.Question, opt => opt.MapFrom(src => src)); // 映射到嵌套的 Question 对象
|
|
||||||
|
|
||||||
CreateMap<QuestionGroupDto, QuestionGroup>()
|
CreateMap<QuestionDto, Question>().ReverseMap();
|
||||||
.ForMember(dest => dest.ChildQuestionGroups, opt => opt.MapFrom(src => src.SubQuestionGroups))
|
|
||||||
.ForMember(dest => dest.Title, opt => opt.MapFrom(src => src.Title))
|
CreateMap<QuestionContext, QuestionContextDto>().ReverseMap();
|
||||||
.ForMember(dest => dest.Description, opt => opt.MapFrom(src => src.Descript));
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
CreateMap<Assignment, ExamDto>();
|
|
||||||
|
// Submission
|
||||||
|
CreateMap<SubmissionDto, Submission>().ReverseMap();
|
||||||
|
|
||||||
|
CreateMap<SubmissionDetailDto, SubmissionDetail>().ReverseMap();
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -29,10 +29,10 @@ namespace TechHelper.Context.Configuration
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnName("due_date");
|
.HasColumnName("due_date");
|
||||||
|
|
||||||
builder.Property(a => a.TotalPoints)
|
builder.Property(a => a.TotalQuestions)
|
||||||
.HasColumnName("total_points");
|
.HasColumnName("total_points");
|
||||||
|
|
||||||
builder.Property(a => a.CreatedBy)
|
builder.Property(a => a.CreatorId)
|
||||||
.HasColumnName("created_by");
|
.HasColumnName("created_by");
|
||||||
|
|
||||||
builder.Property(a => a.CreatedAt)
|
builder.Property(a => a.CreatedAt)
|
||||||
@@ -54,29 +54,16 @@ namespace TechHelper.Context.Configuration
|
|||||||
// 如果 User 有一个名为 AssignmentsCreated 的导航属性,应写为 .WithMany(u => u.AssignmentsCreated)
|
// 如果 User 有一个名为 AssignmentsCreated 的导航属性,应写为 .WithMany(u => u.AssignmentsCreated)
|
||||||
builder.HasOne(a => a.Creator)
|
builder.HasOne(a => a.Creator)
|
||||||
.WithMany() // User 实体没有指向 Assignment 的导航属性 (或我们不知道)
|
.WithMany() // User 实体没有指向 Assignment 的导航属性 (或我们不知道)
|
||||||
.HasForeignKey(a => a.CreatedBy)
|
.HasForeignKey(a => a.CreatorId)
|
||||||
.IsRequired(); // CreatedBy 是必填的,对应 [Required] 在外键属性上
|
.IsRequired(); // CreatedBy 是必填的,对应 [Required] 在外键属性上
|
||||||
|
|
||||||
// 关系: Assignment (一) 到 AssignmentClass (多)
|
|
||||||
// 假设 AssignmentClass 实体包含一个名为 AssignmentId 的外键属性
|
|
||||||
builder.HasMany(a => a.AssignmentClasses)
|
|
||||||
.WithOne(ac => ac.Assignment) // AssignmentClass 没有指向 Assignment 的导航属性 (或我们不知道)
|
|
||||||
.HasForeignKey("AssignmentId") // 指定外键名称为 AssignmentId
|
|
||||||
.OnDelete(DeleteBehavior.Cascade); // 如果 Assignment 被删除,关联的 AssignmentClass 也会被删除
|
|
||||||
|
|
||||||
// 关系: Assignment (一) 到 AssignmentAttachment (多)
|
|
||||||
// 假设 AssignmentAttachment 实体包含一个名为 AssignmentId 的外键属性
|
builder.HasOne(a=>a.ExamStruct)
|
||||||
builder.HasMany(a => a.AssignmentAttachments)
|
.WithOne()
|
||||||
.WithOne(aa => aa.Assignment)
|
.HasForeignKey<Assignment>(a=>a.ExamStructId)
|
||||||
.HasForeignKey("AssignmentId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
// 关系: Assignment (一) 到 Submission (多)
|
|
||||||
// 假设 Submission 实体包含一个名为 AssignmentId 的外键属性
|
|
||||||
builder.HasMany(a => a.Submissions)
|
|
||||||
.WithOne(s => s.Assignment)
|
|
||||||
.HasForeignKey("AssignmentId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,83 +0,0 @@
|
|||||||
using Entities.Contracts;
|
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace TechHelper.Context.Configuration
|
|
||||||
{
|
|
||||||
public class AssignmentGroupConfiguration : IEntityTypeConfiguration<AssignmentGroup>
|
|
||||||
{
|
|
||||||
public void Configure(EntityTypeBuilder<AssignmentGroup> builder)
|
|
||||||
{
|
|
||||||
// 1. 设置表名
|
|
||||||
// 将此实体映射到数据库中名为 "assignment_detail" 的表。
|
|
||||||
builder.ToTable("assignment_group");
|
|
||||||
|
|
||||||
// 2. 配置主键
|
|
||||||
// Id 属性作为主键。
|
|
||||||
builder.HasKey(ag => ag.Id);
|
|
||||||
|
|
||||||
// 3. 配置列名、必需性、长度和默认值
|
|
||||||
|
|
||||||
// 配置 Id 属性对应的数据库列名为 "id"。
|
|
||||||
builder.Property(ag => ag.Id)
|
|
||||||
.HasColumnName("id");
|
|
||||||
// EF Core 默认 Guid 类型主键由应用程序生成,因此无需 ValueGeneratedOnAdd()。
|
|
||||||
|
|
||||||
// 配置 AssignmentId 属性对应的数据库列名为 "assignment",并设置为必需字段。
|
|
||||||
builder.Property(ag => ag.AssignmentId)
|
|
||||||
.HasColumnName("assignment");
|
|
||||||
|
|
||||||
// 配置 Title 属性对应的数据库列名为 "title",设置为必需字段,并设置最大长度。
|
|
||||||
builder.Property(ag => ag.Title)
|
|
||||||
.HasColumnName("title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(65535); // 对应 MaxLength(65535)
|
|
||||||
|
|
||||||
// 配置 Descript 属性对应的数据库列名为 "descript",并设置最大长度。
|
|
||||||
builder.Property(ag => ag.Descript)
|
|
||||||
.HasColumnName("descript")
|
|
||||||
.HasMaxLength(65535); // 对应 MaxLength(65535)
|
|
||||||
|
|
||||||
// 配置 TotalPoints 属性对应的数据库列名为 "total_points"。
|
|
||||||
// TotalPoints 是 decimal? 类型,默认就是可选的,无需 IsRequired(false)。
|
|
||||||
builder.Property(ag => ag.TotalPoints)
|
|
||||||
.HasColumnName("total_points");
|
|
||||||
|
|
||||||
// 配置 Number 属性对应的数据库列名为 "number"。
|
|
||||||
builder.Property(ag => ag.Number)
|
|
||||||
.HasColumnName("number")
|
|
||||||
.IsRequired(); // byte 默认非空,显式 IsRequired 增加可读性。
|
|
||||||
|
|
||||||
// 配置 ParentGroup 属性对应的数据库列名为 "sub_group"。
|
|
||||||
// ParentGroup 是 Guid? 类型,默认就是可选的,无需 IsRequired(false)。
|
|
||||||
builder.Property(ag => ag.ParentGroup)
|
|
||||||
.HasColumnName("parent_group")
|
|
||||||
.IsRequired(false);
|
|
||||||
|
|
||||||
// 配置 IsDeleted 属性对应的数据库列名为 "deleted",并设置默认值为 false。
|
|
||||||
builder.Property(ag => ag.IsDeleted)
|
|
||||||
.HasColumnName("deleted")
|
|
||||||
.HasDefaultValue(false); // 适用于软删除策略
|
|
||||||
|
|
||||||
// 4. 配置导航属性和外键关系
|
|
||||||
|
|
||||||
// 配置 AssignmentGroup 到 Assignment 的多对一关系。
|
|
||||||
// 一个 AssignmentGroup 记录属于一个 Assignment。
|
|
||||||
builder.HasOne(ag => ag.Assignment) // 当前 AssignmentGroup 有一个 Assignment
|
|
||||||
.WithMany(a => a.AssignmentGroups) // 该 Assignment 可以有多个 AssignmentGroup 记录
|
|
||||||
.HasForeignKey(ag => ag.AssignmentId) // 通过 AssignmentId 建立外键
|
|
||||||
.OnDelete(DeleteBehavior.Cascade); // 当关联的 Assignment 被删除时,其所有相关的 AssignmentGroup 记录也级联删除。
|
|
||||||
|
|
||||||
// 配置 AssignmentGroup 到 AssignmentGroup 的自引用关系(父子关系)。
|
|
||||||
// 一个 AssignmentGroup 可以有一个父 AssignmentGroup (SubAssignmentGroup)。
|
|
||||||
// 假设父 AssignmentGroup 实体中有一个名为 ChildAssignmentGroups 的集合属性来表示它所包含的所有子组。
|
|
||||||
builder.HasOne(ag => ag.ParentAssignmentGroup) // 当前 AssignmentGroup 有一个父 AssignmentGroup
|
|
||||||
.WithMany(parentAg => parentAg.ChildAssignmentGroups) // 该父 AssignmentGroup 可以有多个子 AssignmentGroup
|
|
||||||
.HasForeignKey(ag => ag.ParentGroup) // 通过 SubGroup 建立外键
|
|
||||||
.IsRequired(false) // SubGroup 是可空的 (Guid?),所以这个关系是可选的。
|
|
||||||
.OnDelete(DeleteBehavior.SetNull); // 当父 AssignmentGroup 被删除时,其子 AssignmentGroup 的 SubGroup 外键将被设置为 NULL。
|
|
||||||
// 如果你希望父组被删除时子组不能脱离父组(即不允许父组被删除),
|
|
||||||
// 可以使用 DeleteBehavior.Restrict 或 DeleteBehavior.NoAction。
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -25,12 +25,8 @@ namespace TechHelper.Context.Configuration
|
|||||||
.HasColumnName("question_id");
|
.HasColumnName("question_id");
|
||||||
|
|
||||||
|
|
||||||
builder.Property(aq => aq.QuestionGroupId)
|
|
||||||
.HasColumnName("question_group_id");
|
|
||||||
|
|
||||||
|
|
||||||
// 配置 QuestionNumber 列
|
// 配置 QuestionNumber 列
|
||||||
builder.Property(aq => aq.QuestionNumber)
|
builder.Property(aq => aq.Index)
|
||||||
.HasColumnName("question_number")
|
.HasColumnName("question_number")
|
||||||
.IsRequired(); // uint 类型默认非空
|
.IsRequired(); // uint 类型默认非空
|
||||||
|
|
||||||
@@ -42,62 +38,28 @@ namespace TechHelper.Context.Configuration
|
|||||||
builder.Property(aq => aq.Score)
|
builder.Property(aq => aq.Score)
|
||||||
.HasColumnName("score");
|
.HasColumnName("score");
|
||||||
|
|
||||||
// 配置 AssignmentGroupId 列
|
|
||||||
// 该列在数据库中名为 "detail_id"
|
|
||||||
builder.Property(aq => aq.AssignmentGroupId)
|
|
||||||
.HasColumnName("group_id")
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
|
|
||||||
builder.Property(aq => aq.IsGroup)
|
|
||||||
.HasColumnName("is_group") // 修正为一致的列名
|
|
||||||
.IsRequired(); // IsGroup 应该是必需的
|
|
||||||
// 配置 IsDeleted 列
|
|
||||||
builder.Property(aq => aq.IsDeleted)
|
builder.Property(aq => aq.IsDeleted)
|
||||||
.HasColumnName("deleted")
|
.HasColumnName("deleted")
|
||||||
.HasDefaultValue(false); // 适用于软删除策略
|
.HasDefaultValue(false); // 适用于软删除策略
|
||||||
|
|
||||||
// 4. 配置导航属性和外键关系
|
|
||||||
|
|
||||||
// ---
|
builder.HasOne(aq => aq.Question)
|
||||||
// 配置 AssignmentQuestion 到 Question 的关系 (多对一)
|
.WithMany(q => q.AssignmentQuestions)
|
||||||
// 一个 AssignmentQuestion 属于一个 Question。
|
.HasForeignKey(aq => aq.QuestionId)
|
||||||
//
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
// 假设 `Question` 实体中有一个名为 `AssignmentQuestions` 的 `ICollection<AssignmentQuestion>` 集合属性。
|
|
||||||
builder.HasOne(aq => aq.Question) // 当前 AssignmentQuestion 有一个 Question
|
|
||||||
.WithMany(q => q.AssignmentQuestions) // 那个 Question 可以有多个 AssignmentQuestion
|
|
||||||
.HasForeignKey(aq => aq.QuestionId) // 外键是 AssignmentQuestion.QuestionId
|
|
||||||
.OnDelete(DeleteBehavior.Cascade); // 当 Question 被删除时,相关的 AssignmentQuestion 也级联删除。
|
|
||||||
|
|
||||||
builder.HasOne(aq => aq.QuestionGroup)
|
builder.HasOne(aq => aq.ParentAssignmentQuestion)
|
||||||
.WithMany(qg => qg.AssignmentQuestions)
|
.WithMany(aq => aq.ChildrenAssignmentQuestion)
|
||||||
.HasForeignKey(aq => aq.QuestionGroupId)
|
.HasForeignKey(aq => aq.ParentAssignmentQuestionId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
builder.HasOne(aq => aq.QuestionContext)
|
||||||
|
.WithMany(qc => qc.Questions)
|
||||||
|
.HasForeignKey(aq => aq.QuestionContextId)
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
|
||||||
// ---
|
|
||||||
// 配置 AssignmentQuestion 到 AssignmentGroup 的关系 (多对一)
|
|
||||||
// 一个 AssignmentQuestion 属于一个 AssignmentGroup。
|
|
||||||
//
|
|
||||||
// 你的 `AssignmentQuestion` 类现在有了 `public AssignmentGroup AssignmentGroup { get; set; }`
|
|
||||||
// 这是一个非常好的改进,它与 `AssignmentGroupId` 外键完美匹配。
|
|
||||||
// 假设 `AssignmentGroup` 实体中有一个名为 `AssignmentQuestions` 的 `ICollection<AssignmentQuestion>` 集合属性。
|
|
||||||
builder.HasOne(aq => aq.AssignmentGroup) // 当前 AssignmentQuestion 有一个 AssignmentGroup
|
|
||||||
.WithMany(ag => ag.AssignmentQuestions) // 那个 AssignmentGroup 可以有多个 AssignmentQuestion
|
|
||||||
.HasForeignKey(aq => aq.AssignmentGroupId) // 外键是 AssignmentQuestion.AssignmentGroupId (列名 detail_id)
|
|
||||||
.OnDelete(DeleteBehavior.Cascade); // 当 AssignmentGroup 被删除时,相关的 AssignmentQuestion 也级联删除。
|
|
||||||
|
|
||||||
// ---
|
|
||||||
// 配置 AssignmentQuestion 到 SubmissionDetail 的关系 (一对多)
|
|
||||||
// 一个 AssignmentQuestion 可以有多个 SubmissionDetail。
|
|
||||||
//
|
|
||||||
// 这个关系通常从 "多" 的一方(`SubmissionDetail` 实体)来配置外键。
|
|
||||||
// 假设 `SubmissionDetail` 实体有一个 `AssignmentQuestionId` 外键和 `AssignmentQuestion` 导航属性。
|
|
||||||
builder.HasMany(aq => aq.SubmissionDetails) // 当前 AssignmentQuestion 有多个 SubmissionDetail
|
|
||||||
.WithOne(sd => sd.AssignmentQuestion); // 每一个 SubmissionDetail 都有一个 AssignmentQuestion
|
|
||||||
// .HasForeignKey(sd => sd.AssignmentQuestionId); // 外键的配置应在 `SubmissionDetailConfiguration` 中进行
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user