Compare commits

...

14 Commits

Author SHA1 Message Date
SpecialX
0d19ec6bb6 更新班级和学生相关功能
Some checks failed
TechAct / explore-gitea-actions (push) Failing after 6s
2025-09-12 11:31:50 +08:00
SpecialX
439c8a2421 feat: 添加学生提交系统功能
Some checks failed
TechAct / explore-gitea-actions (push) Failing after 30s
- 添加学生提交管理服务 (StudentSubmissionService, StudentSubmissionDetailService)
- 新增学生提交相关控制器 (StudentSubmissionController, StudentSubmissionDetailController)
- 添加学生提交数据传输对象 (StudentSubmissionDetailDto, StudentSubmissionSummaryDto)
- 新增学生提交相关页面组件 (StudentExamView, ExamDetailView, StudentCard等)
- 添加学生提交信息卡片组件 (SubmissionInfoCard, TeacherSubmissionInfoCard)
- 更新数据库迁移文件以支持提交系统
2025-09-09 15:42:31 +08:00
SpecialX
6a65281850 重构作业结构:优化实体模型、DTO映射和前端界面
Some checks failed
TechAct / explore-gitea-actions (push) Failing after 13s
- 重构AppMainStruct、AssignmentQuestion、Question等实体模型
- 更新相关DTO以匹配新的数据结构
- 优化前端页面布局和组件
- 添加全局信息和笔记功能相关代码
- 更新数据库迁移和程序配置
2025-09-04 15:43:33 +08:00
SpecialX
730b0ba04b Update ci.yaml
Some checks failed
TechAct / explore-gitea-actions (push) Failing after 6m0s
2025-08-31 11:35:41 +08:00
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
225 changed files with 24974 additions and 4358 deletions

View File

@@ -1,10 +1,10 @@
name: Tech name: TechAct
on: [push] # 当有新的push事件发生时触发此工作流程 on: [push] # 当有新的push事件发生时触发此工作流程
jobs: jobs:
explore-gitea-actions: explore-gitea-actions:
runs-on: Tech runs-on: TechAct
steps: steps:
- uses: actions/checkout@v4 # 使用actions/checkout来克隆您的仓库代码 - uses: actions/checkout@v4 # 使用actions/checkout来克隆您的仓库代码
- run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner." - run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner."

7
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": []
}

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,189 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
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
{
simple,
easy,
medium,
hard,
veryHard
}
public enum QuestionType : byte
{
Unknown = 0,
Spelling, // 拼写
Pronunciation, // 给带点字选择正确读音
WordFormation, // 组词
FillInTheBlanks, // 选词填空 / 补充词语
SentenceDictation, // 默写句子
SentenceRewriting, // 仿句 / 改写句子
ReadingComprehension, // 阅读理解
Composition // 作文
}
public enum SubjectAreaEnum : byte
{
[Display(Name = "未知", Description = "未知")]
Unknown = 0,
[Display(Name = "数学", Description = "数")]
Mathematics, // 数学
[Display(Name = "物理", Description = "物")]
Physics, // 物理
[Display(Name = "化学", Description = "化")]
Chemistry, // 化学
[Display(Name = "生物", Description = "生")]
Biology, // 生物
[Display(Name = "历史", Description = "史")]
History, // 历史
[Display(Name = "地理", Description = "地")]
Geography, // 地理
[Display(Name = "语文", Description = "语")]
Literature, // 语文/文学
[Display(Name = "英语", Description = "英")]
English, // 英语
[Display(Name = "计算机科学", Description = "计")]
ComputerScience // 计算机科学
}
public enum AssignmentStructType : byte
{
[Display(Name = "根节点", Description = "根")]
Root,
[Display(Name = "单个问题", Description = "问")]
Question,
[Display(Name = "问题组", Description = "组")]
Group,
[Display(Name = "结构", Description = "结")]
Struct,
[Display(Name = "子问题", Description = "子")]
SubQuestion,
[Display(Name = "选项", Description = "选")]
Option
}
public enum ExamType : byte
{
[Display(Name = "期中考试", Description = "中")]
MidtermExam,
[Display(Name = "期末考试", Description = "末")]
FinalExam,
[Display(Name = "月考", Description = "月")]
MonthlyExam,
[Display(Name = "周考", Description = "周")]
WeeklyExam,
[Display(Name = "平时测试", Description = "平")]
DailyTest,
[Display(Name = "AI测试", Description = "AI")]
AITest,
}
public enum SubmissionStatus
{
[Display(Name = "待提交/未开始", Description = "待")]
Pending,
[Display(Name = "已提交", Description = "提")]
Submitted,
[Display(Name = "已批改", Description = "批")]
Graded,
[Display(Name = "待重新提交", Description = "重")]
Resubmission,
[Display(Name = "迟交", Description = "迟")]
Late,
[Display(Name = "草稿", Description = "草")]
Draft,
}
public static class EnumExtensions
{
public static string GetDisplayName(this Enum enumValue)
{
var fieldInfo = enumValue.GetType().GetField(enumValue.ToString());
if (fieldInfo == null)
{
return enumValue.ToString();
}
var displayAttribute = fieldInfo.GetCustomAttribute<DisplayAttribute>();
if (displayAttribute != null)
{
return displayAttribute.Name;
}
return enumValue.ToString();
}
public static string GetShortName(this Enum enumValue)
{
var memberInfo = enumValue.GetType().GetMember(enumValue.ToString()).FirstOrDefault();
if (memberInfo != null)
{
var displayAttribute = memberInfo.GetCustomAttribute<DisplayAttribute>();
if (displayAttribute != null && !string.IsNullOrEmpty(displayAttribute.Description))
{
return displayAttribute.Description;
}
}
return enumValue.ToString();
}
}
}

View File

@@ -5,6 +5,7 @@ using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Entities.DTO;
namespace Entities.Contracts namespace Entities.Contracts
{ {
@@ -24,32 +25,46 @@ namespace Entities.Contracts
public string Description { get; set; } public string Description { get; set; }
[Column("subject_area")] [Column("subject_area")]
public string SubjectArea { get; set; } public SubjectAreaEnum SubjectArea { get; set; }
[Required]
[Column("exam_struct_id")]
public Guid ExamStructId { get; set; }
[Required] [Required]
[Column("due_date")] [Column("due_date")]
public DateTime DueDate { get; set; } public DateTime DueDate { get; set; }
[Column("total_points")] [Column("total_points")]
public float? TotalPoints { get; set; } public byte TotalQuestions { get; set; }
[Column("score")]
public float Score { get; set; }
public string Name { get; set; } = string.Empty;
public ExamType ExamType { get; set; } = ExamType.DailyTest;
[Column("created_by")] [Column("created_by")]
[ForeignKey("Creator")] public Guid CreatorId { get; set; }
public Guid CreatedBy { get; set; }
[Column("created_at")] [Column("created_at")]
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
[Column("updated_at")] [Column("updated_at")]
public DateTime UpdatedAt { get; set; } public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
[Column("deleted")] [Column("deleted")]
public bool IsDeleted { get; set; } public bool IsDeleted { get; set; } = false;
// Navigation Properties // Navigation Properties
[ForeignKey(nameof(CreatorId))]
public User Creator { get; set; } public User Creator { get; set; }
public ICollection<AssignmentClass> AssignmentClasses { get; set; } public ICollection<AssignmentClass> AssignmentClasses { get; set; }
public ICollection<AssignmentGroup> AssignmentGroups { get; set; }
[ForeignKey(nameof(ExamStructId))]
public AssignmentQuestion ExamStruct { get; set; }
public ICollection<AssignmentAttachment> AssignmentAttachments { get; set; } public ICollection<AssignmentAttachment> AssignmentAttachments { get; set; }
public ICollection<Submission> Submissions { get; set; } public ICollection<Submission> Submissions { get; set; }
@@ -58,9 +73,48 @@ namespace Entities.Contracts
Id = Guid.NewGuid(); Id = Guid.NewGuid();
Submissions = new HashSet<Submission>(); Submissions = new HashSet<Submission>();
AssignmentGroups = new HashSet<AssignmentGroup>();
AssignmentClasses = new HashSet<AssignmentClass>(); AssignmentClasses = new HashSet<AssignmentClass>();
AssignmentAttachments = new HashSet<AssignmentAttachment>(); AssignmentAttachments = new HashSet<AssignmentAttachment>();
} }
} }
public static class AssignmentExt
{
public static Submission ConvertToSubmission(this Assignment assignment, Guid studentId, Guid GraderId)
{
if (assignment == null) return new Submission();
var submission = new Submission();
submission.StudentId = studentId;
submission.SubmissionTime = DateTime.Now;
submission.Status = SubmissionStatus.Pending;
submission.GraderId = GraderId;
submission.AssignmentId = assignment.Id;
ConvertExamSturctToSubmissionDetails(assignment.ExamStruct, studentId, submission.SubmissionDetails);
return submission;
}
public static void ConvertExamSturctToSubmissionDetails(AssignmentQuestion examStruct, Guid studentId, ICollection<SubmissionDetail> submissions)
{
if (examStruct == null) return;
submissions.Add(new SubmissionDetail
{
StudentId = studentId,
AssignmentQuestionId = examStruct.Id,
IsCorrect = true,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now,
Status = SubmissionStatus.Pending,
});
examStruct.ChildrenAssignmentQuestion?.ToList().ForEach(s =>
{
ConvertExamSturctToSubmissionDetails(s, studentId, submissions);
});
}
}
} }

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.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Entities.DTO;
namespace Entities.Contracts namespace Entities.Contracts
{ {
@@ -17,22 +18,29 @@ namespace Entities.Contracts
public Guid Id { get; set; } public Guid Id { get; set; }
[Column("question_id")] [Column("question_id")]
public Guid? QuestionId { get; set; } // 设为可空 public Guid? QuestionId { get; set; }
// 当 IsGroup 为 true 时,此为 QuestionGroup 的外键 [Column("title")]
[Column("question_group_id")] // 新增一个外键列 [MaxLength(1024)]
public Guid? QuestionGroupId { get; set; } // 设为可空 public string? Title { get; set; }
[Required]
[Column("group_id")]
[ForeignKey("AssignmentGroup")]
public Guid AssignmentGroupId { get; set; }
[Column("description")]
public Guid? QuestionContextId { get; set; }
[Required] [Required]
[Column("question_number")] [Column("question_number")]
public byte QuestionNumber { get; set; } public byte Index { get; set; }
[Column("sequence")]
public string Sequence { get; set; } = string.Empty;
[Column("parent_question_group_id")]
public Guid? ParentAssignmentQuestionId { get; set; }
[Column("group_state")]
public AssignmentStructType StructType { get; set; } = AssignmentStructType.Question;
public QuestionType Type { get; set; } = QuestionType.Unknown;
[Column("created_at")] [Column("created_at")]
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
@@ -40,18 +48,24 @@ namespace Entities.Contracts
[Column("score")] [Column("score")]
public float? Score { get; set; } public float? Score { get; set; }
[Required] public bool BCorrect { get; set; }
[Column("bgroup")]
public bool IsGroup { get; set; }
[Column("deleted")] [Column("deleted")]
public bool IsDeleted { get; set; } public bool IsDeleted { get; set; }
public Question Question { get; set; } public Question? Question { get; set; }
public QuestionGroup QuestionGroup { get; set; } public Assignment? Assignment { get; set; }
[ForeignKey(nameof(QuestionContextId))]
public QuestionContext? QuestionContext { get; set; }
public ICollection<SubmissionDetail> SubmissionDetails { get; set; } public ICollection<SubmissionDetail> SubmissionDetails { get; set; }
public AssignmentGroup AssignmentGroup { get; set; }
[ForeignKey(nameof(ParentAssignmentQuestionId))]
public AssignmentQuestion? ParentAssignmentQuestion { get; set; }
public ICollection<AssignmentQuestion> ChildrenAssignmentQuestion { get; set; } = new List<AssignmentQuestion>();
public AssignmentQuestion() public AssignmentQuestion()
{ {

View File

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

View File

@@ -0,0 +1,24 @@
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("global")]
public class Global
{
[Key]
[Column("id")]
public Guid Id { get; set; } = Guid.NewGuid();
public SubjectAreaEnum Area { get; set; }
public string Info { get; set; } = string.Empty;
}
}

View File

@@ -16,36 +16,43 @@ namespace Entities.Contracts
public Guid Id { get; set; } public Guid Id { get; set; }
[Required] [Required]
[Column("question_text")] [Column("title")]
[MaxLength(65535)] [MaxLength(65535)]
public string QuestionText { get; set; } public string Title { get; set; }
[Column("answer")]
[MaxLength(65535)]
public string? Answer { get; set; }
[Required] [Required]
[Column("question_type")] [Column("type")]
[MaxLength(20)] [MaxLength(20)]
public QuestionType QuestionType { get; set; } public QuestionType Type { get; set; } = QuestionType.Unknown;
[Column("correct_answer")]
[MaxLength(65535)]
public string CorrectAnswer { get; set; }
[Column("question_group_id")]
public Guid? QuestionGroupId { get; set; }
[Column("difficulty_level")] [Column("difficulty_level")]
[MaxLength(10)] [MaxLength(10)]
public DifficultyLevel DifficultyLevel { get; set; } public DifficultyLevel DifficultyLevel { get; set; } = DifficultyLevel.easy;
[Column("subject_area")] [Column("subject_area")]
public SubjectAreaEnum SubjectArea { get; set; } public SubjectAreaEnum SubjectArea { get; set; } = SubjectAreaEnum.Unknown;
public string QType { get; set; } = string.Empty;
[Column("options")] [Column("options")]
public string? Options { get; set; } public string? Options { get; set; }
[Column("key_point")]
public Guid? KeyPointId { get; set; }
[Column("lesson")]
public Guid? LessonId { get; set; }
[Required] [Required]
[Column("created_by")] [Column("created_by")]
[ForeignKey("Creator")] public Guid CreatorId { get; set; }
public Guid CreatedBy { get; set; }
[Column("created_at")] [Column("created_at")]
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
@@ -56,13 +63,17 @@ namespace Entities.Contracts
[Column("deleted")] [Column("deleted")]
public bool IsDeleted { get; set; } public bool IsDeleted { get; set; }
[Column("valid_question")]
public bool ValidQuestion { get; set; }
// Navigation Properties // Navigation Properties
[ForeignKey(nameof(CreatorId))]
public User Creator { get; set; } public User Creator { get; set; }
public QuestionGroup QuestionGroup { get; set; }
public ICollection<AssignmentQuestion> AssignmentQuestions { get; set; } [ForeignKey(nameof(KeyPointId))]
public KeyPoint? KeyPoint { get; set; }
[ForeignKey(nameof(LessonId))]
public Lesson? Lesson { get; set; }
public ICollection<AssignmentQuestion>? AssignmentQuestions { get; set; }
public Question() public Question()
{ {
@@ -71,39 +82,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,20 +28,20 @@ namespace Entities.Contracts
[Required] [Required]
[Column("attempt_number")] [Column("attempt_number")]
public Guid AttemptNumber { get; set; } public byte AttemptNumber { get; set; }
[Column("submission_time")] [Column("submission_time")]
public DateTime SubmissionTime { get; set; } public DateTime SubmissionTime { get; set; }
[Column("overall_grade")] [Column("overall_grade")]
public float? OverallGrade { get; set; } public float OverallGrade { get; set; } = 0;
[Column("overall_feedback")] [Column("overall_feedback")]
public string OverallFeedback { get; set; } public string? OverallFeedback { get; set; }
[Column("graded_by")] [Column("graded_by")]
[ForeignKey("Grader")] [ForeignKey("Grader")]
public Guid? GradedBy { get; set; } public Guid? GraderId { get; set; }
[Column("graded_at")] [Column("graded_at")]
public DateTime? GradedAt { get; set; } public DateTime? GradedAt { get; set; }
@@ -49,6 +49,12 @@ namespace Entities.Contracts
[Column("deleted")] [Column("deleted")]
public bool IsDeleted { get; set; } public bool IsDeleted { get; set; }
public byte TotalQuesNum { get; set; }
public byte ErrorQuesNum { get; set; }
public byte TotalScore { get; set; }
[Required] [Required]
[Column("status")] [Column("status")]
public SubmissionStatus Status { get; set; } public SubmissionStatus Status { get; set; }
@@ -66,14 +72,4 @@ namespace Entities.Contracts
} }
} }
public enum SubmissionStatus
{
Pending, // 待提交/未开始
Submitted, // 已提交
Graded, // 已批改
Resubmission, // 待重新提交 (如果允许)
Late, // 迟交
Draft, // 草稿
// ... 添加你需要的其他状态
}
} }

