Compare commits

..

10 Commits

Author SHA1 Message Date
SpecialX
c59762a392 UI
Some checks failed
Tech / explore-gitea-actions (push) Has been cancelled
2025-08-31 11:29:26 +08:00
SpecialX
017cc2169c temp 2025-07-01 19:05:07 +08:00
SpecialX
a21ca80782 1 2025-06-27 19:03:10 +08:00
SpecialX
14fbe6397a CleanExamDto 2025-06-25 17:25:13 +08:00
SpecialX
262e7d6396 FixAuth 2025-06-25 17:21:29 +08:00
SpecialX
f9ff57ff72 Finishddd 2025-06-24 19:05:13 +08:00
SpecialX
0ee411bf50 assigonmentDto 2025-06-24 11:37:12 +08:00
SpecialX
681c0862b6 AsiignmentStruct 2025-06-20 18:58:11 +08:00
SpecialX
d20c051c51 struct&&assiQues 2025-06-20 15:37:39 +08:00
SpecialX
f37262d72e 我要重新整理结构,这是这阶段的保存 2025-06-16 10:48:40 +08:00
158 changed files with 14061 additions and 4231 deletions

8
EmailLib/Dockerfile Normal file
View 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

View 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, // 草稿
}
}

View File

@@ -5,6 +5,7 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Entities.DTO;
namespace Entities.Contracts
{
@@ -24,32 +25,46 @@ namespace Entities.Contracts
public string Description { get; set; }
[Column("subject_area")]
public string SubjectArea { get; set; }
public SubjectAreaEnum SubjectArea { get; set; }
[Required]
[Column("exam_struct_id")]
public Guid ExamStructId { get; set; }
[Required]
[Column("due_date")]
public DateTime DueDate { get; set; }
[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")]
[ForeignKey("Creator")]
public Guid CreatedBy { get; set; }
public Guid CreatorId { get; set; }
[Column("created_at")]
public DateTime CreatedAt { get; set; }
[Column("updated_at")]
public DateTime UpdatedAt { get; set; }
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
[Column("deleted")]
public bool IsDeleted { get; set; }
public bool IsDeleted { get; set; } = false;
// Navigation Properties
[ForeignKey(nameof(CreatorId))]
public User Creator { 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<Submission> Submissions { get; set; }
@@ -58,9 +73,48 @@ namespace Entities.Contracts
Id = Guid.NewGuid();
Submissions = new HashSet<Submission>();
AssignmentGroups = new HashSet<AssignmentGroup>();
AssignmentClasses = new HashSet<AssignmentClass>();
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);
});
}
}
}

View File

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

View File

@@ -6,6 +6,7 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Entities.DTO;
namespace Entities.Contracts
{
@@ -17,22 +18,27 @@ namespace Entities.Contracts
public Guid Id { get; set; }
[Column("question_id")]
public Guid? QuestionId { get; set; } // 设为可空
public Guid? QuestionId { get; set; }
// 当 IsGroup 为 true 时,此为 QuestionGroup 的外键
[Column("question_group_id")] // 新增一个外键列
public Guid? QuestionGroupId { get; set; } // 设为可空
[Required]
[Column("group_id")]
[ForeignKey("AssignmentGroup")]
public Guid AssignmentGroupId { get; set; }
[Column("title")]
[MaxLength(1024)]
public string? Title { get; set; }
[Column("description")]
public Guid? QuestionContextId { get; set; }
[Required]
[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")]
public DateTime CreatedAt { get; set; }
@@ -40,18 +46,22 @@ namespace Entities.Contracts
[Column("score")]
public float? Score { get; set; }
[Required]
[Column("bgroup")]
public bool IsGroup { get; set; }
[Column("deleted")]
public bool IsDeleted { get; set; }
public Question Question { get; set; }
public QuestionGroup QuestionGroup { get; set; }
public Question? Question { get; set; }
public Assignment? Assignment { get; set; }
[ForeignKey(nameof(QuestionContextId))]
public QuestionContext? QuestionContext { 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()
{

View File

@@ -14,14 +14,16 @@ namespace Entities.Contracts
[Key]
[Column("class_id")]
public Guid ClassId { get; set; }
[ForeignKey(nameof(ClassId))]
public Class Class { get; set; }
[Key]
[Column("teacher_id")]
public Guid TeacherId { get; set; }
[ForeignKey(nameof(TeacherId))]
public User Teacher { get; set; }
[Column("subject_taught")]
public string SubjectTaught { get; set; }
public SubjectAreaEnum SubjectTaught { get; set; }
}
}

View File

@@ -16,36 +16,42 @@ namespace Entities.Contracts
public Guid Id { get; set; }
[Required]
[Column("question_text")]
[Column("title")]
[MaxLength(65535)]
public string QuestionText { get; set; }
public string Title { get; set; }
[Column("answer")]
[MaxLength(65535)]
public string? Answer { get; set; }
[Required]
[Column("question_type")]
[Column("type")]
[MaxLength(20)]
public QuestionType QuestionType { get; set; }
[Column("correct_answer")]
[MaxLength(65535)]
public string CorrectAnswer { get; set; }
[Column("question_group_id")]
public Guid? QuestionGroupId { get; set; }
public QuestionType Type { get; set; } = QuestionType.Unknown;
[Column("difficulty_level")]
[MaxLength(10)]
public DifficultyLevel DifficultyLevel { get; set; }
public DifficultyLevel DifficultyLevel { get; set; } = DifficultyLevel.easy;
[Column("subject_area")]
public SubjectAreaEnum SubjectArea { get; set; }
public SubjectAreaEnum SubjectArea { get; set; } = SubjectAreaEnum.Unknown;
[Column("options")]
public string? Options { get; set; }
[Column("key_point")]
public Guid? KeyPointId { get; set; }
[Column("lesson")]
public Guid? LessonId { get; set; }
[Required]
[Column("created_by")]
[ForeignKey("Creator")]
public Guid CreatedBy { get; set; }
public Guid CreatorId { get; set; }
[Column("created_at")]
public DateTime CreatedAt { get; set; }
@@ -56,13 +62,17 @@ namespace Entities.Contracts
[Column("deleted")]
public bool IsDeleted { get; set; }
[Column("valid_question")]
public bool ValidQuestion { get; set; }
// Navigation Properties
[ForeignKey(nameof(CreatorId))]
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()
{
@@ -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, // 计算机科学
// ... 你可以根据需要添加更多科目
}
}

View 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>();
}
}
}

View File

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

View File

@@ -28,7 +28,7 @@ namespace Entities.Contracts
[Required]
[Column("attempt_number")]
public Guid AttemptNumber { get; set; }
public byte AttemptNumber { get; set; }
[Column("submission_time")]
public DateTime SubmissionTime { get; set; }
@@ -37,11 +37,11 @@ namespace Entities.Contracts
public float? OverallGrade { get; set; }
[Column("overall_feedback")]
public string OverallFeedback { get; set; }
public string? OverallFeedback { get; set; }
[Column("graded_by")]
[ForeignKey("Grader")]
public Guid? GradedBy { get; set; }
public Guid? GraderId { get; set; }
[Column("graded_at")]
public DateTime? GradedAt { get; set; }
@@ -49,6 +49,12 @@ namespace Entities.Contracts
[Column("deleted")]
public bool IsDeleted { get; set; }
public byte TotalQuesNum { get; set; }
public byte ErrorQuesNum { get; set; }
public byte TotalScore { get; set; }
[Required]
[Column("status")]
public SubmissionStatus Status { get; set; }
@@ -66,14 +72,4 @@ namespace Entities.Contracts
}
}
public enum SubmissionStatus
{
Pending, // 待提交/未开始
Submitted, // 已提交
Graded, // 已批改
Resubmission, // 待重新提交 (如果允许)
Late, // 迟交
Draft, // 草稿
// ... 添加你需要的其他状态
}
}

View File

@@ -23,7 +23,6 @@ namespace Entities.Contracts
[Required]
[Column("student_id")]
[ForeignKey("User")]
public Guid StudentId { get; set; }
[Required]
@@ -32,16 +31,16 @@ namespace Entities.Contracts
public Guid AssignmentQuestionId { get; set; }
[Column("student_answer")]
public string StudentAnswer { get; set; }
public string? StudentAnswer { get; set; }
[Column("is_correct")]
public bool? IsCorrect { get; set; }
[Column("points_awarded")]
public float? PointsAwarded { get; set; }
public float? PointsAwarded { get; set; } // score
[Column("teacher_feedback")]
public string TeacherFeedback { get; set; }
public string? TeacherFeedback { get; set; }
[Column("created_at")]
public DateTime CreatedAt { get; set; }
@@ -52,8 +51,19 @@ namespace Entities.Contracts
[Column("deleted")]
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 User User { get; set; }
[ForeignKey(nameof(AssignmentQuestionId))]
public AssignmentQuestion AssignmentQuestion { get; set; }
public SubmissionDetail()

View 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>();
}
}
}

View 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>();
}
}
}

View 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();
}
}
}

View 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>();
}
}
}

View File

@@ -21,7 +21,9 @@ namespace Entities.Contracts
[Column("deleted")]
public bool IsDeleted { get; set; }
[InverseProperty(nameof(ClassTeacher.Teacher))]
public ICollection<ClassTeacher> TaughtClassesLink { get; set; }
[InverseProperty(nameof(ClassStudent.Student))]
public ICollection<ClassStudent> EnrolledClassesLink { get; set; }
public ICollection<Question> CreatedQuestions { get; set; }

View 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>();
}
}

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

View 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();
}
}

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

View File

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

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

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

View 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>();
}
}

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

View 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>();
}
}

View 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
View 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
View 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

View File

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

View 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>();
}
}
}

View 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

View 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,
});
}
}
}
}

View File

@@ -5,245 +5,4 @@ using System.IO; // 用于 XML 反序列化
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;
// }
// }
// }
//}
}

View File

@@ -1,233 +1,36 @@
using Entities.DTO;
using System.Text.Json.Serialization;
using System.Text.Json;
using Entities.Contracts;
using Microsoft.Extensions.Options;
using AutoMapper;
namespace TechHelper.Client.Exam
{
public static class ExamPaperExtensions
public static class AssignmentExtensions
{
public static ExamDto ConvertToExamDTO(this ExamPaper examPaper)
public static List<string> ParseOptionsFromText(this string optionsText)
{
ExamDto dto = new ExamDto();
dto.AssignmentTitle = examPaper.AssignmentTitle;
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();
ParseMajorQuestionGroup(qg, qgd, false);
dto.QuestionGroups.SubQuestionGroups.Add(qgd);
}
foreach (var question in examPaper.TopLevelQuestions)
{
if (question.SubQuestions != null && question.SubQuestions.Any())
{
var qgDto = new QuestionGroupDto
{
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;
return optionsText.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None)
.Where(line => !string.IsNullOrWhiteSpace(line)).ToList();
}
private static void ParseMajorQuestionGroup(MajorQuestionGroup qg, QuestionGroupDto qgd, bool isParentGroupValidChain)
public static void SeqIndex(this AssignmentDto dto)
{
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 });
});
}
dto.ExamStruct.SeqQGroupIndex();
}
public static void SeqIndex(this ExamDto dto)
public static void SeqQGroupIndex(this AssignmentQuestionDto dto)
{
dto.QuestionGroups.SeqQGroupIndex();
}
public static void SeqQGroupIndex(this QuestionGroupDto dto)
{
dto.SubQuestions?.ForEach(sq =>
foreach (var sqg in dto.ChildrenAssignmentQuestion)
{
sq.Index = (byte)dto.SubQuestions.IndexOf(sq);
});
dto.SubQuestionGroups?.ForEach(sqg =>
{
sqg.Index = (byte)dto.SubQuestionGroups.IndexOf(sqg);
sqg.Index = (byte)(dto.ChildrenAssignmentQuestion.IndexOf(sqg) + 1);
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);
}
}
}

View File

@@ -1,19 +1,25 @@
using Entities.DTO;
using System;
using System.Collections.Generic;
using System.Linq;
using Entities.Contracts; // 假设这些实体合约仍然是必需的
using System.Text.RegularExpressions;
using System.Text;
namespace TechHelper.Client.Exam
{
// --- 新增错误处理相关类 ---
public enum ParseErrorType
{
Validation = 1,
DataParsing = 2,
Structural = 3,
RegexMatchIssue = 4,
UnexpectedError = 5
}
public class ParseError
{
public ParseErrorType Type { get; }
public string Message { get; }
public int? Index { get; } // 错误发生的文本索引或匹配项索引
public string MatchedText { get; } // 如果与某个匹配项相关,记录其文本
public Exception InnerException { get; } // 捕获到的原始异常
public int? Index { get; }
public string MatchedText { get; }
public Exception InnerException { get; }
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()
{
var sb = new System.Text.StringBuilder();
var sb = new StringBuilder();
sb.Append($"[{Type}] {Message}");
if (Index.HasValue) sb.Append($" (Index: {Index.Value})");
if (!string.IsNullOrEmpty(MatchedText)) sb.Append($" (MatchedText: '{MatchedText}')");
@@ -35,46 +41,34 @@ namespace TechHelper.Client.Exam
}
}
public enum ParseErrorType
public class AssignmentEx
{
Validation = 1, // 输入验证失败
DataParsing = 2, // 数据解析失败(如数字转换)
Structural = 3, // 结构性问题(如选项没有对应的问题)
RegexMatchIssue = 4, // 正则表达式匹配结果不符合预期
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 string Title { get; set; } = "Title";
public string Description { get; set; } = "Description";
public SubjectAreaEnum SubjectArea { get; set; } = SubjectAreaEnum.Unknown;
public AssignmentQuestionEx ExamStruct { get; set; } = new AssignmentQuestionEx();
public List<ParseError> Errors { get; set; } = new List<ParseError>();
}
public class MajorQuestionGroup
// 试题的包裹器, 或者 单独作为一个试题结构存在
public class AssignmentQuestionEx
{
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 List<MajorQuestionGroup> SubQuestionGroups { get; set; } = new List<MajorQuestionGroup>();
public List<Question> SubQuestions { get; set; } = new List<Question>();
public string Sequence { get; set; } = string.Empty;
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 class Question
public class QuestionEx
{
public string Number { get; set; } = string.Empty;
public string Stem { get; set; } = string.Empty;
public float Score { get; set; }
public string Title { get; set; } = string.Empty;
public string Answer { get; set; } = string.Empty;
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
@@ -89,155 +83,116 @@ namespace TechHelper.Client.Exam
/// </summary>
public class RegexPatternConfig
{
public string Pattern { get; set; } // 正则表达式字符串
public int Priority { get; set; } // 优先级,数字越小优先级越高
public Regex Regex { get; private set; } // 编译后的Regex对象用于性能优化
public string Pattern { get; set; }
public int Priority { get; set; }
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;
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>
public class ExamParserConfig
{
public List<RegexPatternConfig> MajorQuestionGroupPatterns { get; set; } = new List<RegexPatternConfig>();
public List<RegexPatternConfig> QuestionPatterns { get; set; } = new List<RegexPatternConfig>();
public List<RegexPatternConfig> OptionPatterns { get; set; } = new List<RegexPatternConfig>();
public Regex ScoreRegex { get; private set; } // 独立的得分正则表达式
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) 这是一个子题目"
QuestionPatterns.Add(new RegexPatternConfig(@"^\((\d+)\)\s*(.+?)(?:\s*\(((\d+(?:\.\d+)?))\s*分\))?\s*$", 2));
// 模式 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)); // 小写字母选项
// 选项模式 (保持不变,使用 AssignmentStructType.Option 区分)
OptionPatterns.Add(new RegexPatternConfig(@"([A-Z]\.)\s*(.*?)(?=[A-Z]\.|$)", 1, AssignmentStructType.Option));
OptionPatterns.Add(new RegexPatternConfig(@"([a-z]\.)\s*(.*?)(?=[a-z]\.|$)", 2, AssignmentStructType.Option));
// 独立的得分正则表达式:匹配行末尾的 "(X分)" 格式
// Group 1: 捕获分数(如 "10" 或 "0.5"
ScoreRegex = new Regex(@"(?:\s*\(((\d+(?:\.\d+)?))\s*分\)\s*$)", RegexOptions.Multiline | RegexOptions.Compiled);
}
}
public class PotentialMatch
{
public int StartIndex { get; set; }
public int EndIndex { get; set; } // 匹配到的结构在原始文本中的结束位置
public string MatchedText { get; set; } // 匹配到的完整行或段落
public Match RegexMatch { get; set; } // 原始的Regex.Match对象方便获取捕获组
public RegexPatternConfig PatternConfig { get; set; } // 匹配到的模式配置
public MatchType Type { get; set; } // 枚举MajorQuestionGroup, Question, Option, etc.
public int EndIndex { get; set; }
public string MatchedText { get; set; }
public Match RegexMatch { get; set; }
public RegexPatternConfig PatternConfig { get; set; }
}
public enum MatchType
{
MajorQuestionGroup,
Question,
Option,
Other // 如果有其他需要识别的类型
}
/// <summary>
/// 负责扫描原始文本,收集所有潜在的匹配项(题组、题目、选项)。
/// 它只进行匹配,不进行结构化归属。
/// </summary>
public class ExamDocumentScanner
{
private readonly ExamParserConfig _config;
public ExamDocumentScanner(ExamParserConfig config)
{
_config = config ?? throw new ArgumentNullException(nameof(config)); // 确保配置不为空
_config = config ?? throw new ArgumentNullException(nameof(config));
}
/// <summary>
/// 扫描给定的文本,返回所有潜在的匹配项,并按起始位置排序。
/// </summary>
/// <param name="text">要扫描的文本</param>
/// <returns>所有匹配到的 PotentialMatch 列表</returns>
public List<PotentialMatch> Scan(string text)
public List<PotentialMatch> Scan(string text, List<ParseError> errors)
{
if (string.IsNullOrEmpty(text))
{
return new List<PotentialMatch>(); // 对于空文本,直接返回空列表
return 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 _config.MajorQuestionGroupPatterns)
foreach (var patternConfig in allPatternConfigs)
{
foreach (Match match in patternConfig.Regex.Matches(text))
try
{
allPotentialMatches.Add(new PotentialMatch
foreach (Match match in patternConfig.Regex.Matches(text))
{
StartIndex = match.Index,
EndIndex = match.Index + match.Length,
MatchedText = match.Value,
RegexMatch = match,
PatternConfig = patternConfig,
Type = MatchType.MajorQuestionGroup
});
allPotentialMatches.Add(new PotentialMatch
{
StartIndex = match.Index,
EndIndex = match.Index + match.Length,
MatchedText = match.Value,
RegexMatch = match,
PatternConfig = patternConfig,
});
}
}
catch (Exception ex)
{
errors.Add(new ParseError(ParseErrorType.UnexpectedError,
$"An error occurred during regex matching for pattern: '{patternConfig.Pattern}'.",
innerException: ex));
}
}
// 扫描所有题目模式
foreach (var patternConfig in _config.QuestionPatterns)
{
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.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();
}
}
@@ -251,18 +206,8 @@ namespace TechHelper.Client.Exam
_config = config ?? throw new ArgumentNullException(nameof(config), "ExamParserConfig cannot be null.");
}
/// <summary>
/// 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)
public AssignmentEx BuildExam(string fullExamText, List<PotentialMatch> allPotentialMatches)
{
// 核心输入验证仍然是必要的,因为这些错误是无法恢复的
if (string.IsNullOrWhiteSpace(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.");
}
var examPaper = new ExamPaper(); // ExamPaper 现在包含一个 Errors 列表
// 尝试获取试卷标题
var assignment = new AssignmentEx();
try
{
examPaper.AssignmentTitle = GetExamTitle(fullExamText);
assignment.Title = GetExamTitle(fullExamText);
}
catch (Exception ex)
{
// 如果获取标题失败,记录错误而不是抛出致命异常
examPaper.Errors.Add(new ParseError(ParseErrorType.UnexpectedError, "Failed to extract exam title.", innerException: ex));
examPaper.AssignmentTitle = "未识别试卷标题"; // 提供默认值
assignment.Errors.Add(new ParseError(ParseErrorType.UnexpectedError, "Failed to extract exam title.", innerException: ex));
assignment.Title = "未识别试卷标题";
}
var majorQGStack = new Stack<MajorQuestionGroup>();
MajorQuestionGroup currentMajorQG = null;
var questionStack = new Stack<Question>();
Question currentQuestion = null;
var assignmentQuestionStack = new Stack<AssignmentQuestionEx>();
var rootAssignmentQuestion = new AssignmentQuestionEx { Type = AssignmentStructType.Struct, Priority = 0, Title = "Root Exam Structure" };
assignmentQuestionStack.Push(rootAssignmentQuestion);
assignment.ExamStruct = rootAssignmentQuestion;
int currentContentStart = 0;
// 处理试卷开头的描述性文本
if (allPotentialMatches.Any() && allPotentialMatches[0].StartIndex > 0)
{
string introText = fullExamText.Substring(0, allPotentialMatches[0].StartIndex).Trim();
if (!string.IsNullOrWhiteSpace(introText))
{
examPaper.Description += (string.IsNullOrWhiteSpace(examPaper.Description) ? "" : "\n") + introText;
assignment.Description += (string.IsNullOrWhiteSpace(assignment.Description) ? "" : "\n") + introText;
}
}
currentContentStart = allPotentialMatches[0].StartIndex;
currentContentStart = allPotentialMatches.Any() ? allPotentialMatches[0].StartIndex : 0;
for (int i = 0; i < allPotentialMatches.Count; i++)
{
var pm = allPotentialMatches[i];
try
{
// **数据验证:不再抛出,而是记录错误**
if (pm.StartIndex < currentContentStart || pm.EndIndex > fullExamText.Length || pm.StartIndex > pm.EndIndex)
if (!IsValidPotentialMatch(pm, i, fullExamText.Length, currentContentStart, assignment.Errors))
{
examPaper.Errors.Add(new ParseError(ParseErrorType.Validation,
$"PotentialMatch at index {i} has invalid start/end indices. Start: {pm.StartIndex}, End: {pm.EndIndex}, CurrentContentStart: {currentContentStart}, FullTextLength: {fullExamText.Length}",
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; // 跳过当前循环迭代,处理下一个匹配项
currentContentStart = Math.Max(currentContentStart, pm.EndIndex);
continue;
}
string precedingText = fullExamText.Substring(currentContentStart, pm.StartIndex - currentContentStart).Trim();
if (!string.IsNullOrWhiteSpace(precedingText))
{
if (currentQuestion != null)
if (assignmentQuestionStack.Peek().Question != null)
{
// 将 examPaper.Errors 传递给 ProcessQuestionContent 收集错误
ProcessQuestionContent(currentQuestion, precedingText,
GetSubMatchesForRange(allPotentialMatches, currentContentStart, pm.StartIndex, examPaper.Errors),
examPaper.Errors);
}
else if (currentMajorQG != null)
{
currentMajorQG.Descript += (string.IsNullOrWhiteSpace(currentMajorQG.Descript) ? "" : "\n") + precedingText;
ProcessQuestionContent(assignmentQuestionStack.Peek(), precedingText, assignment.Errors);
}
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 的处理
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
{
examPaper.QuestionGroups.Add(newMajorQG);
}
currentContentStart = pm.EndIndex;
majorQGStack.Push(newMajorQG);
currentMajorQG = newMajorQG;
questionStack.Clear();
currentQuestion = null;
}
catch (Exception ex)
{
examPaper.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;
}
HandleOptionMatch(pm, i, assignmentQuestionStack.Peek(), assignment.Errors);
}
else if (pm.Type == MatchType.Question)
else
{
// 对 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`,因为即使出错也可能只是该选项的问题,不影响后续处理
}
HandleQuestionGroupMatch(pm, i, assignmentQuestionStack, assignment.Errors);
}
currentContentStart = pm.EndIndex; // 更新当前内容起点
currentContentStart = pm.EndIndex;
}
catch (Exception ex)
{
// 捕获任何在处理单个 PotentialMatch 过程中未被更具体 catch 块捕获的意外错误
examPaper.Errors.Add(new ParseError(ParseErrorType.UnexpectedError,
assignment.Errors.Add(new ParseError(ParseErrorType.UnexpectedError,
$"An unexpected error occurred during main loop processing of PotentialMatch at index {i}.",
index: i, matchedText: pm.MatchedText, innerException: ex));
currentContentStart = Math.Max(currentContentStart, pm.EndIndex); // 尝试跳过当前匹配项,继续下一项
// 这里不 `continue` 是因为外层循环会推进 `i`,但确保 `currentContentStart` 更新以避免无限循环
currentContentStart = Math.Max(currentContentStart, pm.EndIndex);
}
}
// --- 处理所有匹配项之后的剩余内容 ---
if (currentContentStart < fullExamText.Length)
{
try
@@ -542,169 +297,183 @@ namespace TechHelper.Client.Exam
string remainingText = fullExamText.Substring(currentContentStart).Trim();
if (!string.IsNullOrWhiteSpace(remainingText))
{
if (currentQuestion != null)
if (assignmentQuestionStack.Peek().Question != null)
{
ProcessQuestionContent(currentQuestion, remainingText,
GetSubMatchesForRange(allPotentialMatches, currentContentStart, fullExamText.Length, examPaper.Errors),
examPaper.Errors);
}
else if (currentMajorQG != null)
{
currentMajorQG.Descript += (string.IsNullOrWhiteSpace(currentMajorQG.Descript) ? "" : "\n") + remainingText;
ProcessQuestionContent(assignmentQuestionStack.Peek(), remainingText, assignment.Errors);
}
else
{
examPaper.Description += (string.IsNullOrWhiteSpace(examPaper.Description) ? "" : "\n") + remainingText;
assignment.Description += (string.IsNullOrWhiteSpace(assignment.Description) ? "" : "\n") + remainingText;
}
}
}
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.",
innerException: ex));
}
}
return examPaper;
return assignment;
}
private bool IsValidPotentialMatch(PotentialMatch pm, int index, int fullTextLength, int currentContentStart, List<ParseError> errors)
{
if (pm.StartIndex < currentContentStart || pm.EndIndex > fullTextLength || pm.StartIndex > pm.EndIndex)
{
errors.Add(new ParseError(ParseErrorType.Validation,
$"PotentialMatch at index {index} has invalid start/end indices. Start: {pm.StartIndex}, End: {pm.EndIndex}, CurrentContentStart: {currentContentStart}, FullTextLength: {fullTextLength}",
index: index, matchedText: pm.MatchedText));
return false;
}
if (pm.RegexMatch == null || pm.PatternConfig == null)
{
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
{
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)
{
errors.Add(new ParseError(ParseErrorType.UnexpectedError,
$"An unexpected error occurred during processing a non-option match (type: {pm.PatternConfig.Type}) at index {index}.",
index: index, matchedText: pm.MatchedText, innerException: ex));
}
}
private void HandleOptionMatch(PotentialMatch pm, int index, AssignmentQuestionEx currentAssignmentQuestion, List<ParseError> errors)
{
try
{
if (currentAssignmentQuestion.Question == null)
{
errors.Add(new ParseError(ParseErrorType.Structural,
$"Found isolated Option at index {index}. Options must belong to a 'Question' type structure. Ignoring this option.",
index: index, matchedText: pm.MatchedText));
return;
}
if (pm.RegexMatch.Groups.Count < 3 || !pm.RegexMatch.Groups[1].Success || string.IsNullOrWhiteSpace(pm.RegexMatch.Groups[2].Value))
{
errors.Add(new ParseError(ParseErrorType.RegexMatchIssue,
$"Option match at index {index} does not have enough regex groups or valid label/text groups (Group 1/2). Skipping this option.",
index: index, matchedText: pm.MatchedText));
return;
}
Option newOption = new Option
{
Label = pm.RegexMatch.Groups[1].Value.Trim(),
Text = pm.RegexMatch.Groups[2].Value.Trim()
};
currentAssignmentQuestion.Question.Options.Add(newOption);
}
catch (Exception ex)
{
errors.Add(new ParseError(ParseErrorType.UnexpectedError,
$"An unexpected error occurred during processing Option at index {index}.",
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;
}
}
/// <summary>
/// Extracts the exam title (simple implementation).
/// Logs errors to the provided error list instead of throwing.
/// </summary>
private string GetExamTitle(string examPaperText)
{
// 内部不再直接抛出异常,而是让外部的 try-catch 负责
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,
$"Invalid range provided to GetSubMatchesForRange. Start: {start}, End: {end}.",
index: start)); // 使用 start 作为大概索引
return new List<PotentialMatch>();
}
// allMatches 为 null 的情况已经在 BuildExamPaper 顶部处理,这里为了方法的健壮性可以再加一次检查
if (allMatches == null)
{
return new List<PotentialMatch>();
}
try
{
return allMatches.Where(pm => pm.StartIndex >= start && pm.StartIndex < end).ToList();
}
catch (Exception ex)
{
errors.Add(new ParseError(ParseErrorType.UnexpectedError,
$"An unexpected error occurred getting sub-matches for range [{start}, {end}).",
innerException: ex));
return new List<PotentialMatch>(); // 出错时返回空列表
}
}
/// <summary>
/// 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
{
int lastOptionEndIndex = 0;
foreach (var pm in potentialMatchesInScope.OrderBy(p => p.StartIndex))
{
// 对每个匹配项的内部处理,记录错误但继续
try
{
if (pm.Type == MatchType.Option)
{
// 验证索引,记录错误但继续
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.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,
$"Option regex match '{pm.MatchedText}' does not have enough groups (expected 3) for label and text. Skipping option.",
index: pm.StartIndex, matchedText: pm.MatchedText));
lastOptionEndIndex = pm.EndIndex; // 更新索引,避免卡死
continue; // 跳过当前选项
}
var newOption = new Option
{
Label = pm.RegexMatch.Groups[1].Value.Trim(),
Text = pm.RegexMatch.Groups[2].Value.Trim()
};
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)
{
// 捕获 ProcessQuestionContent 整个方法内部的意外错误
errors.Add(new ParseError(ParseErrorType.UnexpectedError,
$"An unexpected error occurred while processing content for Question '{question.Number}'.",
innerException: ex));
}
}
}
public class ExamParser
@@ -721,20 +490,16 @@ namespace TechHelper.Client.Exam
}
/// <summary>
/// 解析给定的试卷文本,返回结构化的 ExamPaper 对象。
/// 解析给定的试卷文本,返回结构化的 AssignmentEx 对象。
/// </summary>
/// <param name="examPaperText">完整的试卷文本</param>
/// <returns>解析后的 ExamPaper 对象</returns>
public ExamPaper ParseExamPaper(string examPaperText)
/// <returns>解析后的 AssignmentEx 对象</returns>
public AssignmentEx ParseExamPaper(string examPaperText)
{
// 1. 扫描:一次性扫描整个文本,收集所有潜在的匹配项
// Scan 方法现在已经优化为不抛出 ArgumentNullException
List<PotentialMatch> allPotentialMatches = _scanner.Scan(examPaperText);
// 2. 构建:根据扫描结果和原始文本,线性遍历并构建层级结构
// BuildExamPaper 现在会返回一个包含错误列表的 ExamPaper 对象
// 外部不再需要捕获内部解析异常,只需检查 ExamPaper.Errors 列表
return _builder.BuildExamPaper(examPaperText, allPotentialMatches);
var assignment = new AssignmentEx();
List<PotentialMatch> allPotentialMatches = _scanner.Scan(examPaperText, assignment.Errors);
assignment = _builder.BuildExam(examPaperText, allPotentialMatches);
return assignment;
}
}
}