View File

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

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

@@ -15,13 +15,15 @@ namespace Entities.Contracts
public string? RefreshToken { get; set; } public string? RefreshToken { get; set; }
public DateTime? RefreshTokenExpiryTime { get; set; } public DateTime? RefreshTokenExpiryTime { get; set; }
public string? Address { get; set; } public string? Address { get; set; }
public string? DisplayName { get; set; } public string? DisplayName { get; set; }
public SubjectAreaEnum SubjectArea { get; set; } = SubjectAreaEnum.Unknown;
[Column("deleted")] [Column("deleted")]
public bool IsDeleted { get; set; } public bool IsDeleted { get; set; }
[InverseProperty(nameof(ClassTeacher.Teacher))]
public ICollection<ClassTeacher> TaughtClassesLink { get; set; } public ICollection<ClassTeacher> TaughtClassesLink { get; set; }
[InverseProperty(nameof(ClassStudent.Student))]
public ICollection<ClassStudent> EnrolledClassesLink { get; set; } public ICollection<ClassStudent> EnrolledClassesLink { get; set; }
public ICollection<Question> CreatedQuestions { get; set; } public ICollection<Question> CreatedQuestions { get; set; }

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,33 @@
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 QuestionType Type { get; set; } = QuestionType.Unknown;
public string QType { get; set; } = string.Empty;
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)
{
}
}
}
}