View File

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

View File

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

View File

@@ -1,19 +1,19 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components;
using System.Net;
using System.Net.Http.Headers;
using TechHelper.Client.HttpRepository;
using System.Net.Http.Headers;
using TechHelper.Client.HttpRepository;
namespace BlazorProducts.Client.HttpInterceptor
{
public class HttpInterceptorHandlerService : DelegatingHandler
{
private readonly NavigationManager _navManager;
private readonly RefreshTokenService2 _refreshTokenService;
private readonly NavigationManager _navManager;
private readonly IRefreshTokenService _refreshTokenService;
public HttpInterceptorHandlerService(
NavigationManager navManager,
RefreshTokenService2 refreshTokenService)
IRefreshTokenService refreshTokenService)
{
_navManager = navManager;
_refreshTokenService = refreshTokenService;
@@ -25,15 +25,14 @@ namespace BlazorProducts.Client.HttpInterceptor
if (absolutePath != null && !absolutePath.Contains("token") && !absolutePath.Contains("account"))
{
var token = await _refreshTokenService.TryRefreshToken();
var token = await _refreshTokenService.TryRefreshToken();
if (!string.IsNullOrEmpty(token))
{
request.Headers.Authorization = new AuthenticationHeaderValue("bearer", token);
}
}
var response = await base.SendAsync(request, cancellationToken);
await HandleResponse(response);
@@ -45,17 +44,17 @@ namespace BlazorProducts.Client.HttpInterceptor
{
if (response is null)
{
_navManager.NavigateTo("/error");
throw new HttpResponseException("服务器不可用。");
throw new HttpResponseException("服务器不可用。");
}
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync();
Console.WriteLine($"HTTP 错误: {response.StatusCode}. 详情: {errorContent}");
Console.WriteLine($"HTTP 错误: {response.StatusCode}. 详情: {errorContent}");
switch (response.StatusCode)
{
@@ -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.
break;
case HttpStatusCode.Unauthorized:
// 401 Unauthorized error. Navigate to an unauthorized page or login page.
_navManager.NavigateTo("/unauthorized"); // Or: _navManager.NavigateTo("/authentication/login");
var token = await _refreshTokenService.TryRefreshToken();
if (!string.IsNullOrEmpty(token))
{
_navManager.NavigateTo(_navManager.Uri, forceLoad: true);
}
else
{
_navManager.NavigateTo("/unauthorized");
}
break;
default:
// For all other errors, navigate to a general error page.

View File

@@ -20,13 +20,12 @@ namespace TechHelper.Client.HttpRepository
private readonly ILocalStorageService _localStorageService;
private readonly NavigationManager _navigationManager;
// 构造函数现在直接接收 HttpClient
public AuthenticationClientService(HttpClient client, // <-- 修正点:直接注入 HttpClient
public AuthenticationClientService(HttpClient client,
AuthenticationStateProvider authenticationStateProvider,
ILocalStorageService localStorageService,
NavigationManager navigationManager)
{
_client = client; // <-- 修正点:直接赋值
_client = client;
_localStorageService = localStorageService;
_stateProvider = authenticationStateProvider;
_navigationManager = navigationManager;
@@ -34,8 +33,6 @@ namespace TechHelper.Client.HttpRepository
public async Task<AuthResponseDto> LoginAsync(UserForAuthenticationDto userForAuthenticationDto)
{
// 移除 using (_client = _clientFactory.CreateClient("Default"))
// _client 已经是注入的实例,直接使用它
var reponse = await _client.PostAsJsonAsync("account/login",
userForAuthenticationDto);
@@ -71,7 +68,6 @@ namespace TechHelper.Client.HttpRepository
public async Task<string> RefreshTokenAsync()
{
// 移除 using (_client = _clientFactory.CreateClient("Default"))
var token = _localStorageService.GetItem<string>("authToken");
var refreshToken = _localStorageService.GetItem<string>("refreshToken");
@@ -96,7 +92,6 @@ namespace TechHelper.Client.HttpRepository
public async Task<ResponseDto> RegisterUserAsync(UserForRegistrationDto userForRegistrationDto)
{
// 移除 using (_client = _clientFactory.CreateClient("Default"))
userForRegistrationDto.ClientURI = Path.Combine(
_navigationManager.BaseUri, "emailconfirmation");
@@ -167,6 +162,9 @@ namespace TechHelper.Client.HttpRepository
((AuthStateProvider)_stateProvider).NotifyUserAuthentication(
result.Token);
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(
"bearer", result.Token);

View File

@@ -24,16 +24,15 @@ namespace TechHelper.Client.HttpRepository
var authState = await _authenticationStateProvider.Value.GetAuthenticationStateAsync();
var user = authState.User;
// 如果 user 或 claims 为空,表示用户未认证,直接返回空字符串
if (user?.Identity == null || !user.Identity.IsAuthenticated)
{
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))
{
return string.Empty; // 没有过期时间声明,也直接返回
return string.Empty;
}
var expTime = DateTimeOffset.FromUnixTimeSeconds(
@@ -41,9 +40,11 @@ namespace TechHelper.Client.HttpRepository
var diff = expTime - DateTime.UtcNow;
// 只有当令牌即将过期时才尝试刷新
var n = DateTime.UtcNow;
if (diff.TotalMinutes <= 2)
return await _authenticationClientService.Value.RefreshTokenAsync(); // 访问 .Value 来调用方法
return await _authenticationClientService.Value.RefreshTokenAsync();
return string.Empty;
}

View File

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

View File

@@ -1,48 +1,59 @@
@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">
<DataAnnotationsValidator />
<MudGrid>
<MudItem xs="12" sm="7">
<MudCard>
<MudCardContent>
<MudTextField Label="Email" Class="mt-3"
@bind-Value="_userForAuth.Email" For="@(() => _userForAuth.Email)" />
<MudTextField Label="Password" HelperText="Choose a strong password" Class="mt-3"
@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="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">
<div class="nav-item px-3">
<NavLink class="nav-link" href="forgotpassword">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span>Forgot Password
</NavLink>
</div>
</MudCard>
</MudItem>
<MudItem xs="12" sm="5">
<MudPaper Class="pa-4 mud-height-full">
<MudText Typo="Typo.subtitle2">Validation Summary</MudText>
@if (!ShowRegistrationErrors)
{
<MudText Color="Color.Success">Success</MudText>
}
else
{
<MudText Color="@Color.Error">
<ValidationSummary />
</MudText>
}
</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>
<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 />
<MudPaper Class="d-flex flex-column flex-grow-1 rounded-xl px-15 justify-content-center pt-15 w-100 " Elevation="0" Outlined="false">
<MudText Typo="Typo.h5"> <b>登录账户</b> </MudText>
<MudTextField Label="Email" Class="mt-3"
@bind-Value="_userForAuth.Email" For="@(() => _userForAuth.Email)" />
<MudTextField Label="Password" HelperText="Choose a strong password" Class="mt-3"
@bind-Value="_userForAuth.Password" For="@(() => _userForAuth.Password)" InputType="InputType.Password" />
<MudStack Row=true Class="align-content-center justify-content-start my-3">
<MudCheckBox @bind-Value="Basic_CheckBox2" Color="Color.Primary"></MudCheckBox>
<MudText Typo="Typo.body2" Align=Align.Center Class="align-content-center"> 点击登录,即表示你同意我们的服务条款和隐私政策。 </MudText>
</MudStack>
<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 Typo="Typo.body2" Class="justify-content-center mx-auto mt-5" Color="Color.Dark">
还没有账户?
<a href="/register" style="color: blue;">Sign in</a>
</MudText>
</MudPaper>
</EditForm>
</MudPaper>
</MudItem>
</MudGrid>
</MudPaper>
</div>
</div>

View File

@@ -16,6 +16,8 @@ namespace TechHelper.Client.Pages.Author
[Inject]
public NavigationManager NavigationManager { get; set; }
public bool Basic_CheckBox2 { get; set; } = true;
public bool ShowRegistrationErrors { get; set; }
public string Error { get; set; }

View 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);
}

View File

@@ -1,96 +1,96 @@
@page "/register"
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Components
@using Entities.Contracts
@using System.Globalization;
@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">
<DataAnnotationsValidator />
<MudGrid>
<MudItem xs="12" sm="7">
<MudCard>
<MudCardContent>
<MudTextField Label="Name" HelperText="Max. 8 characters"
@bind-Value="_userForRegistration.Name" For="@(() => _userForRegistration.Email)" />
<MudTextField Label="Email" Class="mt-3"
@bind-Value="_userForRegistration.Email" For="@(() => _userForRegistration.Email)" />
<MudTextField Label="Password" HelperText="Choose a strong password" Class="mt-3"
@bind-Value="_userForRegistration.Password" For="@(() => _userForRegistration.Password)" InputType="InputType.Password" />
<MudTextField Label="Password" HelperText="Repeat the password" Class="mt-3"
@bind-Value="_userForRegistration.ConfirmPassword" For="@(() => _userForRegistration.ConfirmPassword)" InputType="InputType.Password" />
<MudRadioGroup T="UserRoles" Label="Roles" @bind-Value="_userForRegistration.Roles">
@foreach (UserRoles item in Enum.GetValues(typeof(UserRoles)))
{
if (item != UserRoles.Administrator)
{
<MudRadio Value="@item">@item.ToString()</MudRadio>
}
}
</MudRadioGroup>
<MudStack Row="true">
<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>
<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." />
<MudItem xs="12" sm="8" style="background-color:transparent">
<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>
<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 />
<MudPaper Class="d-flex flex-column flex-grow-1 rounded-xl px-5 justify-content-center pt-15 w-100 " Elevation="0" Outlined="false">
<MudText Typo="Typo.h5"> <b>注册账户</b> </MudText>
<MudTextField Label="Name" HelperText="Max. 8 characters"
@bind-Value="_userForRegistration.Name" For="@(() => _userForRegistration.Email)" />
<MudTextField Label="Email" Class="mt-3"
@bind-Value="_userForRegistration.Email" For="@(() => _userForRegistration.Email)" />
<MudTextField Label="Password" HelperText="Choose a strong password" Class="mt-3"
@bind-Value="_userForRegistration.Password" For="@(() => _userForRegistration.Password)" InputType="InputType.Password" />
<MudTextField Label="Password" HelperText="Repeat the password" Class="mt-3"
@bind-Value="_userForRegistration.ConfirmPassword" For="@(() => _userForRegistration.ConfirmPassword)" InputType="InputType.Password" />
<MudChipSet T="UserRoles" @bind-SelectedValue="_userForRegistration.Roles" CheckMark SelectionMode="SelectionMode.SingleSelection" Class="w-100">
<MudChip Text="Student" Color="Color.Primary" Value="@UserRoles.Student">Student</MudChip>
<MudChip Text="Teacher" Color="Color.Secondary" Value="@UserRoles.Teacher">Teacher</MudChip>
</MudChipSet>
<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>
<MudItem xs="12" sm="5">
<MudPaper Class="pa-4 mud-height-full">
<MudText Typo="Typo.subtitle2">Validation Summary</MudText>
@if (success)
{
<MudText Color="Color.Success">Success</MudText>
}
else
{
<MudText Color="@Color.Error">
<ValidationSummary />
</MudText>
}
</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>
<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>
</MudStack>
<MudStack Row=true Class="align-content-center justify-content-start my-3">
<MudCheckBox @bind-Value="Basic_CheckBox2" Color="Color.Primary"></MudCheckBox>
<MudText Typo="Typo.body2" Align=Align.Center Class="align-content-center"> 点击注册,即表示你同意我们的服务条款和隐私政策。 </MudText>
</MudStack>
<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 Typo="Typo.body2" Class="justify-content-center mx-auto mt-5" Color="Color.Dark">
已有账户?
<a href="/login" style="color: blue;">Sign up</a>
</MudText>
</MudPaper>
</EditForm>
</MudPaper>
</MudItem>
</MudGrid>
</MudPaper>
</div>
</div>

View File

@@ -5,6 +5,7 @@ using MudBlazor;
using System.Text.RegularExpressions;
using TechHelper.Features;
using Entities.Contracts;
using TechHelper.Client.Services;
namespace TechHelper.Client.Pages.Author
{
@@ -12,14 +13,19 @@ namespace TechHelper.Client.Pages.Author
{
private UserForRegistrationDto _userForRegistration = new UserForRegistrationDto();
private GradeEnum grade = GradeEnum.Unknown;
[Inject]
public IAuthenticationClientService AuthenticationService { get; set; }
[Inject]
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 string[] Errors { get; set; }
@@ -51,6 +57,25 @@ namespace TechHelper.Client.Pages.Author
[Inject]
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()
{
string eamilTo = "1928360026@qq.com";

View 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);
}

View 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();
}
}

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

View File

@@ -0,0 +1,6 @@
<MudPaper Class="rounded-xl w-100 px-10 ma-3 pt-5">
<MudText> Name </MudText>
<MudText> 错误数量 </MudText>
<MudText> 分数 </MudText>
</MudPaper>

View File

@@ -0,0 +1,6 @@
<MudPaper Class="rounded-xl w-100 px-10 ma-3 pt-5">
<MudText> Name </MudText>
<MudText> 平均数 </MudText>
<MudText> 中为数 </MudText>
</MudPaper>

View 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));
}
}

View 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));
}
}

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

View 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));
}
}

View File

@@ -0,0 +1,10 @@
<MudText> QuestionNum </MudText>
<MudText> QuestionTypeDis </MudText>
<MudText> ErrorQuestionTypeDis </MudText>
<MudText> ErrorLessonDis </MudText>
<MudText> ErrorIndexDis </MudText>
<MudText> ErrorNum </MudText>
@code {
}

View 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();
}
}

View File

@@ -1,223 +1,239 @@
@using Entities.DTO
@using TechHelper.Client.Exam
@using TechHelper.Client.Services
@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" />
@if (_isLoading)
{
<MudProgressCircular Indeterminate="true" Color="Color.Primary" Class="d-flex justify-center my-8" />
<MudText Class="text-center">正在加载试卷和学生数据...</MudText>
<MudProgressCircular Indeterminate="true" Color="Color.Primary" Class="d-flex justify-center my-8" />
<MudText Class="text-center">正在加载试卷和学生数据...</MudText>
}
else if (_questionsForTable.Any() && _students.Any())
{
<MudTable @ref="_table" T="QuestionRowData" Items="@_questionsForTable" Hover="true" Breakpoint="Breakpoint.Sm" Class="mud-elevation-2" Dense="true">
<HeaderContent>
<MudTh Style="width:100px;">序号</MudTh>
<MudTh Style="width:80px; text-align:center;">分值</MudTh>
@foreach (var student in _students)
{
<MudTh Style="width:120px; text-align:center;">
@student.Name
<MudTooltip Text="点击以切换此学生所有题目的对错">
<MudIconButton Icon="@Icons.Material.Filled.Info" Size="Size.Small" Class="ml-1"
@onclick="() => ToggleStudentAllAnswers(student.Id)" />
</MudTooltip>
</MudTh>
}
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="序号">@context.QuestionItem.Sequence</MudTd>
<MudTd DataLabel="分值" Style="text-align:center;">@context.QuestionItem.Score</MudTd>
@foreach (var student in _students)
{
<MudTd DataLabel="@student.Name" Style="text-align:center;">
@if (context.StudentAnswers.ContainsKey(student.Id))
{
<MudCheckBox @bind-Value="context.StudentAnswers[student.Id]" Size="Size.Small" Color="Color.Primary"></MudCheckBox>
}
else
{
<MudText Color="Color.Warning">N/A</MudText>
}
</MudTd>
}
</RowTemplate>
<PagerContent>
<MudTablePager />
</PagerContent>
</MudTable>
<MudTable @ref="_table" T="QuestionRowData" Items="@_questionsForTable" Hover="true" Breakpoint="Breakpoint.Sm" Striped="true" Class="mud-elevation-2" Dense="true">
<HeaderContent>
<MudTh Style="width:100px;">序号</MudTh>
<MudTh Style="width:80px; text-align:center;">分值</MudTh>
@foreach (var student in _students)
{
<MudTh Style="width:120px; text-align:center;">
@student.DisplayName
<MudTooltip Text="点击以切换此学生所有题目的对错">
<MudIconButton Icon="@Icons.Material.Filled.Info" Size="Size.Small" Class="ml-1"
@onclick="() => ToggleStudentAllAnswers(student.Id)" />
</MudTooltip>
</MudTh>
}
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="序号">@context.QuestionItem.Sequence</MudTd>
<MudTd DataLabel="分值" Style="text-align:center;">@context.QuestionItem.Score</MudTd>
@foreach (var student in _students)
{
<MudTd DataLabel="@student.DisplayName" Style="text-align:center;">
@if (context.StudentAnswers.ContainsKey(student.Id))
{
<MudCheckBox @bind-Value="context.StudentAnswers[student.Id]" Size="Size.Small" Color="Color.Primary"></MudCheckBox>
}
else
{
<MudText Color="Color.Warning">N/A</MudText>
}
</MudTd>
}
</RowTemplate>
<PagerContent>
<MudTablePager />
</PagerContent>
</MudTable>
<MudPaper Class="pa-4 mt-4 mud-elevation-2 d-flex flex-column align-end">
<MudText Typo="Typo.h6">学生总分预览:</MudText>
@foreach (var student in _students)
{
<MudText Typo="Typo.subtitle1">
@student.Name: <MudText Typo="Typo.h5" Color="Color.Primary" Class="d-inline-block ml-2">@GetStudentTotalScore(student.Id)</MudText>
</MudText>
}
<MudButton Variant="Variant.Filled" Color="Color.Success" Class="mt-4" @onclick="SubmitGrading">
提交批改结果 (模拟)
</MudButton>
</MudPaper>
<MudPaper Class="pa-4 mt-4 mud-elevation-2 d-flex flex-column align-end">
<MudText Typo="Typo.h6">学生总分预览:</MudText>
@foreach (var student in _students)
{
<MudText Typo="Typo.subtitle1">
@student.DisplayName: <MudText Typo="Typo.h5" Color="Color.Primary" Class="d-inline-block ml-2">@GetStudentTotalScore(student.Id)</MudText>
</MudText>
}
<MudButton Variant="Variant.Filled" Color="Color.Success" Class="mt-4" @onclick="SubmitGrading">
提交批改结果 (模拟)
</MudButton>
</MudPaper>
}
else
{
<MudAlert Severity="Severity.Info" Class="mt-4">无法加载试卷或题目信息。</MudAlert>
<MudButton Variant="Variant.Text" Color="Color.Primary" Class="mt-4" >返回试卷列表</MudButton>
<MudAlert Severity="Severity.Info" Class="mt-4">无法加载试卷或题目信息。</MudAlert>
<MudButton Variant="Variant.Text" Color="Color.Primary" Class="mt-4">返回试卷列表</MudButton>
}
@code {
[Parameter]
public string ExamId { get; set; } // 从路由获取的试卷ID
[Parameter]
public string ExamId { get; set; }
[Inject]
public IExamService ExamService { get; set; } // 注入试卷服务
[Inject]
public IExamService ExamService { get; set; }
[Inject]
private ISnackbar Snackbar { get; set; } // 注入 Snackbar 用于消息提示
[Inject]
private ISnackbar Snackbar { get; set; }
[Inject]
private NavigationManager Navigation { get; set; } // 注入导航管理器
[Inject]
private NavigationManager Navigation { get; set; }
private MudTable<QuestionRowData> _table = new(); // MudTable 实例引用
private ExamDto ExamDto { get; set; } = new ExamDto(); // 原始试卷数据
private ExamStruct _examStruct = new ExamStruct(); // 处理后的试卷结构,包含带序号的题目
private MudTable<QuestionRowData> _table = new();
private AssignmentDto Assignment { get; set; } = new AssignmentDto();
private AssignmentCheckData _examStruct = new AssignmentCheckData();
private List<Student> _students = new List<Student>(); // 临时生成的学生列表
private List<QuestionRowData> _questionsForTable = new List<QuestionRowData>(); // 用于 MudTable 的数据源
private List<StudentDto> _students = new List<StudentDto>();
private List<QuestionRowData> _questionsForTable = new List<QuestionRowData>();
private bool _isLoading = true; // 加载状态
private bool _isLoading = true;
// 在组件初始化时加载数据
protected override async Task OnInitializedAsync()
{
_isLoading = true;
await LoadExamData();
GenerateTemporaryStudentsAndAnswers(); // 生成学生和初始作答数据
_isLoading = false;
}
[Inject]
public IClassServices ClassServices { get; set; }
// 加载试卷数据的方法
private async Task LoadExamData()
{
if (Guid.TryParse(ExamId, out Guid parsedExamId))
{
try
{
var result = await ExamService.GetExam(parsedExamId);
if (result.Status)
{
ExamDto = result.Result as ExamDto ?? new ExamDto();
_examStruct = ExamDto.GetStruct(); // 将 ExamDto 转换为 ExamStruct
}
else
{
Snackbar?.Add($"获取试卷失败: {result.Message}", Severity.Error);
Navigation.NavigateTo("/exam/manager"); // 导航回管理页
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"获取试卷时发生错误: {ex.Message}");
Snackbar?.Add($"获取试卷失败: {ex.Message}", Severity.Error);
Navigation.NavigateTo("/exam/manager");
}
}
else
{
Console.Error.WriteLine($"错误:路由参数 ExamId '{ExamId}' 不是一个有效的 GUID 格式。");
Snackbar?.Add("无效的试卷ID无法加载。", Severity.Error);
Navigation.NavigateTo("/exam/manager");
}
}
protected override async Task OnInitializedAsync()
{
_isLoading = true;
await LoadExamData();
// 生成临时学生和作答数据
private void GenerateTemporaryStudentsAndAnswers()
{
_students = new List<Student>();
// 生成 40 个学生
for (int i = 1; i <= 40; i++)
{
_students.Add(new Student { Name = $"学生{i}" });
}
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;
}
_questionsForTable = _examStruct.Questions.Select(qItem =>
{
var rowData = new QuestionRowData
{
QuestionItem = qItem,
StudentAnswers = new Dictionary<Guid, bool>()
};
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();
}
// 为每个学生随机生成初始的对错状态
var random = new Random();
foreach (var student in _students)
{
// 模拟随机对错50%的概率
rowData.StudentAnswers[student.Id] = random.Next(0, 2) == 1;
}
return rowData;
}).ToList();
}
private async Task LoadExamData()
{
if (Guid.TryParse(ExamId, out Guid parsedExamId))
{
try
{
var result = await ExamService.GetExam(parsedExamId);
if (result.Status)
{
Assignment = result.Result as AssignmentDto ?? new AssignmentDto();
_examStruct = Assignment.GetStruct();
}
else
{
Snackbar?.Add($"获取试卷失败: {result.Message}", Severity.Error);
Navigation.NavigateTo("/exam/manager");
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"获取试卷时发生错误: {ex.Message}");
Snackbar?.Add($"获取试卷失败: {ex.Message}", Severity.Error);
Navigation.NavigateTo("/exam/manager");
}
}
else
{
Console.Error.WriteLine($"错误:路由参数 ExamId '{ExamId}' 不是一个有效的 GUID 格式。");
Snackbar?.Add("无效的试卷ID无法加载。", Severity.Error);
Navigation.NavigateTo("/exam/manager");
}
}
// 当某个学生的某个题目的作答状态改变时触发
private void OnAnswerChanged(string questionSequence, Guid studentId, bool isCorrect)
{
// 可以在这里添加额外的逻辑,例如记录更改
Console.WriteLine($"题目 {questionSequence}, 学生 {studentId} 的答案变为: {isCorrect}");
// 由于是 @bind-Checked数据模型已经自动更新这里只是日志
}
// 计算某个学生的总分
private float GetStudentTotalScore(Guid studentId)
{
float totalScore = 0;
foreach (var row in _questionsForTable)
{
if (row.StudentAnswers.TryGetValue(studentId, out bool isCorrect) && isCorrect)
{
totalScore += row.QuestionItem.Score;
}
}
return totalScore;
}
// 切换某个学生所有题目的对错状态 (用于快速批改)
private void ToggleStudentAllAnswers(Guid studentId)
{
bool allCorrect = _questionsForTable.All(row => row.StudentAnswers.ContainsKey(studentId) && row.StudentAnswers[studentId]);
private float GetStudentTotalScore(Guid studentId)
{
float totalScore = 0;
foreach (var row in _questionsForTable)
{
if (row.StudentAnswers.TryGetValue(studentId, out bool isCorrect) && isCorrect)
{
totalScore += row.QuestionItem.Score;
}
}
return totalScore;
}
foreach (var row in _questionsForTable)
{
if (row.StudentAnswers.ContainsKey(studentId))
{
row.StudentAnswers[studentId] = !allCorrect; // 全部取反
}
}
StateHasChanged(); // 手动通知 Blazor 刷新 UI
}
private void ToggleStudentAllAnswers(Guid studentId)
{
bool allCorrect = _questionsForTable.All(row => row.StudentAnswers.ContainsKey(studentId) && row.StudentAnswers[studentId]);
// 提交批改结果(模拟)
private void SubmitGrading()
{
Console.WriteLine("--- 提交批改结果 ---");
foreach (var student in _students)
{
Console.WriteLine($"学生: {student.Name}, 总分: {GetStudentTotalScore(student.Id)}");
foreach (var row in _questionsForTable)
{
if (row.StudentAnswers.TryGetValue(student.Id, out bool isCorrect))
{
Console.WriteLine($" - 题目 {row.QuestionItem.Sequence}: {(isCorrect ? "正确" : "错误")}");
}
}
}
Snackbar?.Add("批改结果已提交(模拟)", Severity.Success);
// 实际应用中,这里会将 _questionsForTable 和 _students 的数据发送到后端API
}
foreach (var row in _questionsForTable)
{
if (row.StudentAnswers.ContainsKey(studentId))
{
row.StudentAnswers[studentId] = !allCorrect;
}
}
StateHasChanged();
}
private void SubmitGrading()
{
List<SubmissionDto> submissionDto = new List<SubmissionDto>();
foreach (var student in _students)
{
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)
{
if (row.QuestionItem.AssignmentQuestionDto.StructType == Entities.Contracts.AssignmentStructType.Struct) continue;
if (row.StudentAnswers.TryGetValue(student.Id, out bool 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);
}
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);
});
}
}