23
Entities/DTO/GlobalDto.cs Normal file
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 GlobalDto
{
public SubjectAreaEnum SubjectArea { get; set; } = SubjectAreaEnum.Unknown;
public string Data { get; set; } = string.Empty;
}
public class QuestionDisplayTypeData
{
public string Color { get; set; }
public string DisplayName { 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 QuestionContextDto
{
public Guid Id { get; set; } = Guid.Empty;
public string Description { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,49 @@
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 QType { get; set; } = string.Empty;
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,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 StudentDto
{
public Guid Id { get; set; }
public string? DisplayName { get; set; }
public UInt32 ErrorQuestionNum { get; set; }
public Dictionary<string, UInt32> ErrorQuestionTypes { get; set; } = new Dictionary<string, UInt32>();
public Dictionary<SubjectAreaEnum, UInt32> SubjectAreaErrorQuestionDis { get; set; } = new Dictionary<SubjectAreaEnum, UInt32>();
public Dictionary<byte, UInt32> LessonErrorDis { get; set; } = new Dictionary<byte, UInt32>();
public float Score { get; set; }
}
}

View File

@@ -0,0 +1,41 @@
using Entities.Contracts;
using System;
using System.Collections.Generic;
namespace Entities.DTO
{
public class StudentSubmissionDetailDto
{
// 基本信息
public Guid Id { get; set; }
public Guid AssignmentId { get; set; }
public Guid StudentId { get; set; }
public DateTime SubmissionTime { get; set; }
public float OverallGrade { get; set; }
public string OverallFeedback { get; set; } = string.Empty;
public SubmissionStatus Status { get; set; }
// Assignment信息
public AssignmentDto Assignment { get; set; } = new AssignmentDto();
// 错误分析
public Dictionary<string, int> ErrorTypeDistribution { get; set; } = new Dictionary<string, int>();
public Dictionary<string, float> ErrorTypeScoreDistribution { get; set; } = new Dictionary<string, float>();
// 成绩统计
public int TotalRank { get; set; }
public List<float> AllScores { get; set; } = new List<float>();
public float AverageScore { get; set; }
public float ClassAverageScore { get; set; }
// 课文分布
public Dictionary<string, int> LessonErrorDistribution { get; set; } = new Dictionary<string, int>();
public Dictionary<string, int> KeyPointErrorDistribution { get; set; } = new Dictionary<string, int>();
// 基础统计
public int TotalQuestions { get; set; }
public int CorrectCount { get; set; }
public int ErrorCount { get; set; }
public float AccuracyRate { get; set; }
}
}

View File

@@ -0,0 +1,22 @@
using System;
namespace Entities.DTO
{
public class StudentSubmissionSummaryDto
{
public Guid Id { get; set; }
public string AssignmentName { get; set; }
public int ErrorCount { get; set; }
public DateTime CreatedDate { get; set; }
public float Score { get; set; }
public int TotalQuestions { get; set; }
public string StudentName { get; set; }
public string Status { get; set; }
}
public class StudentSubmissionSummaryResponseDto
{
public List<StudentSubmissionSummaryDto> Submissions { get; set; }
public int TotalCount { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
using Entities.Contracts;
namespace Entities.DTO
{
public class SubjectTypeMetadataDto
{
public SubjectAreaEnum SubjectArea { get; set; } = SubjectAreaEnum.Unknown;
//public Dictionary<string, (string Color, string DisplayName)> Data = new Dictionary<string, (string Color, string DisplayName)>();
public string Data = string.Empty;
}
}

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 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 Entities.DTO;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Text.Json; using System.Text.Json;
using Entities.Contracts;
using Microsoft.Extensions.Options;
using AutoMapper;
namespace TechHelper.Client.Exam namespace TechHelper.Client.Exam
{ {
public static class ExamPaperExtensions public static class AssignmentExtensions
{ {
public static ExamDto ConvertToExamDTO(this ExamPaper examPaper)
public static List<string> ParseOptionsFromText(this string optionsText)
{ {
ExamDto dto = new ExamDto(); return optionsText.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None)
.Where(line => !string.IsNullOrWhiteSpace(line)).ToList();
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;
} }
private static void ParseMajorQuestionGroup(MajorQuestionGroup qg, QuestionGroupDto qgd, bool isParentGroupValidChain) public static void SeqIndex(this AssignmentDto dto)
{ {
qgd.Title = qg.Title; dto.ExamStruct.SeqQGroupIndex();
qgd.Score = (int)qg.Score;
qgd.Descript = qg.Descript;
qgd.ValidQuestionGroup = !string.IsNullOrEmpty(qg.Descript) && !isParentGroupValidChain;
bool nextIsParentGroupValidChain = qgd.ValidQuestionGroup || isParentGroupValidChain;
if (qg.SubQuestionGroups != null)
{
qg.SubQuestionGroups.ForEach(sqg =>
{
var sqgd = new QuestionGroupDto();
sqgd.Index = (byte)qg.SubQuestionGroups.IndexOf(sqg);
ParseMajorQuestionGroup(sqg, sqgd, nextIsParentGroupValidChain);
qgd.SubQuestionGroups.Add(sqgd);
});
}
// 处理 MajorQuestionGroup 下的 SubQuestions
if (qg.SubQuestions != null)
{
qg.SubQuestions.ForEach(sq =>
{
// 如果 MajorQuestionGroup 下的 Question 包含子问题,则转为 QuestionGroupDto
if (sq.SubQuestions != null && sq.SubQuestions.Any())
{
var subQgd = new QuestionGroupDto
{
Title = sq.Stem,
Index = (byte)qg.SubQuestions.IndexOf(sq),
Score = (int)sq.Score,
Descript = "" // 默认为空
};
// 判断当前组是否有效:如果有描述,并且其父级链中没有任何一个组是有效组,则当前组有效
subQgd.ValidQuestionGroup = !string.IsNullOrEmpty(subQgd.Descript) && !nextIsParentGroupValidChain;
ParseQuestionWithSubQuestions(sq, subQgd, subQgd.ValidQuestionGroup || nextIsParentGroupValidChain);
qgd.SubQuestionGroups.Add(subQgd);
}
else // 如果 MajorQuestionGroup 下的 Question 没有子问题,则转为 SubQuestionDto
{
var subQd = new SubQuestionDto();
// 只有当所有父组(包括当前组)都不是有效组时,这个题目才有效
ParseSingleQuestion(sq, subQd, !nextIsParentGroupValidChain);
subQd.Index = (byte)qg.SubQuestions.IndexOf(sq);
qgd.SubQuestions.Add(subQd);
}
});
}
}
// 解析包含子问题的 Question将其转换为 QuestionGroupDto
// isParentGroupValidChain 参数表示从顶层到当前组的任一父组是否已经是“有效组”
private static void ParseQuestionWithSubQuestions(Question question, QuestionGroupDto qgd, bool isParentGroupValidChain)
{
qgd.Title = question.Stem;
qgd.Score = (int)question.Score;
qgd.Descript = ""; // 默认为空
// 判断当前组是否有效:如果有描述,并且其父级链中没有任何一个组是有效组,则当前组有效
qgd.ValidQuestionGroup = !string.IsNullOrEmpty(qgd.Descript) && !isParentGroupValidChain;
// 更新传递给子项的 isParentGroupValidChain 状态
bool nextIsParentGroupValidChain = qgd.ValidQuestionGroup || isParentGroupValidChain;
if (question.SubQuestions != null)
{
question.SubQuestions.ForEach(subQ =>
{
// 如果子问题本身还有子问题(多层嵌套),则继续创建 QuestionGroupDto
if (subQ.SubQuestions != null && subQ.SubQuestions.Any())
{
var nestedQgd = new QuestionGroupDto
{
Title = subQ.Stem,
Score = (int)subQ.Score,
Descript = "" // 默认为空
};
// 判断当前组是否有效:如果有描述,并且其父级链中没有任何一个组是有效组,则当前组有效
nestedQgd.ValidQuestionGroup = !string.IsNullOrEmpty(nestedQgd.Descript) && !nextIsParentGroupValidChain;
ParseQuestionWithSubQuestions(subQ, nestedQgd, nestedQgd.ValidQuestionGroup || nextIsParentGroupValidChain);
qgd.SubQuestionGroups.Add(nestedQgd);
}
else // 如果子问题没有子问题,则直接创建 SubQuestionDto
{
var subQd = new SubQuestionDto();
// 只有当所有父组(包括当前组)都不是有效组时,这个题目才有效
ParseSingleQuestion(subQ, subQd, !nextIsParentGroupValidChain);
qgd.SubQuestions.Add(subQd);
}
});
}
}
// 解析单个 Question (没有子问题) 为 SubQuestionDto
private static void ParseSingleQuestion(Question question, SubQuestionDto subQd, bool validQ)
{
subQd.Stem = question.Stem;
subQd.Score = (int)question.Score;
subQd.ValidQuestion = validQ; // 根据传入的 validQ 确定是否是“有效题目”
subQd.SampleAnswer = question.SampleAnswer;
subQd.QuestionType = question.QuestionType;
// 注意DifficultyLevel 在本地 Question 中没有,如果服务器需要,可能需要补充默认值或从其他地方获取
// subQd.DifficultyLevel = ...;
if (question.Options != null)
{
question.Options.ForEach(o =>
{
subQd.Options.Add(new OptionDto { Value = o.Label + o.Text });
});
}
} }
public static void SeqIndex(this ExamDto dto) public static void SeqQGroupIndex(this AssignmentQuestionDto dto)
{ {
dto.QuestionGroups.SeqQGroupIndex();
}
foreach (var sqg in dto.ChildrenAssignmentQuestion)
public static void SeqQGroupIndex(this QuestionGroupDto dto)
{
dto.SubQuestions?.ForEach(sq =>
{ {
sq.Index = (byte)dto.SubQuestions.IndexOf(sq); sqg.Index = (byte)(dto.ChildrenAssignmentQuestion.IndexOf(sqg) + 1);
});
dto.SubQuestionGroups?.ForEach(sqg =>
{
sqg.Index = (byte)dto.SubQuestionGroups.IndexOf(sqg);
sqg.SeqQGroupIndex(); sqg.SeqQGroupIndex();
});
}
}
public static string SerializeExamDto(this ExamDto dto)
{
// 配置序列化选项(可选)
var options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
return JsonSerializer.Serialize(dto, options);
}
public static ExamDto DeserializeExamDto(string jsonString)
{
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
return JsonSerializer.Deserialize<ExamDto>(jsonString, options);
} }
} }

View File

@@ -1,19 +1,25 @@
using Entities.DTO; using Entities.Contracts; // 假设这些实体合约仍然是必需的
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Text;
namespace TechHelper.Client.Exam namespace TechHelper.Client.Exam
{ {
// --- 新增错误处理相关类 --- public enum ParseErrorType
{
Validation = 1,
DataParsing = 2,
Structural = 3,
RegexMatchIssue = 4,
UnexpectedError = 5
}
public class ParseError public class ParseError
{ {
public ParseErrorType Type { get; } public ParseErrorType Type { get; }
public string Message { get; } public string Message { get; }
public int? Index { get; } // 错误发生的文本索引或匹配项索引 public int? Index { get; }
public string MatchedText { get; } // 如果与某个匹配项相关,记录其文本 public string MatchedText { get; }
public Exception InnerException { get; } // 捕获到的原始异常 public Exception InnerException { get; }
public ParseError(ParseErrorType type, string message, int? index = null, string matchedText = null, Exception innerException = null) public ParseError(ParseErrorType type, string message, int? index = null, string matchedText = null, Exception innerException = null)
{ {
@@ -26,7 +32,7 @@ namespace TechHelper.Client.Exam
public override string ToString() public override string ToString()
{ {
var sb = new System.Text.StringBuilder(); var sb = new StringBuilder();
sb.Append($"[{Type}] {Message}"); sb.Append($"[{Type}] {Message}");
if (Index.HasValue) sb.Append($" (Index: {Index.Value})"); if (Index.HasValue) sb.Append($" (Index: {Index.Value})");
if (!string.IsNullOrEmpty(MatchedText)) sb.Append($" (MatchedText: '{MatchedText}')"); if (!string.IsNullOrEmpty(MatchedText)) sb.Append($" (MatchedText: '{MatchedText}')");
@@ -35,46 +41,34 @@ namespace TechHelper.Client.Exam
} }
} }
public enum ParseErrorType public class AssignmentEx
{ {
Validation = 1, // 输入验证失败 public string Title { get; set; } = "Title";
DataParsing = 2, // 数据解析失败(如数字转换) public string Description { get; set; } = "Description";
Structural = 3, // 结构性问题(如选项没有对应的问题) public SubjectAreaEnum SubjectArea { get; set; } = SubjectAreaEnum.Unknown;
RegexMatchIssue = 4, // 正则表达式匹配结果不符合预期 public AssignmentQuestionEx ExamStruct { get; set; } = new AssignmentQuestionEx();
UnexpectedError = 5 // 未预料到的通用错误
}
public class ExamPaper
{
public string AssignmentTitle { get; set; } = "未识别试卷标题";
public string Description { get; set; } = "未识别试卷描述";
public string SubjectArea { get; set; } = "试卷类别";
public List<MajorQuestionGroup> QuestionGroups { get; set; } = new List<MajorQuestionGroup>();
public List<Question> TopLevelQuestions { get; set; } = new List<Question>();
public List<ParseError> Errors { get; set; } = new List<ParseError>(); public List<ParseError> Errors { get; set; } = new List<ParseError>();
} }
public class MajorQuestionGroup // 试题的包裹器, 或者 单独作为一个试题结构存在
public class AssignmentQuestionEx
{ {
public string Title { get; set; } = string.Empty; public string Title { get; set; } = string.Empty;
public string Descript { get; set; } = string.Empty; public string Description { get; set; } = string.Empty;
public byte Index { get; set; } = 0;
public float Score { get; set; } public float Score { get; set; }
public List<MajorQuestionGroup> SubQuestionGroups { get; set; } = new List<MajorQuestionGroup>(); public string Sequence { get; set; } = string.Empty;
public List<Question> SubQuestions { get; set; } = new List<Question>(); public QuestionEx? Question { get; set; }
public AssignmentStructType Type { get; set; }
public List<AssignmentQuestionEx> ChildrenAssignmentQuestion { get; set; } = new List<AssignmentQuestionEx>();
public int Priority { get; set; } public int Priority { get; set; }
} }
public class Question public class QuestionEx
{ {
public string Number { get; set; } = string.Empty; public string Title { get; set; } = string.Empty;
public string Stem { get; set; } = string.Empty; public string Answer { get; set; } = string.Empty;
public float Score { get; set; }
public List<Option> Options { get; set; } = new List<Option>(); public List<Option> Options { get; set; } = new List<Option>();
public List<Question> SubQuestions { get; set; } = new List<Question>();
public string SampleAnswer { get; set; } = string.Empty;
public string QuestionType { get; set; } = string.Empty;
public int Priority { get; set; }
} }
public class Option public class Option
@@ -89,155 +83,116 @@ namespace TechHelper.Client.Exam
/// </summary> /// </summary>
public class RegexPatternConfig public class RegexPatternConfig
{ {
public string Pattern { get; set; } // 正则表达式字符串 public string Pattern { get; set; }
public int Priority { get; set; } // 优先级,数字越小优先级越高 public int Priority { get; set; }
public Regex Regex { get; private set; } // 编译后的Regex对象用于性能优化 public AssignmentStructType Type { get; set; }
public Regex Regex { get; private set; }
public RegexPatternConfig(string pattern, int priority) public RegexPatternConfig(string pattern, int priority, AssignmentStructType type = AssignmentStructType.Question)
{ {
Pattern = pattern; Pattern = pattern;
Priority = priority; Priority = priority;
Regex = new Regex(pattern, RegexOptions.Multiline | RegexOptions.Compiled); // 多行模式,编译以提高性能 Type = type;
Regex = new Regex(pattern, RegexOptions.Multiline | RegexOptions.Compiled);
} }
} }
public enum ExamParserEnum
{
MajorQuestionGroupPatterns = 0,
QuestionPatterns,
OptionPatterns
}
/// <summary> /// <summary>
/// 试卷解析的配置类,包含所有正则表达式 /// 试卷解析的配置类,包含所有正则表达式
/// </summary> /// </summary>
public class ExamParserConfig public class ExamParserConfig
{ {
public List<RegexPatternConfig> MajorQuestionGroupPatterns { get; set; } = new List<RegexPatternConfig>();
public List<RegexPatternConfig> QuestionPatterns { get; set; } = new List<RegexPatternConfig>(); public List<RegexPatternConfig> QuestionPatterns { get; set; } = new List<RegexPatternConfig>();
public List<RegexPatternConfig> OptionPatterns { get; set; } = new List<RegexPatternConfig>(); public List<RegexPatternConfig> OptionPatterns { get; set; } = new List<RegexPatternConfig>();
public Regex ScoreRegex { get; private set; } // 独立的得分正则表达式
public ExamParserConfig() public ExamParserConfig()
{ {
MajorQuestionGroupPatterns.Add(new RegexPatternConfig(@"^([一二三四五六七八九十]+)[、\.]\s*(.+?)(?:\s*\(((\d+(?:\.\d+)?))\s*分\))?\s*$", 1)); // 题目/题组模式:只匹配行开头,并按优先级区分
MajorQuestionGroupPatterns.Add(new RegexPatternConfig(@"^\(([一二三四五六七八九十]{1,2}|十[一二三四五六七八九])\)\s*(.+?)(?:\s*\(((\d+(?:\.\d+)?))\s*分\))?\s*$", 2)); // Group 1: 编号部分
// Group 2: 题目/题组标题内容
// 例如:一. 这是大题一
QuestionPatterns.Add(new RegexPatternConfig(@"^([一二三四五六七八九十]+)[.\、]\s*(.+)", 1, AssignmentStructType.Struct));
// 例如:(一) 这是第一子题组
QuestionPatterns.Add(new RegexPatternConfig(@"^\(([一二三四五六七八九十]{1,2}|十[一二三四五六七八九])\)\s*(.+)", 2, AssignmentStructType.Group));
// 例如1. 这是第一道题目 或 1 这是第一道题目
QuestionPatterns.Add(new RegexPatternConfig(@"^(\d+)\.?\s*(.+)", 3, AssignmentStructType.Question));
// 例如:(1). 这是小问一 或 (1) 这是小问一
QuestionPatterns.Add(new RegexPatternConfig(@"^\((\d+)\)\.?\s*(.+)", 4, AssignmentStructType.Question));
// 例如:① 这是另一种小问 或 ①. 这是另一种小问 (如果 ① 后面会跟点,这个更通用)
// 如果 ① 后面通常没有点,但您希望它也能匹配,则保留原样或根据实际情况调整
QuestionPatterns.Add(new RegexPatternConfig(@"^[①②③④⑤⑥⑦⑧⑨⑩]+\.?\s*(.+)", 5, AssignmentStructType.Question));
// 模式 1: "1. 这是一个题目 (5分)" 或 "1. 这是一个题目"
QuestionPatterns.Add(new RegexPatternConfig(@"^(\d+)\.\s*(.+?)(?:\s*\(((\d+(?:\.\d+)?))\s*分\))?\s*$", 1));
// 模式 2: "(1) 这是一个子题目 (3分)" 或 "(1) 这是一个子题目" // 选项模式 (保持不变,使用 AssignmentStructType.Option 区分)
QuestionPatterns.Add(new RegexPatternConfig(@"^\((\d+)\)\s*(.+?)(?:\s*\(((\d+(?:\.\d+)?))\s*分\))?\s*$", 2)); OptionPatterns.Add(new RegexPatternConfig(@"([A-Z]\.)\s*(.*?)(?=[A-Z]\.|$)", 1, AssignmentStructType.Option));
OptionPatterns.Add(new RegexPatternConfig(@"([a-z]\.)\s*(.*?)(?=[a-z]\.|$)", 2, AssignmentStructType.Option));
// 模式 3: "① 这是一个更深层次的子题目 (2分)" 或 "① 这是一个更深层次的子题目"
QuestionPatterns.Add(new RegexPatternConfig(@"^[①②③④⑤⑥⑦⑧⑨⑩]+\s*(.+?)(?:\s*\(((\d+(?:\.\d+)?))\s*分\))?\s*$", 3));
OptionPatterns.Add(new RegexPatternConfig(@"([A-Z]\.)\s*(.*?)(?=[A-Z]\.|$)", 1)); // 大写字母选项
OptionPatterns.Add(new RegexPatternConfig(@"([a-z]\.)\s*(.*?)(?=[a-z]\.|$)", 1)); // 小写字母选项
// 独立的得分正则表达式:匹配行末尾的 "(X分)" 格式
// Group 1: 捕获分数(如 "10" 或 "0.5"
ScoreRegex = new Regex(@"(?:\s*\(((\d+(?:\.\d+)?))\s*分\)\s*$)", RegexOptions.Multiline | RegexOptions.Compiled);
} }
} }
public class PotentialMatch public class PotentialMatch
{ {
public int StartIndex { get; set; } public int StartIndex { get; set; }
public int EndIndex { get; set; } // 匹配到的结构在原始文本中的结束位置 public int EndIndex { get; set; }
public string MatchedText { get; set; } // 匹配到的完整行或段落 public string MatchedText { get; set; }
public Match RegexMatch { get; set; } // 原始的Regex.Match对象方便获取捕获组 public Match RegexMatch { get; set; }
public RegexPatternConfig PatternConfig { get; set; } // 匹配到的模式配置 public RegexPatternConfig PatternConfig { get; set; }
public MatchType Type { get; set; } // 枚举MajorQuestionGroup, Question, Option, etc.
} }
public enum MatchType
{
MajorQuestionGroup,
Question,
Option,
Other // 如果有其他需要识别的类型
}
/// <summary>
/// 负责扫描原始文本,收集所有潜在的匹配项(题组、题目、选项)。
/// 它只进行匹配,不进行结构化归属。
/// </summary>
public class ExamDocumentScanner public class ExamDocumentScanner
{ {
private readonly ExamParserConfig _config; private readonly ExamParserConfig _config;
public ExamDocumentScanner(ExamParserConfig config) public ExamDocumentScanner(ExamParserConfig config)
{ {
_config = config ?? throw new ArgumentNullException(nameof(config)); // 确保配置不为空 _config = config ?? throw new ArgumentNullException(nameof(config));
} }
/// <summary> public List<PotentialMatch> Scan(string text, List<ParseError> errors)
/// 扫描给定的文本,返回所有潜在的匹配项,并按起始位置排序。
/// </summary>
/// <param name="text">要扫描的文本</param>
/// <returns>所有匹配到的 PotentialMatch 列表</returns>
public List<PotentialMatch> Scan(string text)
{ {
if (string.IsNullOrEmpty(text)) if (string.IsNullOrEmpty(text))
{ {
return new List<PotentialMatch>(); // 对于空文本,直接返回空列表 return new List<PotentialMatch>();
} }
var allPotentialMatches = new List<PotentialMatch>(); var allPotentialMatches = new List<PotentialMatch>();
var allPatternConfigs = new List<RegexPatternConfig>();
allPatternConfigs.AddRange(_config.QuestionPatterns);
allPatternConfigs.AddRange(_config.OptionPatterns);
// 扫描所有题组模式 foreach (var patternConfig in allPatternConfigs)
foreach (var patternConfig in _config.MajorQuestionGroupPatterns)
{ {
foreach (Match match in patternConfig.Regex.Matches(text)) try
{ {
allPotentialMatches.Add(new PotentialMatch foreach (Match match in patternConfig.Regex.Matches(text))
{ {
StartIndex = match.Index, allPotentialMatches.Add(new PotentialMatch
EndIndex = match.Index + match.Length, {
MatchedText = match.Value, StartIndex = match.Index,
RegexMatch = match, EndIndex = match.Index + match.Length,
PatternConfig = patternConfig, MatchedText = match.Value,
Type = MatchType.MajorQuestionGroup 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(); return allPotentialMatches.OrderBy(pm => pm.StartIndex).ToList();
} }
} }
@@ -251,18 +206,8 @@ namespace TechHelper.Client.Exam
_config = config ?? throw new ArgumentNullException(nameof(config), "ExamParserConfig cannot be null."); _config = config ?? throw new ArgumentNullException(nameof(config), "ExamParserConfig cannot be null.");
} }
/// <summary> public AssignmentEx BuildExam(string fullExamText, List<PotentialMatch> allPotentialMatches)
/// Builds the ExamPaper structure from raw text and potential matches.
/// Collects and returns parsing errors encountered during the process.
/// </summary>
/// <param name="fullExamText">The complete text of the exam paper.</param>
/// <param name="allPotentialMatches">A list of all identified potential matches.</param>
/// <returns>An ExamPaper object containing the parsed structure and a list of errors.</returns>
/// <exception cref="ArgumentException">Thrown if fullExamText is null or empty.</exception>
/// <exception cref="ArgumentNullException">Thrown if allPotentialMatches is null.</exception>
public ExamPaper BuildExamPaper(string fullExamText, List<PotentialMatch> allPotentialMatches)
{ {
// 核心输入验证仍然是必要的,因为这些错误是无法恢复的
if (string.IsNullOrWhiteSpace(fullExamText)) if (string.IsNullOrWhiteSpace(fullExamText))
{ {
throw new ArgumentException("Full exam text cannot be null or empty.", nameof(fullExamText)); throw new ArgumentException("Full exam text cannot be null or empty.", nameof(fullExamText));
@@ -272,269 +217,79 @@ namespace TechHelper.Client.Exam
throw new ArgumentNullException(nameof(allPotentialMatches), "Potential matches list cannot be null."); throw new ArgumentNullException(nameof(allPotentialMatches), "Potential matches list cannot be null.");
} }
var examPaper = new ExamPaper(); // ExamPaper 现在包含一个 Errors 列表 var assignment = new AssignmentEx();
// 尝试获取试卷标题
try try
{ {
examPaper.AssignmentTitle = GetExamTitle(fullExamText); assignment.Title = GetExamTitle(fullExamText);
} }
catch (Exception ex) catch (Exception ex)
{ {
// 如果获取标题失败,记录错误而不是抛出致命异常 assignment.Errors.Add(new ParseError(ParseErrorType.UnexpectedError, "Failed to extract exam title.", innerException: ex));
examPaper.Errors.Add(new ParseError(ParseErrorType.UnexpectedError, "Failed to extract exam title.", innerException: ex)); assignment.Title = "未识别试卷标题";
examPaper.AssignmentTitle = "未识别试卷标题"; // 提供默认值
} }
var majorQGStack = new Stack<MajorQuestionGroup>(); var assignmentQuestionStack = new Stack<AssignmentQuestionEx>();
MajorQuestionGroup currentMajorQG = null; var rootAssignmentQuestion = new AssignmentQuestionEx { Type = AssignmentStructType.Struct, Priority = 0, Title = "Root Exam Structure" };
assignmentQuestionStack.Push(rootAssignmentQuestion);
var questionStack = new Stack<Question>(); assignment.ExamStruct = rootAssignmentQuestion;
Question currentQuestion = null;
int currentContentStart = 0; int currentContentStart = 0;
// 处理试卷开头的描述性文本
if (allPotentialMatches.Any() && allPotentialMatches[0].StartIndex > 0) if (allPotentialMatches.Any() && allPotentialMatches[0].StartIndex > 0)
{ {
string introText = fullExamText.Substring(0, allPotentialMatches[0].StartIndex).Trim(); string introText = fullExamText.Substring(0, allPotentialMatches[0].StartIndex).Trim();
if (!string.IsNullOrWhiteSpace(introText)) if (!string.IsNullOrWhiteSpace(introText))
{ {
examPaper.Description += (string.IsNullOrWhiteSpace(examPaper.Description) ? "" : "\n") + introText; assignment.Description += (string.IsNullOrWhiteSpace(assignment.Description) ? "" : "\n") + introText;
} }
} }
currentContentStart = allPotentialMatches.Any() ? allPotentialMatches[0].StartIndex : 0;
currentContentStart = allPotentialMatches[0].StartIndex;
for (int i = 0; i < allPotentialMatches.Count; i++) for (int i = 0; i < allPotentialMatches.Count; i++)
{ {
var pm = allPotentialMatches[i]; var pm = allPotentialMatches[i];
try try
{ {
// **数据验证:不再抛出,而是记录错误** if (!IsValidPotentialMatch(pm, i, fullExamText.Length, currentContentStart, assignment.Errors))
if (pm.StartIndex < currentContentStart || pm.EndIndex > fullExamText.Length || pm.StartIndex > pm.EndIndex)
{ {
examPaper.Errors.Add(new ParseError(ParseErrorType.Validation, currentContentStart = Math.Max(currentContentStart, pm.EndIndex);
$"PotentialMatch at index {i} has invalid start/end indices. Start: {pm.StartIndex}, End: {pm.EndIndex}, CurrentContentStart: {currentContentStart}, FullTextLength: {fullExamText.Length}", continue;
index: i, matchedText: pm.MatchedText));
currentContentStart = Math.Max(currentContentStart, pm.EndIndex); // 尝试跳过这个损坏的匹配项
continue; // 跳过当前循环迭代,处理下一个匹配项
}
if (pm.RegexMatch == null || pm.PatternConfig == null)
{
examPaper.Errors.Add(new ParseError(ParseErrorType.Validation,
$"PotentialMatch at index {i} is missing RegexMatch or PatternConfig.",
index: i, matchedText: pm.MatchedText));
currentContentStart = Math.Max(currentContentStart, pm.EndIndex); // 尝试跳过这个损坏的匹配项
continue; // 跳过当前循环迭代,处理下一个匹配项
} }
string precedingText = fullExamText.Substring(currentContentStart, pm.StartIndex - currentContentStart).Trim(); string precedingText = fullExamText.Substring(currentContentStart, pm.StartIndex - currentContentStart).Trim();
if (!string.IsNullOrWhiteSpace(precedingText)) if (!string.IsNullOrWhiteSpace(precedingText))
{ {
if (currentQuestion != null) if (assignmentQuestionStack.Peek().Question != null)
{ {
// 将 examPaper.Errors 传递给 ProcessQuestionContent 收集错误 ProcessQuestionContent(assignmentQuestionStack.Peek(), precedingText, assignment.Errors);
ProcessQuestionContent(currentQuestion, precedingText,
GetSubMatchesForRange(allPotentialMatches, currentContentStart, pm.StartIndex, examPaper.Errors),
examPaper.Errors);
}
else if (currentMajorQG != null)
{
currentMajorQG.Descript += (string.IsNullOrWhiteSpace(currentMajorQG.Descript) ? "" : "\n") + precedingText;
} }
else else
{ {
examPaper.Description += (string.IsNullOrWhiteSpace(examPaper.Description) ? "" : "\n") + precedingText; assignment.Description += (string.IsNullOrWhiteSpace(assignment.Description) ? "" : "\n") + precedingText;
} }
} }
if (pm.Type == MatchType.MajorQuestionGroup) if (pm.PatternConfig.Type == AssignmentStructType.Option)
{ {
// 对 MajorQuestionGroup 的处理 HandleOptionMatch(pm, i, assignmentQuestionStack.Peek(), assignment.Errors);
try
{
while (majorQGStack.Any() && pm.PatternConfig.Priority <= majorQGStack.Peek().Priority)
{
majorQGStack.Pop();
}
// RegexMatch Groups 验证:不再抛出,记录错误
if (pm.RegexMatch.Groups.Count < 2 || string.IsNullOrWhiteSpace(pm.RegexMatch.Groups[1].Value))
{
examPaper.Errors.Add(new ParseError(ParseErrorType.RegexMatchIssue,
$"MajorQuestionGroup match at index {i} does not have enough regex groups or a valid title group (Group 1). Skipping this group.",
index: i, matchedText: pm.MatchedText));
currentContentStart = pm.EndIndex; // 继续,尝试跳过此项
continue;
}
float score = 0;
// 使用 float.TryParse 避免异常
if (pm.RegexMatch.Groups.Count > 3 && pm.RegexMatch.Groups[4].Success) // 假设纯数字分数是 Group 4
{
if (!float.TryParse(pm.RegexMatch.Groups[4].Value, out score))
{
examPaper.Errors.Add(new ParseError(ParseErrorType.DataParsing,
$"Failed to parse score '{pm.RegexMatch.Groups[4].Value}' for MajorQuestionGroup at index {i}. Defaulting to 0.",
index: i, matchedText: pm.MatchedText));
}
}
MajorQuestionGroup newMajorQG = new MajorQuestionGroup
{
Title = pm.RegexMatch.Groups[2].Value.Trim(), // 标题是 Group 2
Score = score,
Priority = pm.PatternConfig.Priority,
};
if (majorQGStack.Any())
{
majorQGStack.Peek().SubQuestionGroups.Add(newMajorQG);
}
else
{
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;
}
} }
else if (pm.Type == MatchType.Question) else
{ {
// 对 Question 的处理 HandleQuestionGroupMatch(pm, i, assignmentQuestionStack, assignment.Errors);
try
{
while (questionStack.Any() && pm.PatternConfig.Priority <= questionStack.Peek().Priority)
{
questionStack.Pop();
}
// RegexMatch Groups 验证
if (pm.RegexMatch.Groups.Count < 3 || string.IsNullOrWhiteSpace(pm.RegexMatch.Groups[1].Value) || string.IsNullOrWhiteSpace(pm.RegexMatch.Groups[2].Value))
{
examPaper.Errors.Add(new ParseError(ParseErrorType.RegexMatchIssue,
$"Question match at index {i} does not have enough regex groups or valid number/text groups (Group 1/2). Skipping this question.",
index: i, matchedText: pm.MatchedText));
currentContentStart = pm.EndIndex; // 尝试跳过此项
continue;
}
float score = 0;
// 使用 float.TryParse 避免异常
if (pm.RegexMatch.Groups.Count > 4 && pm.RegexMatch.Groups[4].Success) // 假设纯数字分数是 Group 4
{
if (!float.TryParse(pm.RegexMatch.Groups[4].Value, out score))
{
examPaper.Errors.Add(new ParseError(ParseErrorType.DataParsing,
$"Failed to parse score '{pm.RegexMatch.Groups[4].Value}' for Question at index {i}. Defaulting to 0.",
index: i, matchedText: pm.MatchedText));
}
}
Question newQuestion = new Question
{
Number = pm.RegexMatch.Groups[1].Value.Trim(),
Stem = pm.RegexMatch.Groups[2].Value.Trim(),
Priority = pm.PatternConfig.Priority,
Score = score // 赋值解析到的分数
};
if (questionStack.Any())
{
questionStack.Peek().SubQuestions.Add(newQuestion);
}
else if (currentMajorQG != null)
{
currentMajorQG.SubQuestions.Add(newQuestion);
}
else
{
examPaper.TopLevelQuestions.Add(newQuestion);
}
currentContentStart = pm.EndIndex;
questionStack.Push(newQuestion);
currentQuestion = newQuestion;
}
catch (Exception ex)
{
examPaper.Errors.Add(new ParseError(ParseErrorType.UnexpectedError,
$"An unexpected error occurred during processing Question at index {i}.",
index: i, matchedText: pm.MatchedText, innerException: ex));
currentContentStart = pm.EndIndex; // 尝试跳过此项
continue;
}
}
else if (pm.Type == MatchType.Option)
{
// 对 Option 的处理
try
{
if (currentQuestion != null)
{
// RegexMatch Groups 验证
if (pm.RegexMatch.Groups.Count < 3 || string.IsNullOrWhiteSpace(pm.RegexMatch.Groups[1].Value) || string.IsNullOrWhiteSpace(pm.RegexMatch.Groups[2].Value))
{
examPaper.Errors.Add(new ParseError(ParseErrorType.RegexMatchIssue,
$"Option match at index {i} does not have enough regex groups or valid label/text groups (Group 1/2). Skipping this option.",
index: i, matchedText: pm.MatchedText));
currentContentStart = pm.EndIndex; // 尝试跳过此项
continue;
}
Option newOption = new Option
{
Label = pm.RegexMatch.Groups[1].Value.Trim(),
Text = pm.RegexMatch.Groups[2].Value.Trim()
};
currentQuestion.Options.Add(newOption);
}
else
{
// 结构性问题:找到孤立的选项,记录错误但继续
examPaper.Errors.Add(new ParseError(ParseErrorType.Structural,
$"Found isolated Option at index {i}. Options must belong to a question. Ignoring this option.",
index: i, matchedText: pm.MatchedText));
}
}
catch (Exception ex)
{
examPaper.Errors.Add(new ParseError(ParseErrorType.UnexpectedError,
$"An unexpected error occurred during processing Option at index {i}.",
index: i, matchedText: pm.MatchedText, innerException: ex));
// 这里不需要 `continue`,因为即使出错也可能只是该选项的问题,不影响后续处理
}
} }
currentContentStart = pm.EndIndex; // 更新当前内容起点 currentContentStart = pm.EndIndex;
} }
catch (Exception ex) catch (Exception ex)
{ {
// 捕获任何在处理单个 PotentialMatch 过程中未被更具体 catch 块捕获的意外错误 assignment.Errors.Add(new ParseError(ParseErrorType.UnexpectedError,
examPaper.Errors.Add(new ParseError(ParseErrorType.UnexpectedError,
$"An unexpected error occurred during main loop processing of PotentialMatch at index {i}.", $"An unexpected error occurred during main loop processing of PotentialMatch at index {i}.",
index: i, matchedText: pm.MatchedText, innerException: ex)); index: i, matchedText: pm.MatchedText, innerException: ex));
currentContentStart = Math.Max(currentContentStart, pm.EndIndex); // 尝试跳过当前匹配项,继续下一项 currentContentStart = Math.Max(currentContentStart, pm.EndIndex);
// 这里不 `continue` 是因为外层循环会推进 `i`,但确保 `currentContentStart` 更新以避免无限循环
} }
} }
// --- 处理所有匹配项之后的剩余内容 ---
if (currentContentStart < fullExamText.Length) if (currentContentStart < fullExamText.Length)
{ {
try try
@@ -542,169 +297,183 @@ namespace TechHelper.Client.Exam
string remainingText = fullExamText.Substring(currentContentStart).Trim(); string remainingText = fullExamText.Substring(currentContentStart).Trim();
if (!string.IsNullOrWhiteSpace(remainingText)) if (!string.IsNullOrWhiteSpace(remainingText))
{ {
if (currentQuestion != null) if (assignmentQuestionStack.Peek().Question != null)
{ {
ProcessQuestionContent(currentQuestion, remainingText, ProcessQuestionContent(assignmentQuestionStack.Peek(), remainingText, assignment.Errors);
GetSubMatchesForRange(allPotentialMatches, currentContentStart, fullExamText.Length, examPaper.Errors),
examPaper.Errors);
}
else if (currentMajorQG != null)
{
currentMajorQG.Descript += (string.IsNullOrWhiteSpace(currentMajorQG.Descript) ? "" : "\n") + remainingText;
} }
else else
{ {
examPaper.Description += (string.IsNullOrWhiteSpace(examPaper.Description) ? "" : "\n") + remainingText; assignment.Description += (string.IsNullOrWhiteSpace(assignment.Description) ? "" : "\n") + remainingText;
} }
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
examPaper.Errors.Add(new ParseError(ParseErrorType.UnexpectedError, assignment.Errors.Add(new ParseError(ParseErrorType.UnexpectedError,
"An unexpected error occurred while processing remaining text after all potential matches.", "An unexpected error occurred while processing remaining text after all potential matches.",
innerException: ex)); innerException: ex));
} }
} }
return examPaper; return assignment;
}
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) private string GetExamTitle(string examPaperText)
{ {
// 内部不再直接抛出异常,而是让外部的 try-catch 负责
var firstLine = examPaperText.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) var firstLine = examPaperText.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
.FirstOrDefault(line => !string.IsNullOrWhiteSpace(line)); .FirstOrDefault(line => !string.IsNullOrWhiteSpace(line));
return firstLine ?? "未识别试卷标题"; 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 public class ExamParser
@@ -721,20 +490,16 @@ namespace TechHelper.Client.Exam
} }
/// <summary> /// <summary>
/// 解析给定的试卷文本,返回结构化的 ExamPaper 对象。 /// 解析给定的试卷文本,返回结构化的 AssignmentEx 对象。
/// </summary> /// </summary>
/// <param name="examPaperText">完整的试卷文本</param> /// <param name="examPaperText">完整的试卷文本</param>
/// <returns>解析后的 ExamPaper 对象</returns> /// <returns>解析后的 AssignmentEx 对象</returns>
public ExamPaper ParseExamPaper(string examPaperText) public AssignmentEx ParseExamPaper(string examPaperText)
{ {
// 1. 扫描:一次性扫描整个文本,收集所有潜在的匹配项 var assignment = new AssignmentEx();
// Scan 方法现在已经优化为不抛出 ArgumentNullException List<PotentialMatch> allPotentialMatches = _scanner.Scan(examPaperText, assignment.Errors);
List<PotentialMatch> allPotentialMatches = _scanner.Scan(examPaperText); assignment = _builder.BuildExam(examPaperText, allPotentialMatches);
return assignment;
// 2. 构建:根据扫描结果和原始文本,线性遍历并构建层级结构
// BuildExamPaper 现在会返回一个包含错误列表的 ExamPaper 对象
// 外部不再需要捕获内部解析异常,只需检查 ExamPaper.Errors 列表
return _builder.BuildExamPaper(examPaperText, allPotentialMatches);
} }
} }
} }

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

@@ -0,0 +1,31 @@
using MudBlazor;
namespace TechHelper.Client.Helper
{
public static class Helper
{
public static Color GetColorFromInt(int value)
{
var v = value % Enum.GetValues(typeof(Color)).Length;
switch (value)
{
case 1:
return MudBlazor.Color.Primary;
case 2:
return MudBlazor.Color.Secondary;
case 3:
return MudBlazor.Color.Success;
case 4:
return MudBlazor.Color.Info;
case 5:
return MudBlazor.Color.Warning;
case 6:
return MudBlazor.Color.Error;
case 7:
return MudBlazor.Color.Dark;
default:
return MudBlazor.Color.Default;
}
}
}
}

View File

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

View File

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

View File

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

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

@@ -4,83 +4,63 @@
<MudSnackbarProvider /> <MudSnackbarProvider />
<MudPopoverProvider /> <MudPopoverProvider />
@* <MudLayout>
<MudPaper Style="position: fixed; <MudAppBar Elevation="0" Class="rounded-xl" Style="background-color: transparent; border:none">
top: 0; <MudBreakpointProvider>
left: 0; <MudHidden Breakpoint="Breakpoint.SmAndDown" Invert=true>
width: 100vw; <MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Primary" Edge="Edge.Start" OnClick="@((e) => DrawerToggle())" />
height: 100vh; </MudHidden>
background-image: url('/ref/bg4.jpg'); <MudHidden Breakpoint="Breakpoint.SmAndDown">
background-size: cover; <SearchBar></SearchBar>
background-position: center center; <MudButton Class="mt-1">application</MudButton>
background-repeat: no-repeat; </MudHidden>
filter: blur(10px); </MudBreakpointProvider>
z-index: -1;"> <MudSpacer />
</MudPaper> <MudIconButton Icon="@Icons.Material.Filled.MoreVert" Color="Color.Primary" Edge="Edge.End" />
<MudPaper Class="d-flex flex-column flex-grow-0 overflow-auto" Style="height: 100vh; background-color:#22222200"> </MudAppBar>
<MudDrawer @bind-Open="_drawerOpen" Height="100%" Elevation="0" Style="background-color:#f5f6fb">
<MudDrawerHeader Class="h-100 d-flex flex-grow-1" Style="background-color:#f5f6fb">
<MudPaper Width="250px" Class="d-flex py-3 flex-column justify-content-between rounded-xl" Elevation="3">
<MudNavMenu Bordered="true" Dense="true" Rounded="true" Color="Color.Error" Margin="Margin.Dense">
<ApplicationMainIconCard></ApplicationMainIconCard>
<MudDivider Class="my-2" />
<MudNavLink Href="/">Home</MudNavLink>
<MudNavLink Href="/exam">Exam</MudNavLink>
<MudNavLink Href="/students">Students</MudNavLink>
<MudSpacer />
<MudPaper Class="d-flex flex-column flex-grow-1 overflow-hidden" Style="background-color:transparent"> <MudNavLink Class="align-content-end" Href="/about">About</MudNavLink>
</MudNavMenu>
<MudSpacer />
<MudPaper Elevation="3" Height="10%" Class=" d-flex justify-content-around flex-grow-0" Style="background-color:#ffffff55"> <MudNavMenu Class="align-content-end " Bordered="true" Dense="true" Rounded="true" Margin="Margin.Dense">
<NavBar Class="flex-column flex-grow-1 " Style="background-color:transparent" /> <TechHelper.Client.Pages.Global.LoginInOut.LoginInOut></TechHelper.Client.Pages.Global.LoginInOut.LoginInOut>
<AuthLinks Class="flex-column flex-grow-0 " Style="background-color:transparent" /> <MudNavLink Class="align-content-end" Href="/Account/Manage">Setting</MudNavLink>
</MudPaper> </MudNavMenu>
<MudPaper Elevation="3" Class="d-flex flex-row flex-grow-1 overflow-hidden" Style="background-color:transparent">
<MudPaper Width="10%" Class="pa-2 ma-1 d-flex flex-column flex-grow-0 justify-content-between" Style="background-color:#ffffffaa">
</MudPaper> </MudPaper>
</MudDrawerHeader>
</MudDrawer>
<MudPaper Elevation="3" Class="d-flex flex-grow-1 pa-3 ma-1 overflow-hidden" Style="background-color:#ffffff22 "> <MudMainContent Style="background: #f5f6fb">
@Body <SnackErrorBoundary @ref="errorBoundary">
<MudPaper Height="calc(100vh - 64px)" Style="background-color:transparent" Class="overflow-hidden px-1 py-2" Elevation="0">
<MudPaper Style="background-color:transparent" Elevation="0" Class="d-flex w-100 h-100 overflow-hidden pa-2 rounded-xl">
@Body
</MudPaper>
</MudPaper> </MudPaper>
</SnackErrorBoundary>
</MudMainContent>
</MudLayout>
@code {
ErrorBoundary? errorBoundary;
protected override void OnParametersSet()
{
errorBoundary?.Recover();
}
bool _drawerOpen = true;
</MudPaper> void DrawerToggle()
{
_drawerOpen = !_drawerOpen;
</MudPaper> }
}
</MudPaper>
*@
<MudPaper Style="position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-image: url('/ref/bg4.jpg');
background-size: cover;
background-position: center center;
background-repeat: no-repeat;
filter: blur(10px);
z-index: -1;">
</MudPaper>
<MudPaper Style="background-color:transparent ; height:100vh" Class="overflow-hidden">
<MudPaper Class="justify-content-center" Style="background-color:blue; height: 50px">
<MudStack Row="true" Class="justify-content-between">
<NavBar Class="flex-grow-1" Style="background-color:transparent; color:white" />
<AuthLinks Class="justify-content-end " Style="background-color:transparent; color:white" />
</MudStack>
</MudPaper>
<MudPaper Class="d-flex flex-grow-0 " Style="background-color:#30303022; height:calc(100vh - 50px)">
@* <MudPaper Class="ma-1" Width="200px">
</MudPaper> *@
<MudPaper Class="d-flex ma-1 flex-grow-1 overflow-auto">
@Body
</MudPaper>
</MudPaper>
</MudPaper>

View File

@@ -1,48 +1,59 @@
@page "/login" @page "/login"
<MudText Typo="Typo.h2"> Login Account </MudText> <div class="d-flex flex-column flex-grow-1 page-containerr">
<EditForm Model="@_userForAuth" OnValidSubmit="Logining" FormName="LoginingForm"> <div class="d-flex mud-paper-80-percent-centeredd w-75">
<DataAnnotationsValidator /> <MudPaper Class="d-flex flex-grow-1 ma-0 pa-0" style="background-color:transparent; min-height:100%" Elevation="0">
<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="nav-item px-3"> <MudGrid Class="d-flex flex-grow-1" style="background-color:transparent; min-height:100%">
<NavLink class="nav-link" href="forgotpassword"> <MudItem xs="12" sm="4" style="background-color:transparent">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span>Forgot Password <MudPaper Class="d-flex flex-column align-start justify-start mud-width-full h-100 pa-8" Elevation="0" Style="background-color:transparent">
</NavLink> <MudText Style="color:#ffffff" Typo="Typo.h4">TechHelper</MudText>
</div> <MudText Style="color:#ffffff" Typo="Typo.body2">轻松管理,高效学习。</MudText>
</MudCard> <MudSpacer />
</MudItem> <MudText Style="color:#ffffff" Typo="Typo.h4">教育不是注满一桶水,</MudText>
<MudItem xs="12" sm="5"> <MudText Style="color:#ffffff" Typo="Typo.h4"> 而是点燃一把火。</MudText>
<MudPaper Class="pa-4 mud-height-full"> <MudImage Alt="Hello World" Fluid="true" Src="ref/UnFinish.png" />
<MudText Typo="Typo.subtitle2">Validation Summary</MudText> </MudPaper>
@if (!ShowRegistrationErrors) </MudItem>
{
<MudText Color="Color.Success">Success</MudText> <MudItem xs="12" sm="8" style="background-color:transparent">
}
else <MudPaper Class="d-flex flex-row flex-grow-1 justify-center rounded-xl px-0 mud-height-full" >
{
<MudText Color="@Color.Error"> <EditForm Model="@_userForAuth" OnValidSubmit="Logining" FormName="LoginingForm" class="w-100">
<ValidationSummary /> <DataAnnotationsValidator />
</MudText> <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>
</MudPaper> <MudTextField Label="Email" Class="mt-3"
</MudItem> @bind-Value="_userForAuth.Email" For="@(() => _userForAuth.Email)" />
<MudItem xs="12"> <MudTextField Label="Password" HelperText="Choose a strong password" Class="mt-3"
<MudText Typo="Typo.body2" Align="Align.Center"> @bind-Value="_userForAuth.Password" For="@(() => _userForAuth.Password)" InputType="InputType.Password" />
Fill out the form correctly to see the success message.
</MudText>
</MudItem> <MudStack Row=true Class="align-content-center justify-content-start my-3">
</MudGrid> <MudCheckBox @bind-Value="Basic_CheckBox2" Color="Color.Primary"></MudCheckBox>
</EditForm> <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] [Inject]
public NavigationManager NavigationManager { get; set; } public NavigationManager NavigationManager { get; set; }
public bool Basic_CheckBox2 { get; set; } = true;
public bool ShowRegistrationErrors { get; set; } public bool ShowRegistrationErrors { get; set; }
public string Error { get; set; } public string Error { get; set; }

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" @page "/register"
@using System.ComponentModel.DataAnnotations @using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Components
@using Entities.Contracts @using Entities.Contracts
@using System.Globalization;
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
<div class="d-flex flex-grow-1 page-container">
<MudText Typo="Typo.h2"> Create Account </MudText> <div class="d-flex mud-paper-80-percent-centered w-75">
<MudPaper Class="d-flex flex-grow-1 ma-0 pa-0" style="background-color:transparent; min-height:100%" Elevation="0">
<EditForm Model="@_userForRegistration" OnValidSubmit="Register" FormName="RegistrationForm"> <MudGrid Class="d-flex flex-grow-1" style="background-color:transparent; min-height:100%">
<DataAnnotationsValidator /> <MudItem xs="12" sm="4" style="background-color:transparent">
<MudGrid> <MudPaper Class="d-flex flex-column align-start justify-start mud-width-full h-100 pa-8" Elevation="0" Style="background-color:transparent">
<MudItem xs="12" sm="7"> <MudText Style="color:#ffffff" Typo="Typo.h4">TechHelper</MudText>
<MudCard> <MudText Style="color:#ffffff" Typo="Typo.body2">快速注册,开始你的管理之旅。</MudText>
<MudCardContent> <MudSpacer />
<MudTextField Label="Name" HelperText="Max. 8 characters" <MudText Style="color:#ffffff" Typo="Typo.h4">学而不思则罔,</MudText>
@bind-Value="_userForRegistration.Name" For="@(() => _userForRegistration.Email)" /> <MudText Style="color:#ffffff" Typo="Typo.h4"> 思而不学则殆。</MudText>
<MudTextField Label="Email" Class="mt-3" <MudImage Alt="Hello World" Fluid="true" Src="ref/UnFinish.png" />
@bind-Value="_userForRegistration.Email" For="@(() => _userForRegistration.Email)" /> </MudPaper>
<MudTextField Label="Password" HelperText="Choose a strong password" Class="mt-3" </MudItem>
@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">
<MudTextField Label="Class" <MudItem xs="12" sm="8" style="background-color:transparent">
HelperText="Enter a class number between 1 and 14."
Class="mt-3"
@bind-Value="_userForRegistration.Class"
For="@(() => _userForRegistration.Class)"
InputType="InputType.Number"
Required="true"
RequiredError="Class is required." />
<MudTextField Label="Grade" <MudPaper Class="d-flex flex-row flex-grow-1 justify-center rounded-xl px-20 mud-height-full">
HelperText="Enter a grade number between 1 and 6." <EditForm Model="@_userForRegistration" OnValidSubmit="Register" FormName="RegistrationForm" class="w-100">
Class="mt-3" <DataAnnotationsValidator />
@bind-Value="_userForRegistration.Grade" <MudPaper Class="d-flex flex-column flex-grow-1 rounded-xl px-5 justify-content-center pt-15 w-100 " Elevation="0" Outlined="false">
For="@(() => _userForRegistration.Grade)" <MudText Typo="Typo.h5"> <b>注册账户</b> </MudText>
InputType="InputType.Number" <MudTextField Label="Name" HelperText="Max. 8 characters"
Required="true" @bind-Value="_userForRegistration.Name" For="@(() => _userForRegistration.Email)" />
RequiredError="Grade is required." /> <MudTextField Label="Email" Class="mt-3"
</MudStack> @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" <MudStack Row="true">
HelperText="Enter your phone number (optional, 7-20 digits)." <MudSelect T="GradeEnum" Value="grade" Label="Select Grade" AdornmentColor="Color.Secondary" ValueChanged="HandleSelectedValuesChanged">
Class="mt-3" @foreach (GradeEnum item in Enum.GetValues(typeof(GradeEnum)))
@bind-Value="_userForRegistration.PhoneNumber" {
For="@(() => _userForRegistration.PhoneNumber)" <MudSelectItem Value="@item">@item</MudSelectItem>
InputType="InputType.Telephone" /> <MudTextField Label="Home Address" }
HelperText="Enter your home address (optional, max 200 characters)." </MudSelect>
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>
<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 System.Text.RegularExpressions;
using TechHelper.Features; using TechHelper.Features;
using Entities.Contracts; using Entities.Contracts;
using TechHelper.Client.Services;
namespace TechHelper.Client.Pages.Author namespace TechHelper.Client.Pages.Author
{ {
@@ -12,14 +13,19 @@ namespace TechHelper.Client.Pages.Author
{ {
private UserForRegistrationDto _userForRegistration = new UserForRegistrationDto(); private UserForRegistrationDto _userForRegistration = new UserForRegistrationDto();
private GradeEnum grade = GradeEnum.Unknown;
[Inject] [Inject]
public IAuthenticationClientService AuthenticationService { get; set; } public IAuthenticationClientService AuthenticationService { get; set; }
[Inject] [Inject]
public NavigationManager NavigationManager { get; set; } public NavigationManager NavigationManager { get; set; }
[Inject]
public IClassServices ClassServices { get; set; }
public bool Basic_CheckBox2 { get; set; } = true;
public byte selectclass { get; set; } = 0;
public List<byte> Classes { get; set; } = new List<byte>();
public bool ShowRegistrationErrors { get; set; } public bool ShowRegistrationErrors { get; set; }
public string[] Errors { get; set; } public string[] Errors { get; set; }
@@ -51,6 +57,25 @@ namespace TechHelper.Client.Pages.Author
[Inject] [Inject]
public IEmailSender emailSender { get; set; } public IEmailSender emailSender { get; set; }
private async void HandleSelectedValuesChanged(GradeEnum selectedValues)
{
grade = selectedValues;
Snackbar.Add("<22><><EFBFBD>ȴ<EFBFBD>,<2C><><EFBFBD>ڻ<EFBFBD>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD>༶", Severity.Info);
var result = await ClassServices.GetGradeClasses((byte)selectedValues);
if (result.Status)
{
Classes = result.Result as List<byte> ?? new List<byte>();
Snackbar.Add("<22><>ȡ<EFBFBD>ɹ<EFBFBD>", Severity.Success);
return;
}
Snackbar.Add("<22><>ȡʧ<C8A1><CAA7>", Severity.Error);
}
private void HandleListSelectedValuesChanged(byte selectedValues)
{
selectclass = selectedValues;
}
public async void SendEmail() public async void SendEmail()
{ {
string eamilTo = "1928360026@qq.com"; string eamilTo = "1928360026@qq.com";

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,137 @@
<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 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">
总数:
<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>
<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 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>
</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,191 @@
@using Entities.DTO
@using TechHelper.Client.Services
<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 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> @StudentSubmissionDetail.AverageScore </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">
总数:
<span style="color: #fefefe;"> @StudentSubmissionDetail.TotalQuestions </span>
</MudText>
</MudPaper>
<MudPaper Elevation=0 Height="50%" Style="background-color:transparent">
<MudText Style="color:#9ed5f7" Typo="Typo.body2">
总分:
<span style="color: #fefefe;"> 150 </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;"> @StudentSubmissionDetail.TotalRank </span>
</MudText>
</MudPaper>
<MudPaper Elevation=0 Height="50%" Style="background-color:transparent">
<MudText Style="color:#9ed5f7" Typo="Typo.body2">
平均:
<span style="color: #fefefe;"> @StudentSubmissionDetail.ClassAverageScore </span>
</MudText>
</MudPaper>
</MudPaper>
</MudStack>
</MudPaper>
</MudStack>
</MudItem>
<MudItem 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>
</MudChipSet>
</MudItem>
</MudGrid>
</MudPaper>
@* <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> *@
@code {
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 } },
};
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;
}
[Parameter]
public Guid SubmissionID { get; set; } = Guid.Empty;
private StudentSubmissionDetailDto StudentSubmissionDetail { get; set; } = new StudentSubmissionDetailDto();
private IReadOnlyCollection<string> _selected;
#pragma warning restore 1998
#nullable restore
#line (82, 8) - (143, 1) "D:\AllWX\AllC\TechHelper\TechHelper.Client\Pages\Common\Exam\SubmissionInfoCard.razor"
[Inject]
public IStudentSubmissionDetailService StudentSubmissionDetailService { get; set; }
[Inject]
public ISnackbar Snackbar { get; set; }
protected override async Task OnInitializedAsync()
{
if (SubmissionID != Guid.Empty)
{
StudentSubmissionDetailDto result;
try
{
result = await StudentSubmissionDetailService.GetSubmissionDetailAsync(SubmissionID);
if (result != null)
{
StudentSubmissionDetail = result;
XAxisLabels = result.ErrorTypeDistribution.Keys.ToArray();
Series.Clear();
Series.Add(new ChartSeries
{
Name = "类型错误数量分布",
Data = result.ErrorTypeDistribution.Values.Select(d => (double)d).ToArray()
});
Series.Add(new ChartSeries
{
Name = "类型错误成绩分布",
Data = result.ErrorTypeScoreDistribution.Values.Select(d => (double)d).ToArray()
});
}
}
catch (Exception ex)
{
Snackbar.Add($"获取提交错误, 请重试, {ex.Message}", Severity.Warning);
}
}
}
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,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">
总数:
<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>
<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>
</MudChipSet>
</MudItem>
</MudGrid>
</MudPaper>
@* <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> *@
@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,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>
<GlobalInfoCard AssignmentDto="Assignment"></GlobalInfoCard>
</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 Assignment { get; set; } = new AssignmentDto();
private void Cancel() => MudDialog.Cancel();
private void Confirm()
{
Snackbar.Add("属性已更新", Severity.Success);
MudDialog.Close(DialogResult.Ok(Assignment));
}
}

View File

@@ -0,0 +1,37 @@
@using Entities.DTO
@using Entities.Contracts
@using Helper
<MudPaper Elevation=5 Class="w-100 pa-5 rounded-xl" Height="@Height" Style="@Style">
<MudTextField Value="@AssignmentDto.Title"></MudTextField>
<MudTextField Value="@AssignmentDto.Score">SCORE</MudTextField>
<MudTextField Value="@AssignmentDto.TotalQuestions">NUMQUESTION</MudTextField>
<MudChipSet T="SubjectAreaEnum" SelectedValue="@AssignmentDto.SubjectArea" CheckMark SelectionMode="SelectionMode.SingleSelection" SelectedValueChanged="HandleQTSelectedValueChanged">
@foreach (SubjectAreaEnum item in Enum.GetValues(typeof(SubjectAreaEnum)))
{
var color = Helper.GetColorFromInt((int)item);
<MudChip Color=@color
Value="@item">
</MudChip>
}
</MudChipSet>
<MudText>DUETIME</MudText>
<MudText>EXAMTYPE</MudText>
</MudPaper>
@code {
[Parameter]
public AssignmentDto AssignmentDto { get; set; }
[Parameter]
public string Style { get; set; }
[Parameter]
public string Height { get; set; } = "auto";
public void HandleQTSelectedValueChanged(SubjectAreaEnum subject)
{
AssignmentDto.SubjectArea = subject;
}
}

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=1 Class="w-100 rounded-xl ma-2 pa-2" 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,7 @@
<MudPaper>
<MudText> ExamName </MudText>
<MudText> 已经指派人数 </MudText>
<MudText> 总人数 </MudText>
<MudText> 平均S </MudText>
<MudText> 指派 </MudText>
</MudPaper>

View File

@@ -0,0 +1,113 @@
@using Entities.DTO
@using Entities.Contracts
@using Newtonsoft.Json
@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.Root">@AssignmentStructType.Root</MudChip>
<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>
<MudChipSet T="string" SelectedValue="@AssignmentQuestion.QType" CheckMark SelectionMode="SelectionMode.SingleSelection" SelectedValueChanged="HandleQTSelectedValueChanged">
@foreach (var item in QuestionTypes)
{
var qt = item;
@* Style = "@($"background - color:{ item.Value.Color} ")"*@
<MudChip Style="@(qt.Key == AssignmentQuestion.QType ?
$"background-color:#ffffff; color:{item.Value.Color}" :
$"background-color:{item.Value.Color}; color:#ffffff")"
Value="@item.Key">
@item.Value.DisplayName
</MudChip>
}
</MudChipSet>
</MudPaper>
@if (AssignmentQuestion.Question != null)
{
<QuestionEdit Question="AssignmentQuestion.Question" />
}
</MudPaper>
@code {
[Parameter]
public AssignmentQuestionDto AssignmentQuestion { get; set; } = new AssignmentQuestionDto();
public QuestionDto TempQuesdto;
Dictionary<string, QuestionDisplayTypeData> QuestionTypes = new Dictionary<string, QuestionDisplayTypeData>();
[Inject]
private ILocalStorageService LocalStorageService { get; set; }
protected override void OnInitialized()
{
base.OnInitialized();
if (AssignmentQuestion.Question != null)
{
TempQuesdto = AssignmentQuestion.Question;
}
var cs = LocalStorageService.GetItem<string>("GlobalInfo");
var GlobalInfo = JsonConvert.DeserializeObject<Dictionary<string, QuestionDisplayTypeData>>(cs);
if(GlobalInfo != null)
{
QuestionTypes = GlobalInfo;
}
}
private void HandleQTSelectedValueChanged(string type)
{
AssignmentQuestion.QType = type;
if (AssignmentQuestion.ChildrenAssignmentQuestion.Count > 0 && AssignmentQuestion.StructType == AssignmentStructType.Group)
{
foreach (var item in AssignmentQuestion.ChildrenAssignmentQuestion)
{
item.QType = type;
if (item.Question != null)
{
item.Question.QType = type;
}
}
}
StateHasChanged();
}
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 Entities.DTO
@using TechHelper.Client.Exam @using TechHelper.Client.Exam
@using TechHelper.Client.Services
@page "/exam/check/{ExamID}" @page "/exam/check/{ExamID}"
<MudText Typo="Typo.h4" Class="mb-4">试卷批改预览: @ExamDto.AssignmentTitle</MudText> <MudText Typo="Typo.h4" Class="mb-4">试卷批改预览: @Assignment.Title</MudText>
<MudDivider Class="my-4" /> <MudDivider Class="my-4" />
@if (_isLoading) @if (_isLoading)
{ {
<MudProgressCircular Indeterminate="true" Color="Color.Primary" Class="d-flex justify-center my-8" /> <MudProgressCircular Indeterminate="true" Color="Color.Primary" Class="d-flex justify-center my-8" />
<MudText Class="text-center">正在加载试卷和学生数据...</MudText> <MudText Class="text-center">正在加载试卷和学生数据...</MudText>
} }
else if (_questionsForTable.Any() && _students.Any()) else if (_questionsForTable.Any() && _students.Any())
{ {
<MudTable @ref="_table" T="QuestionRowData" Items="@_questionsForTable" Hover="true" Breakpoint="Breakpoint.Sm" Class="mud-elevation-2" Dense="true"> <MudTable @ref="_table" T="QuestionRowData" Items="@_questionsForTable" Hover="true" Breakpoint="Breakpoint.Sm" Striped="true" Class="mud-elevation-2" Dense="true">
<HeaderContent> <HeaderContent>
<MudTh Style="width:100px;">序号</MudTh> <MudTh Style="width:100px;">序号</MudTh>
<MudTh Style="width:80px; text-align:center;">分值</MudTh> <MudTh Style="width:80px; text-align:center;">分值</MudTh>
@foreach (var student in _students) @foreach (var student in _students)
{ {
<MudTh Style="width:120px; text-align:center;"> <MudTh Style="width:120px; text-align:center;">
@student.Name @student.DisplayName
<MudTooltip Text="点击以切换此学生所有题目的对错"> <MudTooltip Text="点击以切换此学生所有题目的对错">
<MudIconButton Icon="@Icons.Material.Filled.Info" Size="Size.Small" Class="ml-1" <MudIconButton Icon="@Icons.Material.Filled.Info" Size="Size.Small" Class="ml-1"
@onclick="() => ToggleStudentAllAnswers(student.Id)" /> @onclick="() => ToggleStudentAllAnswers(student.Id)" />
</MudTooltip> </MudTooltip>
</MudTh> </MudTh>
} }
</HeaderContent> </HeaderContent>
<RowTemplate> <RowTemplate>
<MudTd DataLabel="序号">@context.QuestionItem.Sequence</MudTd> <MudTd DataLabel="序号">@context.QuestionItem.Sequence</MudTd>
<MudTd DataLabel="分值" Style="text-align:center;">@context.QuestionItem.Score</MudTd> <MudTd DataLabel="分值" Style="text-align:center;">@context.QuestionItem.Score</MudTd>
@foreach (var student in _students) @foreach (var student in _students)
{ {
<MudTd DataLabel="@student.Name" Style="text-align:center;"> <MudTd DataLabel="@student.DisplayName" Style="text-align:center;">
@if (context.StudentAnswers.ContainsKey(student.Id)) @if (context.StudentAnswers.ContainsKey(student.Id))
{ {
<MudCheckBox @bind-Value="context.StudentAnswers[student.Id]" Size="Size.Small" Color="Color.Primary"></MudCheckBox> <MudCheckBox @bind-Value="context.StudentAnswers[student.Id]" Size="Size.Small" Color="Color.Primary"></MudCheckBox>
} }
else else
{ {
<MudText Color="Color.Warning">N/A</MudText> <MudText Color="Color.Warning">N/A</MudText>
} }
</MudTd> </MudTd>
} }
</RowTemplate> </RowTemplate>
<PagerContent> <PagerContent>
<MudTablePager /> <MudTablePager />
</PagerContent> </PagerContent>
</MudTable> </MudTable>
<MudPaper Class="pa-4 mt-4 mud-elevation-2 d-flex flex-column align-end"> <MudPaper Class="pa-4 mt-4 mud-elevation-2 d-flex flex-column align-end">
<MudText Typo="Typo.h6">学生总分预览:</MudText> <MudText Typo="Typo.h6">学生总分预览:</MudText>
@foreach (var student in _students) @foreach (var student in _students)
{ {
<MudText Typo="Typo.subtitle1"> <MudText Typo="Typo.subtitle1">
@student.Name: <MudText Typo="Typo.h5" Color="Color.Primary" Class="d-inline-block ml-2">@GetStudentTotalScore(student.Id)</MudText> @student.DisplayName: <MudText Typo="Typo.h5" Color="Color.Primary" Class="d-inline-block ml-2">@GetStudentTotalScore(student.Id)</MudText>
</MudText> </MudText>
} }
<MudButton Variant="Variant.Filled" Color="Color.Success" Class="mt-4" @onclick="SubmitGrading"> <MudButton Variant="Variant.Filled" Color="Color.Success" Class="mt-4" @onclick="SubmitGrading">
提交批改结果 (模拟) 提交批改结果 (模拟)
</MudButton> </MudButton>
</MudPaper> </MudPaper>
} }
else else
{ {
<MudAlert Severity="Severity.Info" Class="mt-4">无法加载试卷或题目信息。</MudAlert> <MudAlert Severity="Severity.Info" Class="mt-4">无法加载试卷或题目信息。</MudAlert>
<MudButton Variant="Variant.Text" Color="Color.Primary" Class="mt-4" >返回试卷列表</MudButton> <MudButton Variant="Variant.Text" Color="Color.Primary" Class="mt-4">返回试卷列表</MudButton>
} }
@code { @code {
[Parameter] [Parameter]
public string ExamId { get; set; } // 从路由获取的试卷ID public string ExamId { get; set; }
[Inject] [Inject]
public IExamService ExamService { get; set; } // 注入试卷服务 public IExamService ExamService { get; set; }
[Inject] [Inject]
private ISnackbar Snackbar { get; set; } // 注入 Snackbar 用于消息提示 private ISnackbar Snackbar { get; set; }
[Inject] [Inject]
private NavigationManager Navigation { get; set; } // 注入导航管理器 private NavigationManager Navigation { get; set; }
private MudTable<QuestionRowData> _table = new(); // MudTable 实例引用 private MudTable<QuestionRowData> _table = new();
private ExamDto ExamDto { get; set; } = new ExamDto(); // 原始试卷数据 private AssignmentDto Assignment { get; set; } = new AssignmentDto();
private ExamStruct _examStruct = new ExamStruct(); // 处理后的试卷结构,包含带序号的题目 private AssignmentCheckData _examStruct = new AssignmentCheckData();
private List<Student> _students = new List<Student>(); // 临时生成的学生列表 private List<StudentDto> _students = new List<StudentDto>();
private List<QuestionRowData> _questionsForTable = new List<QuestionRowData>(); // 用于 MudTable 的数据源 private List<QuestionRowData> _questionsForTable = new List<QuestionRowData>();
private bool _isLoading = true; // 加载状态 private bool _isLoading = true;
// 在组件初始化时加载数据 [Inject]
protected override async Task OnInitializedAsync() public IClassServices ClassServices { get; set; }
{
_isLoading = true;
await LoadExamData();
GenerateTemporaryStudentsAndAnswers(); // 生成学生和初始作答数据
_isLoading = false;
}
// 加载试卷数据的方法 protected override async Task OnInitializedAsync()
private async Task LoadExamData() {
{ _isLoading = true;
if (Guid.TryParse(ExamId, out Guid parsedExamId)) await LoadExamData();
{
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");
}
}
// 生成临时学生和作答数据 var result = await ClassServices.GetClassStudents();
private void GenerateTemporaryStudentsAndAnswers() if (!result.Status) Snackbar.Add($"获取学生失败, {result.Message}", Severity.Error);
{ _students = result.Result as List<StudentDto> ?? new List<StudentDto>();
_students = new List<Student>(); BuildTable();
// 生成 40 个学生 _isLoading = false;
for (int i = 1; i <= 40; i++) }
{
_students.Add(new Student { Name = $"学生{i}" });
}
_questionsForTable = _examStruct.Questions.Select(qItem => private void BuildTable()
{ {
var rowData = new QuestionRowData _questionsForTable = _examStruct.Questions.Select(q =>
{ {
QuestionItem = qItem, var rowData = new QuestionRowData
StudentAnswers = new Dictionary<Guid, bool>() {
}; QuestionItem = q,
StudentAnswers = new Dictionary<Guid, bool>()
};
foreach (var student in _students)
{
rowData.StudentAnswers[student.Id] = false;
}
return rowData;
}).ToList();
}
// 为每个学生随机生成初始的对错状态 private async Task LoadExamData()
var random = new Random(); {
foreach (var student in _students) if (Guid.TryParse(ExamId, out Guid parsedExamId))
{ {
// 模拟随机对错50%的概率 try
rowData.StudentAnswers[student.Id] = random.Next(0, 2) == 1; {
} var result = await ExamService.GetExam(parsedExamId);
return rowData; if (result.Status)
}).ToList(); {
} 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 float GetStudentTotalScore(Guid studentId)
private void ToggleStudentAllAnswers(Guid studentId) {
{ float totalScore = 0;
bool allCorrect = _questionsForTable.All(row => row.StudentAnswers.ContainsKey(studentId) && row.StudentAnswers[studentId]); 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) private void ToggleStudentAllAnswers(Guid studentId)
{ {
if (row.StudentAnswers.ContainsKey(studentId)) bool allCorrect = _questionsForTable.All(row => row.StudentAnswers.ContainsKey(studentId) && row.StudentAnswers[studentId]);
{
row.StudentAnswers[studentId] = !allCorrect; // 全部取反
}
}
StateHasChanged(); // 手动通知 Blazor 刷新 UI
}
// 提交批改结果(模拟) foreach (var row in _questionsForTable)
private void SubmitGrading() {
{ if (row.StudentAnswers.ContainsKey(studentId))
Console.WriteLine("--- 提交批改结果 ---"); {
foreach (var student in _students) row.StudentAnswers[studentId] = !allCorrect;
{ }
Console.WriteLine($"学生: {student.Name}, 总分: {GetStudentTotalScore(student.Id)}"); }
foreach (var row in _questionsForTable) StateHasChanged();
{ }
if (row.StudentAnswers.TryGetValue(student.Id, out bool isCorrect))
{ private void SubmitGrading()
Console.WriteLine($" - 题目 {row.QuestionItem.Sequence}: {(isCorrect ? "正确" : "错误")}"); {
}
} List<SubmissionDto> submissionDto = new List<SubmissionDto>();
}
Snackbar?.Add("批改结果已提交(模拟)", Severity.Success);
// 实际应用中,这里会将 _questionsForTable 和 _students 的数据发送到后端API 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,10 @@
@page "/exam/create" @page "/exam/create"
@using AutoMapper
@using Entities.Contracts
@using Newtonsoft.Json
@using TechHelper.Client.Pages.Common
@using TechHelper.Client.Pages.Exam.ExamView
@using TechHelper.Client.Services
@using Blazored.TextEditor @using Blazored.TextEditor
@using Entities.DTO @using Entities.DTO
@using TechHelper.Client.Exam @using TechHelper.Client.Exam
@@ -7,64 +13,55 @@
@using Microsoft.AspNetCore.Components @using Microsoft.AspNetCore.Components
@using System.Globalization; @using System.Globalization;
@using TechHelper.Client.Pages.Editor @using TechHelper.Client.Pages.Editor
@inject IDialogService DialogService
<MudPaper Elevation="5" Class="d-flex overflow-hidden flex-grow-1" Style="overflow:hidden; position:relative;height:100%"> <MudPaper Elevation="5" Class="d-flex overflow-hidden flex-grow-1 pa-1 rounded-xl " Style="overflow:hidden; position:relative;height:100%">
<MudDrawerContainer Class="mud-height-full flex-grow-1" Style="height:100%"> <MudDrawerContainer Class="mud-height-full flex-grow-1" Style="height:100%">
<MudDrawer @bind-Open="@_open" Elevation="0" Variant="@DrawerVariant.Persistent" Color="Color.Primary" Anchor="Anchor.End" OverlayAutoClose="true"> <MudDrawer @bind-Open="@_open" Elevation="0" Variant="@DrawerVariant.Persistent" Color="Color.Primary" Anchor="Anchor.End" OverlayAutoClose="true">
<MudDrawerHeader>
<MudText Typo="Typo.h6"> 配置 </MudText>
</MudDrawerHeader>
<MudStack Class="overflow-auto"> @if (_edit)
<ParseRoleConfig /> {
<MudButton Color="Color.Success"> ParseExam </MudButton> <AssignmentQuestionEdit AssignmentQuestion="selectedAssignmentQuestion" />
</MudStack> }
else
{
<MudDrawerHeader>
<MudText Typo="Typo.h6"> 配置 </MudText>
</MudDrawerHeader>
<MudStack Class="overflow-auto">
<ParseRoleConfig />
<MudButton Color="Color.Success"> ParseExam </MudButton>
</MudStack>
}
</MudDrawer> </MudDrawer>
<MudStack Row="true" Class="flex-grow-1" Style="height:100%"> <MudStack Class="flex-grow-1 h-100">
<ExamView Class="overflow-auto" ParsedExam="ExamContent"></ExamView>
<MudPaper Class="ma-2"> <MudPaper Class="rounded-xl ma-1 pa-2" Style="background-color:#fefefefe">
<MudPaper Elevation="0" Style="height:calc(100% - 80px)"> <MudButton Variant="Variant.Filled" Color="Color.Secondary" Class="rounded-xl" Size="Size.Small" OnClick="OpenEditor">文本编辑器</MudButton>
<BlazoredTextEditor @ref="@_textEditor"> <MudButton Variant="Variant.Filled" Color="Color.Secondary" Class="rounded-xl" Size="Size.Small" OnClick="ParseExam">载入</MudButton>
<ToolbarContent> <MudButton Variant="Variant.Filled" Color="Color.Secondary" Class="rounded-xl" Size="Size.Small" OnClick="OpenPublish">发布</MudButton>
<select class="ql-header"> <MudButton Variant="Variant.Filled" Color="Color.Secondary" Class="rounded-xl" Size="Size.Small" OnClick="OpenPublish">指派</MudButton>
<option selected=""></option> <MudButton Variant="Variant.Filled" Color="Color.Secondary" Class="rounded-xl" Size="Size.Small" OnClick="OpenTest">Test</MudButton>
<option value="1"></option> <MudButton Variant="Variant.Filled" Color="Color.Secondary" Class="rounded-xl" Size="Size.Small" OnClick="HandleGlobalInfo">GlobalExamInfo</MudButton>
<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> </MudPaper>
<MudButtonGroup Vertical="true" Color="Color.Primary" Variant="Variant.Filled">
<MudIconButton Icon="@Icons.Material.Filled.Settings" OnClick="@ToggleDrawer" Color="Color.Secondary" /> <ExamView Class="overflow-auto ma-1 pa-2 rounded-xl" ClickedStruct="HandleClickedStruct" ParsedExam="ExamContent"></ExamView>
<MudIconButton Icon="@Icons.Material.Filled.TransitEnterexit" OnClick="@ParseExam" Color="Color.Secondary" />
<MudIconButton Icon="@Icons.Material.Filled.Save" OnClick="@ToggleDrawer" Color="Color.Secondary" /> <MudPaper MaxWidth="300">
<MudIconButton Icon="@Icons.Material.Filled.Publish" OnClick="@Publish" Color="Color.Secondary" />
</MudButtonGroup> @if (_parsedExam.Errors.Any())
{
foreach (var item in _parsedExam.Errors)
{
<MudText> @item.Message </MudText>
}
}
</MudPaper>
</MudStack> </MudStack>
</MudDrawerContainer> </MudDrawerContainer>
</MudPaper> </MudPaper>
@@ -74,23 +71,97 @@
[CascadingParameter] [CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; } private Task<AuthenticationState> authenticationStateTask { get; set; }
private AssignmentQuestionDto selectedAssignmentQuestion = new AssignmentQuestionDto();
private IReadOnlyCollection<string> _selected;
private bool _open = false; private bool _open = false;
private bool _edit = false;
private void ToggleDrawer() private void ToggleDrawer()
{ {
_open = !_open; _open = !_open;
_edit = false;
} }
private BlazoredTextEditor _textEditor = new BlazoredTextEditor(); private BlazoredTextEditor _textEditor = new BlazoredTextEditor();
private ExamPaper _parsedExam = new ExamPaper(); private AssignmentEx _parsedExam = new AssignmentEx();
private ExamDto ExamContent = new ExamDto(); private AssignmentDto ExamContent = new AssignmentDto();
private ExamParserConfig _examParserConfig { get; set; } = new ExamParserConfig(); private ExamParserConfig _examParserConfig { get; set; } = new ExamParserConfig();
private string EditorText = "";
[Inject]
private ILocalStorageService LocalStorageService { get; set; }
[Inject]
public IMapper Mapper { get; set; }
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
var response = await NoteService.GetNote((byte)SubjectAreaEnum.Literature);
if (response.Status)
{
try
{
LocalStorageService.SetItem("GlobalInfo", response.Result);
}
catch (Exception ex)
{
}
}
}
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() private async Task ParseExam()
{ {
var plainText = await _textEditor.GetText(); var plainText = await _textEditor.GetText();
EditorText = plainText;
if (!string.IsNullOrWhiteSpace(plainText)) if (!string.IsNullOrWhiteSpace(plainText))
{ {
@@ -100,7 +171,8 @@
_parsedExam = exampar.ParseExamPaper(plainText); _parsedExam = exampar.ParseExamPaper(plainText);
Snackbar.Add("试卷解析成功。", Severity.Success); Snackbar.Add("试卷解析成功。", Severity.Success);
Snackbar.Add($"{_parsedExam.Errors}。", Severity.Success); Snackbar.Add($"{_parsedExam.Errors}。", Severity.Success);
ExamContent = _parsedExam.ConvertToExamDTO(); StateHasChanged();
ExamContent = Mapper.Map<AssignmentDto>(_parsedExam);
ExamContent.SeqIndex(); ExamContent.SeqIndex();
} }
catch (Exception ex) catch (Exception ex)
@@ -122,12 +194,91 @@
[Inject] [Inject]
public IExamService examService { get; set; } public IExamService examService { get; set; }
[Inject]
public INoteService NoteService { get; set; }
public async Task Publish() public async Task Publish()
{ {
ExamContent.CreaterEmail = authenticationStateTask.Result.User.Identity.Name;
var apiRespon = await examService.SaveParsedExam(ExamContent); var apiRespon = await examService.SaveParsedExam(ExamContent);
Snackbar.Add(apiRespon.Message); Snackbar.Add(apiRespon.Message);
} }
public async Task OpenTest()
{
Dictionary<string, (Color, string)> Note = new Dictionary<string, (Color, string)> { { "Hello", (Color.Surface, "World") }, { "My", (Color.Surface, "App") }, };
var json = JsonConvert.SerializeObject(Note);
var result = await NoteService.AddNote(new GlobalDto { SubjectArea = Entities.Contracts.SubjectAreaEnum.Physics, Data = json });
}
Console.WriteLine(json);
var res = JsonConvert.DeserializeObject<Dictionary<string, (Color, string)>>(json);
}
private async void HandleGlobalInfo()
{
// _open = true;
// _edit = true;
// StateHasChanged();
var parameters = new DialogParameters<ExamGlobalInfoDialog> { { x => x.Assignment, ExamContent } };
var dialog = await DialogService.ShowAsync<ExamGlobalInfoDialog>("Exam_GlobalInfo", parameters);
var result = await dialog.Result;
if (!result.Canceled)
{
}
StateHasChanged();
}
}
<!-- #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}" @page "/exam/edit/{ExamId}"
@using Entities.DTO @using Entities.DTO
@using TechHelper.Client.Pages.Exam.ExamView
@using TechHelper.Client.Services
@using Entities.DTO
@using TechHelper.Client.Exam @using TechHelper.Client.Exam
<ExamView ParsedExam="@ExamDto"/> <ExamView ParsedExam="@ExamDto"/>
@@ -17,7 +20,7 @@
[Inject] [Inject]
private ISnackbar Snackbar { get; set; } private ISnackbar Snackbar { get; set; }
private ExamDto ExamDto { get; set; } private AssignmentDto ExamDto { get; set; }
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
@@ -28,7 +31,7 @@
try try
{ {
var result = await ExamService.GetExam(parsedExamId); var result = await ExamService.GetExam(parsedExamId);
if (result.Status) ExamDto = result.Result as ExamDto ?? new ExamDto(); if (result.Status) ExamDto = result.Result as AssignmentDto ?? new AssignmentDto();
} }
catch (Exception ex) catch (Exception ex)
{ {

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 Entities.DTO
@using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Authorization
@using TechHelper.Client.Exam @using TechHelper.Client.Exam
@using TechHelper.Client.Pages.Common.Exam
@page "/exam/manage" @page "/exam/manage"
@using Entities.DTO
@using TechHelper.Client.Services
@attribute [Authorize] @attribute [Authorize]
@@ -19,7 +21,6 @@ else
<MudPaper Class="d-flex flex-wrap flex-grow-0 gap-4" Height="100%" Width="100%"> <MudPaper Class="d-flex flex-wrap flex-grow-0 gap-4" Height="100%" Width="100%">
@foreach (var item in examDtos) @foreach (var item in examDtos)
{ {
<ExamPreview examDto="item" Width="256px" Height="256px"> </ExamPreview>
} }
</MudPaper> </MudPaper>
@@ -33,7 +34,7 @@ else
[CascadingParameter] [CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; } private Task<AuthenticationState> authenticationStateTask { get; set; }
private List<ExamDto> examDtos = new List<ExamDto>(); private List<AssignmentDto> examDtos = new List<AssignmentDto>();
private bool isloding = true; private bool isloding = true;
@@ -47,10 +48,17 @@ else
{ {
isloding = true; isloding = true;
Snackbar.Add("正在加载", Severity.Info); Snackbar.Add("正在加载", Severity.Info);
var result = await ExamService.GetAllExam(authenticationStateTask.Result.User.Identity.Name); var result = await ExamService.GetAllExam();
examDtos = result.Result as List<ExamDto> ?? new List<ExamDto>(); if (result.Status)
{
examDtos = result.Result as List<AssignmentDto> ?? new List<AssignmentDto>();
Snackbar.Add("加载成功", Severity.Info);
}
else
{
Snackbar.Add($"加载失败 {result.Message}", Severity.Error);
}
isloding = false; isloding = false;
Snackbar.Add("加载成功", Severity.Info);
StateHasChanged(); 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="overflow-hidden " Style="background-color:pink" Width="@Width" Height="@Height" MaxHeight="@MaxHeight" MaxWidth="@MaxWidth">
<MudPaper Class="d-flex flex-column flex-grow-1 justify-content-between" Height="100%" Style="background-color:green"> <MudPaper Class="d-flex flex-column flex-grow-1 justify-content-between" Height="100%" Style="background-color:green">
<MudText Typo="Typo.body2"> @examDto.AssignmentTitle </MudText> <MudText Typo="Typo.body2"> @AssignmentDto.Title </MudText>
<MudPaper> <MudPaper>
@@ -12,7 +12,11 @@
<MudButton OnClick="ExamClick"> 详情 </MudButton> <MudButton OnClick="ExamClick"> 详情 </MudButton>
<MudIconButton Icon="@Icons.Material.Filled.Delete" aria-label="delete" /> <MudIconButton Icon="@Icons.Material.Filled.Delete" aria-label="delete" />
<MudIconButton Icon="@Icons.Custom.Brands.GitHub" OnClick="CheckExam" Color="Color.Primary" aria-label="github" /> @if (bteacher)
{
<MudIconButton Icon="@Icons.Material.Filled.Check" OnClick="CheckExam" Color="Color.Primary" aria-label="github" />
}
<MudIconButton Icon="@Icons.Material.Filled.Favorite" Color="Color.Secondary" aria-label="add to favorite" /> <MudIconButton Icon="@Icons.Material.Filled.Favorite" Color="Color.Secondary" aria-label="add to favorite" />
</MudButtonGroup> </MudButtonGroup>
</MudPaper> </MudPaper>
@@ -21,12 +25,16 @@
</MudPaper> </MudPaper>
@code { @code {
[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }
private bool bteacher = false;
[Inject] [Inject]
public NavigationManager navigationManager { get; set; } public NavigationManager navigationManager { get; set; }
[Parameter] [Parameter]
public ExamDto examDto { get; set; } public AssignmentDto AssignmentDto { get; set; }
[Parameter] [Parameter]
@@ -45,13 +53,19 @@
public string? MaxHeight { get; set; } = "64"; public string? MaxHeight { get; set; } = "64";
protected override Task OnInitializedAsync()
{
bteacher = authenticationStateTask.Result.User.IsInRole("Teacher");
return base.OnInitializedAsync();
}
private void ExamClick() private void ExamClick()
{ {
navigationManager.NavigateTo($"exam/edit/{examDto.AssignmentId}"); navigationManager.NavigateTo($"exam/edit/{AssignmentDto.Id}");
} }
private void CheckExam() private void CheckExam()
{ {
navigationManager.NavigateTo($"exam/check/{examDto.AssignmentId}"); navigationManager.NavigateTo($"exam/check/{AssignmentDto.Id}");
} }
} }

View File

@@ -0,0 +1,115 @@
@using Entities.Contracts
@using Entities.DTO
@using Newtonsoft.Json
@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>
<MudChip T="string" Color="Color.Warning" Class="justify-content-end">@(ExamStruct.QType == string.Empty ? "" : QuestionTypes[ExamStruct.QType].DisplayName)</MudChip>
@if(ExamStruct.Question!=null)
{
<MudRating SelectedValue="@((int)ExamStruct.Question.DifficultyLevel)" ReadOnly="true" Size="Size.Small" />
}
</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";
Dictionary<string, QuestionDisplayTypeData> QuestionTypes = new Dictionary<string, QuestionDisplayTypeData>();
[Inject]
private ILocalStorageService LocalStorageService { get; set; }
protected override void OnInitialized()
{
base.OnInitialized();
var cs = LocalStorageService.GetItem<string>("GlobalInfo");
var GlobalInfo = JsonConvert.DeserializeObject<Dictionary<string, QuestionDisplayTypeData>>(cs);
if (GlobalInfo != null)
{
QuestionTypes = GlobalInfo;
}
}
private async void HandleClick()
{
await ClickedStruct.InvokeAsync(ExamStruct);
}
private async void HandleChildStructClick(AssignmentQuestionDto clickedChildExamStruct)
{
await ClickedStruct.InvokeAsync(clickedChildExamStruct);
}
private void HandleSelected(int num)
{
ExamStruct.Question.DifficultyLevel = (DifficultyLevel)num;
}
}

View File

@@ -4,11 +4,11 @@
@if (ParsedExam != null) @if (ParsedExam != null)
{ {
<MudPaper Height="@Height" Class="@Class" Style="@Style" Width="@Width"> <MudPaper Height="@Height" Class="@Class" Style="@Style" Width="@Width" Elevation="5">
<MudText Class="d-flex justify-content-center" Typo="Typo.h6"> @ParsedExam.AssignmentTitle </MudText> <MudText Class="d-flex justify-content-center" Typo="Typo.button"> <b> @ParsedExam.Title </b></MudText>
<MudText Typo="Typo.body1"> @ParsedExam.Description </MudText> <MudText Typo="Typo.body1"> @ParsedExam.Description </MudText>
<ExamGroupView MajorQGList="@ParsedExam.QuestionGroups.SubQuestionGroups" Elevation="1" Class="ma-0 pa-2" /> <ExamStructView ExamStruct="@ParsedExam.ExamStruct" Elevation="0" ClickedStruct="HandleClickedStruct" Class="ma-0 pa-2 rounded-xl" />
</MudPaper> </MudPaper>
} }
@@ -24,7 +24,11 @@ else
@code { @code {
[Parameter] [Parameter]
public ExamDto ParsedExam { get; set; } = new ExamDto(); public AssignmentDto ParsedExam { get; set; } = new AssignmentDto();
[Parameter]
public EventCallback<AssignmentQuestionDto> ClickedStruct { get; set; }
[Parameter] [Parameter]
public string Height { get; set; } = "100%"; public string Height { get; set; } = "100%";
[Parameter] [Parameter]
@@ -33,4 +37,10 @@ else
public string Class { get; set; } = ""; public string Class { get; set; } = "";
[Parameter] [Parameter]
public string Style { get; set; } = ""; public string Style { get; set; } = "";
private void HandleClickedStruct(AssignmentQuestionDto dto)
{
ClickedStruct.InvokeAsync(dto);
}
} }

View File

@@ -1,7 +1,34 @@
@page "/exam" @page "/exam"
@using TechHelper.Client.Pages.Student.BaseInfoCard
@inject NavigationManager NavigationManager
<AuthorizeView Roles="Teacher">
<Authorized>
<MudPaper Class="rounded-xl ma-2 px-2 overflow-auto w-100 h-100">
<StudentSubmissionPreviewTableCard />
</MudPaper>
</Authorized>
</AuthorizeView>
<MudText>HELLO WORLD</MudText> <AuthorizeView Roles="Student">
<Authorized>
<MudPaper Class="rounded-xl ma-2 px-2 overflow-auto w-100 h-100">
<StudentSubmissionPreviewTableCard />
</MudPaper>
</Authorized>
</AuthorizeView>
@code { @code {
[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }
protected override void OnParametersSet()
{
if (authenticationStateTask is null)
{
NavigationManager.Refresh(forceReload: true);
}
}
} }

View File

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

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,68 @@
@using Entities.DTO
@using Entities.Contracts
@using Newtonsoft.Json
@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>
<MudChipSet T="string" SelectedValue="@Question.QType" CheckMark SelectionMode="SelectionMode.SingleSelection" SelectedValueChanged="HandleQTSelectedValueChanged">
@foreach (var item in QuestionTypes)
{
var qt = item;
@* Style = "@($"background - color:{ item.Value.Color} ")"*@
<MudChip Style="@(qt.Key == Question.QType ?
$"background-color:#ffffff; color:{item.Value.Color}" :
$"background-color:{item.Value.Color}; color:#ffffff")"
Value="@item.Key">
@item.Value.DisplayName
</MudChip>
}
</MudChipSet>
<MudRating SelectedValue="@(diffi)" SelectedValueChanged="HandleSelected" Size="Size.Small" />
<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();
public int diffi = 0;
Dictionary<string, QuestionDisplayTypeData> QuestionTypes = new Dictionary<string, QuestionDisplayTypeData>();
[Inject]
private ILocalStorageService LocalStorageService { get; set; }
protected override void OnInitialized()
{
base.OnInitialized();
var cs = LocalStorageService.GetItem<string>("GlobalInfo");
var GlobalInfo = JsonConvert.DeserializeObject<Dictionary<string, QuestionDisplayTypeData>>(cs);
if (GlobalInfo != null)
{
QuestionTypes = GlobalInfo;
}
}
private void HandleSelectedValueChanged(QuestionType type)
{
Question.Type = type;
}
private void HandleSelected(int num)
{
Question.DifficultyLevel = (DifficultyLevel)num;
}
private void HandleQTSelectedValueChanged(string type)
{
Question.QType = type;
StateHasChanged();
}
}

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

@@ -0,0 +1,4 @@

@code {
}

View File

@@ -0,0 +1,32 @@
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject NavigationManager Navigation
@inject IAuthenticationClientService AuthenticationClientService
<AuthorizeView>
<Authorized>
<MudText>
Hello, @context.User.Identity.Name!
</MudText>
<MudButton OnClick="Logout"> LOGOUT </MudButton>
</Authorized>
<NotAuthorized>
<MudButton Class="" Href="Login"> Login </MudButton>
</NotAuthorized>
</AuthorizeView>
@code {
[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }
private async Task Logout()
{
await AuthenticationClientService.LogoutAsync();
Navigation.NavigateTo("/");
}
private void LoginIN()
{
Navigation.NavigateToLogin("/login");
}
}

View File

@@ -0,0 +1,11 @@
<MudPaper Class="d-flex flex-row my-3" Height="@Height" Width="@Width" Elevation="0">
<MudIcon Icon="@Icons.Custom.Brands.MudBlazor" Color="Color.Primary" />
<MudText Class="mx-3"><b>TechHelper</b></MudText>
</MudPaper>
@code {
[Parameter]
public string Height { get; set; } = "30px";
[Parameter]
public string Width { get; set; } = "100%";
}

View File

@@ -0,0 +1,8 @@
<MudPaper Class="d-flex flex-grow-1 rounded-xl pl-6" Elevation="0">
<MudTextField @bind-Value="TextValue" Label="Search for everything" Variant="Variant.Text"></MudTextField>
<MudIconButton Icon="@Icons.Material.Filled.Search"></MudIconButton>
</MudPaper>
@code {
public string TextValue { get; set; }
}

View File

@@ -0,0 +1,35 @@
@inherits ErrorBoundary
@inject ISnackbar Snackbar
@if (CurrentException is null)
{
@ChildContent
}
else if (ErrorContent is not null)
{
@ErrorContent(CurrentException)
}
else
{
<div class="custom-error-ui">
<MudAlert Severity="Severity.Error" Icon="@Icons.Material.Filled.Error">
<MudText>组件加载或执行时出现了问题。</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Class="mt-3">
重试
</MudButton>
</MudAlert>
</div>
}
@code {
protected override async Task OnErrorAsync(Exception exception)
{
Snackbar.Add("操作失败,请重试或联系管理员。", Severity.Error);
await base.OnErrorAsync(exception);
}
}

View File

@@ -1,118 +1,19 @@
@page "/" @page "/"
@using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Authorization
@using TechHelper.Client.Pages.Common.Exam
<AuthorizeView Roles="Administrator"> <AuthorizeView Roles="Student">
<Authorized>
<MudText> Hello @context.User.Identity.Name</MudText> <TechHelper.Client.Pages.Student.HomePage />
@foreach (var item in context.User.Claims) </Authorized>
{
<MudPaper class="ma-2 pa-2">
<MudText> @item.Value </MudText>
<MudText> @item.Issuer </MudText>
<MudText> @item.Subject </MudText>
<MudText> @item.Properties </MudText>
<MudText> @item.ValueType </MudText>
</MudPaper>
}
Welcome to your new app.
</AuthorizeView> </AuthorizeView>
@code {
[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }
<MudText>Hello </MudText> protected override Task OnInitializedAsync()
<MudText>Hello </MudText> {
<MudText>Hello </MudText> return base.OnInitializedAsync();
<MudText>Hello </MudText> Console.WriteLine(authenticationStateTask.Result.User.IsInRole("Student"));
<MudText>Hello </MudText> }
<MudText>Hello </MudText> }
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>
<MudText>Hello </MudText>

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("Grade")?.Value.ToString() </MudText>
<MudText Class="ma-3 pa-3"> 班级 : @authenticationStateTask.Result.User.FindFirst("Class")?.Value.ToString() </MudText> <MudText Class="ma-3 pa-3"> 班级 : @authenticationStateTask.Result.User.FindFirst("Class")?.Value.ToString() </MudText>
</MudPaper> </MudPaper>
} }
<RestoreUserRole></RestoreUserRole>

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
<MudPaper Class="d-flex my-3 flex-column justify-content-center mx-auto" Height="@Height" Width="@Width" Elevation="0">
<MudImage Width="150" Height="150" Class="rounded-pill justify-content-center" Src="ref/Keda.png"></MudImage>
<MudText Class="mx-3"><b>TechHelper</b></MudText>
</MudPaper>
@code {
[Parameter]
public string Height { get; set; } = "250px";
[Parameter]
public string Width { get; set; } = "100%";
}

View File

@@ -0,0 +1,28 @@
@using static TechHelper.Client.Pages.Student.BaseInfoCard.StudentSubmissionPreviewTableCard
@if(StudentSubmission!=null)
{
<MudPaper Class="ma-2 pa-2 rounded-xl d-flex w-100 flex-nowrap">
<MudText Class="flex-grow-0 flex-shrink-0" Style="width:60%"> @StudentSubmission.StudentName </MudText>
<MudText Class="flex-grow-0 flex-shrink-0 text-start" Style="width:10%"> @StudentSubmission.TotalProblems </MudText>
<MudText Class="flex-grow-0 flex-shrink-0 text-start" Style="width:10%"> @StudentSubmission.ErrorCount </MudText>
<MudText Class="flex-grow-0 flex-shrink-0 text-start" Style="width:10%"> @StudentSubmission.TimeSpent </MudText>
<MudText Class="flex-grow-0 flex-shrink-0 text-start" Style="width:10%"> @StudentSubmission.Score </MudText>
</MudPaper>
}
else
{
<MudPaper Class="ma-1 pa-2 rounded-xl d-flex w-100 flex-nowrap">
<MudText Class="flex-grow-0 flex-shrink-0" Style="width:60%"> 名称 </MudText>
<MudText Class="flex-grow-0 flex-shrink-0 text-start" Style="width:10%"> 题目总数 </MudText>
<MudText Class="flex-grow-0 flex-shrink-0 text-start" Style="width:10%"> 错误总数 </MudText>
<MudText Class="flex-grow-0 flex-shrink-0 text-start" Style="width:10%"> 时间 </MudText>
<MudText Class="flex-grow-0 flex-shrink-0 text-start" Style="width:10%"> 得分 </MudText>
</MudPaper>
}
@code{
[Parameter]
public StudentSubmission StudentSubmission{ get; set; }
}

View File

@@ -0,0 +1,97 @@
@using TechHelper.Client.Services
@inject IStudentSubmissionService StudentSubmissionService
<MudPaper Class="ma-2 pa-2 rounded-xl d-flex flex-column flex-grow-1 overflow-auto" MaxHeight="100%">
<StudentSubmissionPreviewCard />
@if (_isLoading)
{
<div class="d-flex justify-content-center align-items-center" style="height: 200px;">
<MudProgressCircular Color="Color.Primary" Size="Size.Large" />
</div>
}
else if (_studentSubmissions == null || _studentSubmissions.Count == 0)
{
<div class="d-flex justify-content-center align-items-center" style="height: 200px;">
<MudText TextColor="Color.TextSecondary" Align="Align.Center">暂无提交记录</MudText>
</div>
}
else
{
@foreach (var submission in _studentSubmissions)
{
<StudentSubmissionPreviewCard StudentSubmission="@submission" />
}
}
</MudPaper>
@code {
// 学生提交数据模型
public class StudentSubmission
{
public string StudentName { get; set; }
public int TotalProblems { get; set; }
public int ErrorCount { get; set; }
public DateTime CreatedDate { get; set; }
public float Score { get; set; }
public string AssignmentName { get; set; }
public string Status { get; set; }
public TimeSpan TimeSpent { get; set; }
}
// 学生提交列表
private List<StudentSubmission> _studentSubmissions = new();
private bool _isLoading = true;
protected override async Task OnInitializedAsync()
{
await LoadStudentSubmissions();
}
private async Task LoadStudentSubmissions()
{
try
{
_isLoading = true;
StateHasChanged();
var result = await StudentSubmissionService.GetMySubmissionsAsync();
if (result.Status && result.Result != null)
{
// 从服务器获取的数据映射到我们的模型
var submissions = result.Result as List<Entities.DTO.StudentSubmissionSummaryDto>;
if (submissions != null)
{
_studentSubmissions = submissions.Select(submission => new StudentSubmission
{
AssignmentName = submission.AssignmentName,
CreatedDate = submission.CreatedDate,
ErrorCount = submission.ErrorCount,
Score = submission.Score,
StudentName = submission.StudentName,
Status = submission.Status,
TotalProblems = submission.TotalQuestions,
TimeSpent = TimeSpan.FromMinutes(30) // 默认值,实际应用中可以从服务器获取
}).ToList();
}
}
else
{
// 如果API调用失败使用空列表
_studentSubmissions = new List<StudentSubmission>();
}
}
catch (Exception ex)
{
// 处理异常,可以记录日志
_studentSubmissions = new List<StudentSubmission>();
}
finally
{
_isLoading = false;
StateHasChanged();
}
}
}

View File

@@ -0,0 +1,79 @@
@using MudBlazor
@using System.Collections.Generic
<MudDataGrid Items="@Elements.Take(4)" Hover="@_hover" Dense="@_dense" Striped="@_striped" Bordered="@_bordered"
RowStyleFunc="@_rowStyleFunc" RowClass="my-2 rounded-xl">
<Columns >
<PropertyColumn Property="x => x.Number" Title="Nr" />
<PropertyColumn Property="x => x.Sign" />
<PropertyColumn Property="x => x.Name" CellStyleFunc="@_cellStyleFunc" />
<PropertyColumn Property="x => x.Position" />
<PropertyColumn Property="x => x.Molar" Title="Molar mass" />
</Columns>
</MudDataGrid>
<div class="d-flex flex-wrap mt-4">
<MudSwitch @bind-Value="_hover" Color="Color.Primary">Hover</MudSwitch>
<MudSwitch @bind-Value="_dense" Color="Color.Secondary">Dense</MudSwitch>
<MudSwitch @bind-Value="_striped" Color="Color.Tertiary">Striped</MudSwitch>
<MudSwitch @bind-Value="_bordered" Color="Color.Warning">Bordered</MudSwitch>
</div>
@code {
// Element类定义
public class Element
{
public int Number { get; set; }
public string Sign { get; set; }
public string Name { get; set; }
public int Position { get; set; }
public decimal Molar { get; set; }
}
// 示例数据
private IEnumerable<Element> Elements = new List<Element>
{
new Element { Number = 1, Sign = "H", Name = "Hydrogen", Position = 1, Molar = 1.008m },
new Element { Number = 2, Sign = "He", Name = "Helium", Position = 0, Molar = 4.0026m },
new Element { Number = 3, Sign = "Li", Name = "Lithium", Position = 1, Molar = 6.94m },
new Element { Number = 4, Sign = "Be", Name = "Beryllium", Position = 2, Molar = 9.0122m },
new Element { Number = 5, Sign = "B", Name = "Boron", Position = 13, Molar = 10.81m }
};
private bool _hover;
private bool _dense;
private bool _striped;
private bool _bordered;
// 行样式函数Position为0的行显示为斜体
private Func<Element, int, string> _rowStyleFunc => (x, i) =>
{
if (x.Position == 0)
return "font-style:italic";
return "";
};
// 单元格样式函数:根据元素编号设置背景色,根据摩尔质量设置字体粗细
private Func<Element, string> _cellStyleFunc => x =>
{
string style = "";
if (x.Number == 1)
style += "background-color:#8CED8C"; // 浅绿色
else if (x.Number == 2)
style += "background-color:#E5BDE5"; // 浅紫色
else if (x.Number == 3)
style += "background-color:#EACE5D"; // 浅黄色
else if (x.Number == 4)
style += "background-color:#F1F165"; // 浅黄色
if (x.Molar > 5)
style += ";font-weight:bold";
return style;
};
}

Some files were not shown because too many files have changed in this diff Show More