View File

@@ -1,4 +1,8 @@
@page "/exam/create"
@using AutoMapper
@using TechHelper.Client.Pages.Common
@using TechHelper.Client.Pages.Exam.ExamView
@using TechHelper.Client.Services
@using Blazored.TextEditor
@using Entities.DTO
@using TechHelper.Client.Exam
@@ -7,64 +11,53 @@
@using Microsoft.AspNetCore.Components
@using System.Globalization;
@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%">
<MudDrawer @bind-Open="@_open" Elevation="0" Variant="@DrawerVariant.Persistent" Color="Color.Primary" Anchor="Anchor.End" OverlayAutoClose="true">
<MudDrawerHeader>
<MudText Typo="Typo.h6"> 配置 </MudText>
</MudDrawerHeader>
<MudStack Class="overflow-auto">
<ParseRoleConfig />
<MudButton Color="Color.Success"> ParseExam </MudButton>
</MudStack>
@if (_edit)
{
<AssignmentQuestionEdit AssignmentQuestion="selectedAssignmentQuestion" />
}
else
{
<MudDrawerHeader>
<MudText Typo="Typo.h6"> 配置 </MudText>
</MudDrawerHeader>
<MudStack Class="overflow-auto">
<ParseRoleConfig />
<MudButton Color="Color.Success"> ParseExam </MudButton>
</MudStack>
}
</MudDrawer>
<MudStack Row="true" Class="flex-grow-1" Style="height:100%">
<ExamView Class="overflow-auto" ParsedExam="ExamContent"></ExamView>
<MudStack Class="flex-grow-1 h-100">
<MudPaper Class="ma-2">
<MudPaper Elevation="0" Style="height:calc(100% - 80px)">
<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>
</MudPaper>
<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>
<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>
<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>
@@ -74,23 +67,76 @@
[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 ExamPaper _parsedExam = new ExamPaper();
private ExamDto ExamContent = new ExamDto();
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))
{
@@ -100,7 +146,8 @@
_parsedExam = exampar.ParseExamPaper(plainText);
Snackbar.Add("试卷解析成功。", Severity.Success);
Snackbar.Add($"{_parsedExam.Errors}。", Severity.Success);
ExamContent = _parsedExam.ConvertToExamDTO();
StateHasChanged();
ExamContent = Mapper.Map<AssignmentDto>(_parsedExam);
ExamContent.SeqIndex();
}
catch (Exception ex)
@@ -125,9 +172,58 @@
public async Task Publish()
{
ExamContent.CreaterEmail = authenticationStateTask.Result.User.Identity.Name;
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)">
<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>
</MudPaper>
</MudPaper> *@
<!-- #endregion -->

View File

@@ -1,5 +1,8 @@
@page "/exam/edit/{ExamId}"
@using Entities.DTO
@using TechHelper.Client.Pages.Exam.ExamView
@using TechHelper.Client.Services
@using Entities.DTO
@using TechHelper.Client.Exam
<ExamView ParsedExam="@ExamDto"/>
@@ -17,7 +20,7 @@
[Inject]
private ISnackbar Snackbar { get; set; }
private ExamDto ExamDto { get; set; }
private AssignmentDto ExamDto { get; set; }
protected override async Task OnInitializedAsync()
{
@@ -28,7 +31,7 @@
try
{
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)
{

View File

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

View File

@@ -1,9 +1,11 @@
@using Entities.DTO
@using Microsoft.AspNetCore.Authorization
@using TechHelper.Client.Exam
@using TechHelper.Client.Pages.Common.Exam
@page "/exam/manage"
@using Entities.DTO
@using TechHelper.Client.Services
@attribute [Authorize]
@@ -19,7 +21,8 @@ else
<MudPaper Class="d-flex flex-wrap flex-grow-0 gap-4" Height="100%" Width="100%">
@foreach (var item in examDtos)
{
<ExamPreview examDto="item" Width="256px" Height="256px"> </ExamPreview>
@* <ExamPreview AssignmentDto="item" Width="256px" Height="256px"> </ExamPreview> *@
<AssignmentInfoCard></AssignmentInfoCard>
}
</MudPaper>
@@ -33,7 +36,7 @@ else
[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }
private List<ExamDto> examDtos = new List<ExamDto>();
private List<AssignmentDto> examDtos = new List<AssignmentDto>();
private bool isloding = true;
@@ -47,10 +50,17 @@ else
{
isloding = true;
Snackbar.Add("正在加载", Severity.Info);
var result = await ExamService.GetAllExam(authenticationStateTask.Result.User.Identity.Name);
examDtos = result.Result as List<ExamDto> ?? new List<ExamDto>();
var result = await ExamService.GetAllExam();
if (result.Status)
{
examDtos = result.Result as List<AssignmentDto> ?? new List<AssignmentDto>();
Snackbar.Add("加载成功", Severity.Info);
}
else
{
Snackbar.Add($"加载失败 {result.Message}", Severity.Error);
}
isloding = false;
Snackbar.Add("加载成功", Severity.Info);
StateHasChanged();
}
}

View File

@@ -3,7 +3,7 @@
<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">
<MudText Typo="Typo.body2"> @examDto.AssignmentTitle </MudText>
<MudText Typo="Typo.body2"> @AssignmentDto.Title </MudText>
<MudPaper>
@@ -12,7 +12,11 @@
<MudButton OnClick="ExamClick"> 详情 </MudButton>
<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" />
</MudButtonGroup>
</MudPaper>
@@ -21,12 +25,16 @@
</MudPaper>
@code {
[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }
private bool bteacher = false;
[Inject]
public NavigationManager navigationManager { get; set; }
[Parameter]
public ExamDto examDto { get; set; }
public AssignmentDto AssignmentDto { get; set; }
[Parameter]
@@ -45,13 +53,19 @@
public string? MaxHeight { get; set; } = "64";
protected override Task OnInitializedAsync()
{
bteacher = authenticationStateTask.Result.User.IsInRole("Teacher");
return base.OnInitializedAsync();
}
private void ExamClick()
{
navigationManager.NavigateTo($"exam/edit/{examDto.AssignmentId}");
navigationManager.NavigateTo($"exam/edit/{AssignmentDto.Id}");
}
private void CheckExam()
{
navigationManager.NavigateTo($"exam/check/{examDto.AssignmentId}");
navigationManager.NavigateTo($"exam/check/{AssignmentDto.Id}");
}
}

View 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);
}
}

View File

@@ -4,11 +4,11 @@
@if (ParsedExam != null)
{
<MudPaper Height="@Height" Class="@Class" Style="@Style" Width="@Width">
<MudText Class="d-flex justify-content-center" Typo="Typo.h6"> @ParsedExam.AssignmentTitle </MudText>
<MudPaper Height="@Height" Class="@Class" Style="@Style" Width="@Width" Elevation="5">
<MudText Class="d-flex justify-content-center" Typo="Typo.button"> <b> @ParsedExam.Title </b></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>
}
@@ -24,7 +24,11 @@ else
@code {
[Parameter]
public ExamDto ParsedExam { get; set; } = new ExamDto();
public AssignmentDto ParsedExam { get; set; } = new AssignmentDto();
[Parameter]
public EventCallback<AssignmentQuestionDto> ClickedStruct { get; set; }
[Parameter]
public string Height { get; set; } = "100%";
[Parameter]
@@ -33,4 +37,10 @@ else
public string Class { get; set; } = "";
[Parameter]
public string Style { get; set; } = "";
private void HandleClickedStruct(AssignmentQuestionDto dto)
{
ClickedStruct.InvokeAsync(dto);
}
}

View File

@@ -1,127 +1,62 @@
@using TechHelper.Client.Exam
@using Entities.Contracts // Assuming SubjectAreaEnum is defined here, adjust if not
<MudPaper Outlined="true" Class="mt-2">
<MudRadioGroup @bind-Value="_examParser">
@foreach (ExamParserEnum item in Enum.GetValues(typeof(ExamParserEnum)))
{
<MudRadio T="ExamParserEnum" Value="@item">@item</MudRadio>
}
</MudRadioGroup>
<MudText Typo="Typo.h6" Class="mb-4">Current Parsing Rules</MudText>
<MudTextField @bind-Value="_ParserConfig" Label="正则表达式模式" Variant="Variant.Outlined" FullWidth="true" Class="mb-2" />
<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>
@* Display Question Patterns *@
@if (ExamParserConfig.QuestionPatterns.Any())
{
<MudExpansionPanel Text="Question Patterns" Class="mb-2" IsInitiallyExpanded="true">
<MudStack Spacing="1">
@foreach (var config in ExamParserConfig.QuestionPatterns)
{
<MudChip T="string" Color="Color.Info" Variant="Variant.Outlined" Class="d-flex justify-content-between align-items-center">
<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>
}
</MudStack>
</MudExpansionPanel>
}
else
{
<MudText Typo="Typo.body2" Class="mb-2">No question patterns configured.</MudText>
}
<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())
{
<MudExpansionPanel Text="题目模式详情" Class="mb-2">
<MudStack>
@foreach (var config in ExamParserConfig.QuestionPatterns)
{
<MudChip T="string">
**模式:** <code>@config.Pattern</code>, **优先级:** @config.Priority
</MudChip>
}
</MudStack>
</MudExpansionPanel>
}
else
{
<MudText Typo="Typo.body2" Class="mb-2">暂无题目模式。</MudText>
}
@if (ExamParserConfig.OptionPatterns.Any())
{
<MudExpansionPanel Text="选项模式详情" Class="mb-2">
<MudStack>
@foreach (var config in ExamParserConfig.OptionPatterns)
{
<MudChip T="string">
**模式:** <code>@config.Pattern</code>, **优先级:** @config.Priority
</MudChip>
}
</MudStack>
</MudExpansionPanel>
}
else
{
<MudText Typo="Typo.body2" Class="mb-2">暂无选项模式。</MudText>
}
<MudButton Variant="Variant.Filled" Color="Color.Secondary" OnClick="ResetPatterns">重置默认规则</MudButton>
@* Display Option Patterns *@
@if (ExamParserConfig.OptionPatterns.Any())
{
<MudExpansionPanel Text="Option Patterns" Class="mb-2" IsInitiallyExpanded="true">
<MudStack Spacing="1">
@foreach (var config in ExamParserConfig.OptionPatterns)
{
<MudChip T="string" Color="Color.Warning" Variant="Variant.Outlined" Class="d-flex justify-content-between align-items-center">
<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>
}
</MudStack>
</MudExpansionPanel>
}
else
{
<MudText Typo="Typo.body2" Class="mb-2">No option patterns configured.</MudText>
}
</MudPaper>
@code {
[Parameter]
public ExamParserConfig ExamParserConfig { get; set; } = new ExamParserConfig();
public ExamParserEnum _examParser { get; set; } = ExamParserEnum.MajorQuestionGroupPatterns;
private string _ParserConfig;
private int _Priority = 1;
[Parameter]
public ExamParserConfig ExamParserConfig { get; set; } = new ExamParserConfig();
[Inject]
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();
}
// No other properties or methods are needed as the component is now purely for display.
}

View File

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

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

View 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();
}

View 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 {
}

View File

@@ -1,118 +1,23 @@
@page "/"
@using Microsoft.AspNetCore.Authorization
@using TechHelper.Client.Pages.Common.Exam
<AuthorizeView Roles="Administrator">
<MudText> Hello @context.User.Identity.Name</MudText>
@foreach (var item in context.User.Claims)
{
<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 Roles="Student">
<Authorized>
<TechHelper.Client.Pages.Student.HomePage />
</Authorized>
</AuthorizeView>
<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>
<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>
<AssignmentInfoCard></AssignmentInfoCard>
@code {
[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }
protected override Task OnInitializedAsync()
{
return base.OnInitializedAsync();
Console.WriteLine(authenticationStateTask.Result.User.IsInRole("Student"));
}
}

View File

@@ -55,4 +55,5 @@ else
<MudText Class="ma-3 pa-3"> 年级 : @authenticationStateTask.Result.User.FindFirst("Grade")?.Value.ToString() </MudText>
<MudText Class="ma-3 pa-3"> 班级 : @authenticationStateTask.Result.User.FindFirst("Class")?.Value.ToString() </MudText>
</MudPaper>
}
}
<RestoreUserRole></RestoreUserRole>

View File

@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Components.Authorization;
using MudBlazor;
using System.Collections.Frozen;
using System.ComponentModel.DataAnnotations;
using System.Security.Claims;
using TechHelper.Client.HttpRepository;
using TechHelper.Client.Services;

View File

@@ -3,58 +3,56 @@
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Identity
<MudPaper Class="flex-grow-1">
<PageTitle>Profile</PageTitle>
<h3>Profile</h3>
<div class="row">
<div class="col-md-6">
<EditForm Model="Input" FormName="profile" OnValidSubmit="OnValidSubmitAsync" method="post">
<DataAnnotationsValidator />
<ValidationSummary class="text-danger" role="alert" />
<div class="form-floating mb-3">
<input type="text" value="@username" class="form-control" placeholder="Please choose your username." disabled />
<label for="username" class="form-label">Username</label>
</div>
<div class="form-floating mb-3">
<InputText @bind-Value="Input.PhoneNumber" class="form-control" placeholder="Please enter your phone number." />
<label for="phone-number" class="form-label">Phone number</label>
<ValidationMessage For="() => Input.PhoneNumber" class="text-danger" />
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Save</button>
</EditForm>
</div>
</div>
<div class="row">
<div class="col-md-6">
<EditForm Model="Input" FormName="profile" OnValidSubmit="OnValidSubmitAsync" method="post">
<DataAnnotationsValidator />
<ValidationSummary class="text-danger" role="alert" />
<div class="form-floating mb-3">
<input type="text" value="@username" class="form-control" placeholder="Please choose your username." disabled />
<label for="username" class="form-label">Username</label>
</div>
<div class="form-floating mb-3">
<InputText @bind-Value="Input.PhoneNumber" class="form-control" placeholder="Please enter your phone number." />
<label for="phone-number" class="form-label">Phone number</label>
<ValidationMessage For="() => Input.PhoneNumber" class="text-danger" />
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Save</button>
</EditForm>
</div>
</div>
</MudPaper>
@code {
private string? username;
private string? phoneNumber;
private string? username;
private string? phoneNumber;
[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }
[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
protected override async Task OnInitializedAsync()
{
username = authenticationStateTask.Result.User.Identity.Name;
phoneNumber = authenticationStateTask.Result.User.Identity.IsAuthenticated.ToString();
protected override async Task OnInitializedAsync()
{
username = authenticationStateTask.Result.User.Identity.Name;
phoneNumber = authenticationStateTask.Result.User.Identity.IsAuthenticated.ToString();
Input.PhoneNumber ??= phoneNumber;
}
Input.PhoneNumber ??= phoneNumber;
}
private async Task OnValidSubmitAsync()
{
private async Task OnValidSubmitAsync()
{
}
}
private sealed class InputModel
{
[Phone]
[Display(Name = "Phone number")]
public string? PhoneNumber { get; set; }
}
private sealed class InputModel
{
[Phone]
[Display(Name = "Phone number")]
public string? PhoneNumber { get; set; }
}
}

View 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();
}
}

View 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();
}
}

View File

@@ -13,8 +13,8 @@ using TechHelper.Client.Services;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using TechHelper.Client.AI;
using TechHelper.Client.Exam;
using Microsoft.AspNetCore.Components;
using TechHelper.Context;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
@@ -27,12 +27,16 @@ builder.Services.AddOidcAuthentication(options =>
builder.Configuration.Bind("Local", options.ProviderOptions);
});
builder.Services.AddAutoMapper(typeof(AutoMapperProFile).Assembly);
builder.Services.Configure<ApiConfiguration>(builder.Configuration.GetSection("ApiConfiguration"));
builder.Services.AddAuthorizationCore();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddLocalStorageServices();
builder.Services.AddScoped<IAuthenticationClientService, AuthenticationClientService>();
builder.Services.AddScoped<IExamService, ExamService>();
builder.Services.AddScoped<AuthenticationStateProvider, AuthStateProvider>();
@@ -40,9 +44,8 @@ builder.Services.AddScoped<IRefreshTokenService, RefreshTokenService>();
builder.Services.AddScoped<IClassServices, ClasssServices>();
builder.Services.AddScoped<IEmailSender, QEmailSender>();
builder.Services.AddScoped<HttpInterceptorHandlerService>();
builder.Services.AddScoped<RefreshTokenService2>();
builder.Services.AddScoped<IAIService, AiService>();
builder.Services.AddScoped<IUserServices, UserServices>();
builder.Services.AddHttpClient("WebApiClient", client =>
{
var baseAddress = builder.Configuration.GetSection("ApiConfiguration:BaseAddress").Value;

View File

@@ -14,7 +14,7 @@
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://localhost:7047;http://localhost:5190",
"applicationUrl": "https://localhost:7047",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
@@ -28,4 +28,6 @@
}
}
}
}

View File

@@ -1,15 +1,21 @@
using Entities.DTO;
using System.Net.Http.Json;
using Entities.Contracts;
using Entities.DTO;
using Newtonsoft.Json;
using System.Net.Http.Json;
using TechHelper.Client.HttpRepository;
using TechHelper.Services;
namespace TechHelper.Client.Services
{
public class ClasssServices : IClassServices
{
private readonly HttpClient _client;
private readonly IAuthenticationClientService _authenticationClientService;
public ClasssServices(HttpClient client)
public ClasssServices(HttpClient client, IAuthenticationClientService authenticationClientService)
{
_client = client;
_authenticationClientService = authenticationClientService;
}
public Task<ResponseDto> CreateClass(UserRegistrationToClassDto userClass)
@@ -17,6 +23,37 @@ namespace TechHelper.Client.Services
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)
{
try
@@ -25,6 +62,8 @@ namespace TechHelper.Client.Services
userRegistrationToClassDto);
var data = await result.Content.ReadAsStringAsync();
await _authenticationClientService.RefreshTokenAsync();
return new ResponseDto
{
IsSuccessfulRegistration = result.IsSuccessStatusCode,

View File

@@ -3,19 +3,19 @@ using TechHelper.Client.AI;
using TechHelper.Services;
using Entities.DTO;
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
{
private readonly IAIService _aIService; // 遵循命名规范,字段前加下划线
private readonly HttpClient _client; // 直接注入 HttpClient
private readonly IAIService _aIService;
private readonly HttpClient _client;
public ExamService(IAIService aIService, HttpClient client) // 修正点:直接注入 HttpClient
public ExamService(IAIService aIService, HttpClient client)
{
_aIService = aIService;
_client = client; // 赋值注入的 HttpClient 实例
_client = client;
}
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?user={user}");
var response = await _client.GetAsync($"exam/getAllPreview");
if (response.IsSuccessStatusCode)
{
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);
}
else
@@ -105,14 +104,33 @@ namespace TechHelper.Client.Exam
}
}
public async Task<ApiResponse> GetAllSubmission()
{
try
{
var response = await _client.GetAsync($"exam/getAllSubmission");
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
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/get?id={guid}");
var response = await _client.GetAsync($"exam/{guid}");
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
var exam = JsonConvert.DeserializeObject<ExamDto>(content);
var exam = JsonConvert.DeserializeObject<AssignmentDto>(content);
return ApiResponse.Success(result: exam);
}
else
@@ -144,12 +162,12 @@ namespace TechHelper.Client.Exam
}
}
public async Task<ApiResponse> SaveParsedExam(ExamDto examDto)
public async Task<ApiResponse> SaveParsedExam(AssignmentDto assiDto)
{
// 直接使用注入的 _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: "试题保存成功。");
}
@@ -159,5 +177,18 @@ namespace TechHelper.Client.Exam
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("提交失败");
}
}
}
}

View File

@@ -8,5 +8,7 @@ namespace TechHelper.Client.Services
{
public Task<ResponseDto> UserRegister(UserRegistrationToClassDto userRegistrationToClassDto);
public Task<ResponseDto> CreateClass(UserRegistrationToClassDto userClass);
public Task<ApiResponse> GetClassStudents();
public Task<ApiResponse> GetGradeClasses(byte grade);
}
}

View File

@@ -1,17 +1,19 @@
using Entities.DTO;
using TechHelper.Services;
namespace TechHelper.Client.Exam
namespace TechHelper.Client.Services
{
public interface IExamService
{
public Task<ApiResponse> FormatExam(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 ApiResponse ConvertToXML<T>(string xmlContent);
public Task<ApiResponse> GetAllExam(string user);
public Task<ApiResponse> GetAllExam();
public Task<ApiResponse> GetExam(Guid guid);
public Task<ApiResponse> SubmissionAssignment(SubmissionDto submission);
public Task<ApiResponse> GetAllSubmission();
}
}

View File

@@ -0,0 +1,9 @@
using TechHelper.Services;
namespace TechHelper.Client.Services
{
public interface IUserServices
{
public Task<ApiResponse> RestoreUserInfo();
}
}

View 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();
}
}
}

View File

@@ -5,8 +5,6 @@
<MudPaper Class="d-flex flex-row flex-grow-1 overflow-hidden" Height="100%">
<MudPaper Width="200px">
<h1>Manage your account</h1>
<h2>Change your account settings</h2>
<MudDivider Class="flex-grow-0" />
<ExamNavMenu />

View File

@@ -3,11 +3,8 @@
<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" />
<MudStack Row="true">
<MudStack Row="true" Class="d-flex flex-grow-1">
<ManageNavMenu />
@Body
</MudStack>

View File

@@ -4,6 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
@@ -22,6 +23,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="14.0.0" />
<PackageReference Include="Blazor.LocalStorage.WebAssembly" Version="8.0.0" />
<PackageReference Include="Blazored.TextEditor" Version="1.1.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.12" />
@@ -38,4 +40,8 @@
<ProjectReference Include="..\Entities\Entities.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Pages\Exam\TemplateCard\" />
</ItemGroup>
</Project>

View File

@@ -4,6 +4,6 @@
"ClientId": "33333333-3333-3333-33333333333333333"
},
"ApiConfiguration": {
"BaseAddress": "http://localhost:5099"
"BaseAddress": "http://localhost:8080"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

View File

@@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Entities.Contracts;
using TechHelper.Server.Context.Configuration;
namespace TechHelper.Context
{
@@ -14,7 +13,6 @@ namespace TechHelper.Context
public DbSet<AssignmentClass> AssignmentClasses { get; set; }
public DbSet<Assignment> Assignments { get; set; }
public DbSet<AssignmentQuestion> AssignmentGroups { get; set; }
public DbSet<AssignmentQuestion> AssignmentQuestions { get; set; }
public DbSet<Class> Classes { get; set; }
public DbSet<ClassTeacher> ClassStudents { get; set; }
@@ -22,6 +20,7 @@ namespace TechHelper.Context
public DbSet<Question> Questions { get; set; }
public DbSet<Submission> Submissions { get; set; }
public DbSet<SubmissionDetail> SubmissionDetails { get; set; }
public DbSet<QuestionContext> QuestionContexts { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
@@ -29,14 +28,12 @@ namespace TechHelper.Context
builder.ApplyConfiguration(new RoleConfiguration());
builder.ApplyConfiguration(new AssignmentConfiguration());
builder.ApplyConfiguration(new AssignmentClassConfiguration());
builder.ApplyConfiguration(new AssignmentGroupConfiguration());
builder.ApplyConfiguration(new AssignmentQuestionConfiguration());
builder.ApplyConfiguration(new ClassConfiguration());
builder.ApplyConfiguration(new ClassStudentConfiguration());
builder.ApplyConfiguration(new ClassTeacherConfiguration());
builder.ApplyConfiguration(new QuestionConfiguration());
builder.ApplyConfiguration(new SubmissionConfiguration());
builder.ApplyConfiguration(new QuestionGroupConfiguration());
builder.ApplyConfiguration(new SubmissionDetailConfiguration());
}
}

View File

@@ -23,73 +23,37 @@ namespace TechHelper.Context
public AutoMapperProFile()
{
CreateMap<UserForRegistrationDto, User>()
.ForMember(dest => dest.Id, opt => opt.Ignore()) // ID由IdentityUser生成
.ForMember(dest => dest.UserName, opt => opt.MapFrom(src => src.Name)) // 或者 MapFrom(src => src.Name)
.ForMember(dest => dest.Id, opt => opt.Ignore())
.ForMember(dest => dest.UserName, opt => opt.MapFrom(src => src.Name))
.ForMember(dest => dest.Email, opt => opt.MapFrom(src => src.Email))
.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.PasswordHash, opt => opt.Ignore()) // 密码哈希由 UserManager 处理
.ForMember(dest => dest.EmailConfirmed, opt => opt.Ignore()); // 邮箱确认状态由服务层处理
.ForMember(dest => dest.Address, opt => opt.MapFrom(src => src.HomeAddress))
.ForMember(dest => dest.PasswordHash, opt => opt.Ignore())
.ForMember(dest => dest.EmailConfirmed, opt => opt.Ignore());
CreateMap<ClassDto, Class>()
.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()));
.ForMember(d => d.Number, o => o.MapFrom(src => src.Class))
.ReverseMap();
CreateMap<QuestionGroupDto, AssignmentGroup>()
.ForMember(dest => dest.ChildAssignmentGroups, opt => opt.MapFrom(src => src.SubQuestionGroups))
.ForMember(dest => dest.AssignmentQuestions, opt => opt.MapFrom(src => src.SubQuestions));
// Assignment
CreateMap<AssignmentDto, Assignment>().ReverseMap();
CreateMap<SubQuestionDto, AssignmentQuestion>()
.ForMember(dest => dest.Question, opt => opt.MapFrom(src => src)); // 映射到嵌套的 Question 对象
CreateMap<AssignmentQuestionDto, AssignmentQuestion>().ReverseMap();
CreateMap<QuestionGroupDto, QuestionGroup>()
.ForMember(dest => dest.ChildQuestionGroups, opt => opt.MapFrom(src => src.SubQuestionGroups))
.ForMember(dest => dest.Title, opt => opt.MapFrom(src => src.Title))
.ForMember(dest => dest.Description, opt => opt.MapFrom(src => src.Descript));
CreateMap<QuestionDto, Question>().ReverseMap();
CreateMap<QuestionContext, QuestionContextDto>().ReverseMap();
CreateMap<Assignment, ExamDto>();
// Submission
CreateMap<SubmissionDto, Submission>().ReverseMap();
CreateMap<SubmissionDetailDto, SubmissionDetail>().ReverseMap();
}
}

View File

@@ -29,10 +29,10 @@ namespace TechHelper.Context.Configuration
.IsRequired()
.HasColumnName("due_date");
builder.Property(a => a.TotalPoints)
builder.Property(a => a.TotalQuestions)
.HasColumnName("total_points");
builder.Property(a => a.CreatedBy)
builder.Property(a => a.CreatorId)
.HasColumnName("created_by");
builder.Property(a => a.CreatedAt)
@@ -54,29 +54,16 @@ namespace TechHelper.Context.Configuration
// 如果 User 有一个名为 AssignmentsCreated 的导航属性,应写为 .WithMany(u => u.AssignmentsCreated)
builder.HasOne(a => a.Creator)
.WithMany() // User 实体没有指向 Assignment 的导航属性 (或我们不知道)
.HasForeignKey(a => a.CreatedBy)
.HasForeignKey(a => a.CreatorId)
.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.HasMany(a => a.AssignmentAttachments)
.WithOne(aa => aa.Assignment)
.HasForeignKey("AssignmentId")
.OnDelete(DeleteBehavior.Cascade);
// 关系: Assignment (一) 到 Submission (多)
// 假设 Submission 实体包含一个名为 AssignmentId 的外键属性
builder.HasMany(a => a.Submissions)
.WithOne(s => s.Assignment)
.HasForeignKey("AssignmentId")
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne(a=>a.ExamStruct)
.WithOne()
.HasForeignKey<Assignment>(a=>a.ExamStructId)
.OnDelete(DeleteBehavior.Cascade);
}
}
}

View File

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

View File

@@ -25,12 +25,8 @@ namespace TechHelper.Context.Configuration
.HasColumnName("question_id");
builder.Property(aq => aq.QuestionGroupId)
.HasColumnName("question_group_id");
// 配置 QuestionNumber 列
builder.Property(aq => aq.QuestionNumber)
builder.Property(aq => aq.Index)
.HasColumnName("question_number")
.IsRequired(); // uint 类型默认非空
@@ -42,62 +38,28 @@ namespace TechHelper.Context.Configuration
builder.Property(aq => aq.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)
.HasColumnName("deleted")
.HasDefaultValue(false); // 适用于软删除策略
// 4. 配置导航属性和外键关系
// ---
// 配置 AssignmentQuestion 到 Question 的关系 (多对一)
// 一个 AssignmentQuestion 属于一个 Question。
//
// 假设 `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.Question)
.WithMany(q => q.AssignmentQuestions)
.HasForeignKey(aq => aq.QuestionId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne(aq => aq.QuestionGroup)
.WithMany(qg => qg.AssignmentQuestions)
.HasForeignKey(aq => aq.QuestionGroupId)
.OnDelete(DeleteBehavior.SetNull);
builder.HasOne(aq => aq.ParentAssignmentQuestion)
.WithMany(aq => aq.ChildrenAssignmentQuestion)
.HasForeignKey(aq => aq.ParentAssignmentQuestionId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne(aq => aq.QuestionContext)
.WithMany(qc => qc.Questions)
.HasForeignKey(aq => aq.QuestionContextId)
.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