添加项目文件。

This commit is contained in:
SpecialX
2025-05-23 19:03:00 +08:00
parent 6fa7679fd3
commit d36fef2bbb
185 changed files with 13413 additions and 0 deletions

View File

@@ -0,0 +1,108 @@
namespace TechHelper.Client.AI
{
public static class AIConfiguration
{
public static string APIkey = "c62e38f2b74e47a080487a7a2fef014a.Q6Gx5HBy6Pj0OhkI";
public static string ExamAnsConfig = "你是一个专业的试题生成助手,请根据提供的科目、年级和主题," +
"严格按照下方指定的 JSON 格式和内容生成规则,输出高质量的试题数据。\r\n\r\n---\r\n**输出格式要求:" +
"**\r\n\r\n请严格输出一个 **JSON 数组**。数组的每个元素都是一个**试题对象**。\r\n\r\n```json\r\n[\r\n " +
"{\r\n \"题号\": \"X\",\r\n \"标题\": \"XXXXXX\",\r\n \"分值\": N,\r\n \"分值问题标记\": \"(若" +
"分值存在问题,请在此处简要说明,例如:'该题分值分配需复核';否则,此字段留空)\",\r\n \"题目引用\": \"(若" +
"为阅读理解等引用原文的题型,请在此处补充完整原文内容,并支持换行,例如:'(一)葡萄沟(节选)\\\\n葡萄种在山坡上" +
"的梯田里...';否则,此字段留空)\",\r\n \"子题目\": [\r\n {\r\n \"子题号\": \"X\",\r\n " +
" \"题干\": \"XXXXXXX独立成题例如'一( )大象';或拼音题:'pú táo ';或成语填空题的单个成语:' )心( )意'\",\r\n " +
" \"分值\": N,\r\n \"分值问题标记\": \"(若子题分值存在问题,请在此处简要说明,例如:'该子题分值偏高';否则,此字段留空)\",\r\n " +
" \"选项\": [\"A\", \"B\", \"C\"],\r\n \"示例答案\": \"XXXXXXX\"\r\n }\r\n ],\r\n \"子" +
"题组\": []\r\n }\r\n]\r\n内容生成规则\r\n\r\n强制拆分独立考察点\r\n核心规则 如果一个概念题干中包含多个独立的、" +
"可分别作答或评分的部分(例如:多个量词填空、每个成语填空、每个拼音写词语),必须将其拆分为各自独立的 子题目 对象。\r\n示" +
"例:\r\n原题干“一 )大象 一( )大船 一( )菜叶” → 拆分为 3 个 子题目。\r\n原题干“看拼音写词语pú táo 、xiāng " +
"jiāo )” → 拆分为 2 个 子题目。\r\n每个拆分后的 子题目 必须有独立的 子题号 和 分值。\r\n合并重复题号\r\n在 JSON 数组中,如果多" +
"个题组(如阅读理解下的不同文段 (一)、(二))属于同一大题,它们可以有相同的 题号 字段值。但每个题组必须是 JSON 数组中的一个独" +
"立对象。\r\n题目引用完整性\r\n若有引用文本如阅读理解的原文必须在 题目引用 字段中补充完整原文内容。支持 \\n 进行换行。如果" +
"无引用文本,此字段留空 \"\"。\r\n分值分配与转换\r\n所有 子题目 的 分值 总和必须等于对应大题QuestionGroup的 分值。\r\n如果原始" +
"分值以百分比表示,必须将其转换为具体的数值。\r\n选择题的每个选项的分值必须相同。\r\n标点规范\r\n所有中文标点统一使用全角符号。\r\n长文" +
"本换行:\r\n题目引用、题干 等长文本内容应自动换行,每行不超过 80 个字符(使用 \\n。\r\n特殊处理要求\r\n\r\n选择题 (选项 字段" +
"不为空)\r\n选项 字段必须包含一个字符串数组,格式为 [\"选项A内容\", \"选项B内容\", \"选项C内容\"]。\r\n示例答案 字段必须提供一个具体" +
"的示例答案。\r\n填空题 (选项 字段为空)\r\n示例答案 字段必须提供一个具体的示例答案(例如:“纷纷扬扬”)。\r\n阅读理解统计题例如句" +
"数/小节数): 需在 题干 中明确标注单位。\r\n主旨题若需多选 请在 题干 描述中清晰体现其多选特性。\r\n子题组 字段: 在当前 JSON 结构中,子" +
"题组 字段应始终保持为空数组 [],除非我另行通知需要更深层次的题组嵌套。\r\n格式验证要求模型内部验证确保合规性\r\n\r\n题目引用 字" +
"段若为空字符串 \"\",表示该题型无原文引用。\r\n分值问题标记 字段若不为空字符串 \"\",表示该分值需人工复核,模型无需自动修改分值。\r\n禁止 " +
"子题目 与 题目引用 以外的内容混杂在 题干 或 标题 中。\r\n每个 子题目 必须独立成题,具备独立的 分值 和 题干。\r\nJSON 语法必须正确," +
"能够通过任何在线 JSON 校验工具的验证。\r\n所有分值分配合理子题目 总分等于大题 分值),且百分比分值已转换为具体数值。\r\n所有标" +
"点符号 100% 使用中文全角。\r\n长文本自动换行每行不超过 80 字符,使用 \\n。\r\n题号 合并与 子题目 区分应通过 JSON 数组中的独立对象体现。";
public static string ExamToXML = "文本试卷解析请求模板 你只需要给出转换后的结果,不需要其他任何无关的输出\r\n请将我提供的" +
"文本试卷内容,按照以下精简版 XAML 风格的标记规则进行解析和结构化输出。\r\n\r\n输出" +
"格式要求:\r\n请以精简版 XAML 风格的文本标记形式输出,并确保以下标签和属性的正确" +
"使用:\r\n\r\n根元素: <EP>\r\n大题: <QG Id=\"X\" T=\"标题\" S=\"分值\" SPM=\"分值问" +
"题标记\"/>\r\nId: 大题题号(应从文本中提取)。\r\nT: 大题标题(应从文本中提取)。\r\nS: 大题总分" +
"(应从文本中提取,百分比请转换为具体数值)。\r\nSPM: 分值问题标记(如果文本中无则为空字符串 \"\";如果有任何" +
"分值分配上的疑问或需要复核,请在此标记)。\r\n题目引用: <QR>引用文本</QR> (如果原始文本中无引用部分,则省" +
"略此标签)\r\n子题目列表容器: <SQs>\r\n子题目: <SQ Id=\"X\" T=\"题干\" S=\"分值\" SPM=\"分值问题标记\" SA=\"示例" +
"答案\"/>\r\nId: 子题号(应从文本中提取,例如 \"1.1\", \"2.3a\")。\r\nT: 子题目题干。\r\n【核心解析规则】 除了明显的拼" +
"写和成语填空类题目外,所有子题的 T 属性都必须包含完整的原始题目表述,包括所有选项、括号等,以最大程度地保留原始文本结构。\r\nS: 子题" +
"分值(应从文本中提取)。\r\nSPM: 分值问题标记(如果文本中无则为空字符串 \"\";如果有任何分值分配上的疑问或需要复核,请在此标记)。\r\nSA: 示例" +
"答案。\r\n对于有多个填空或判断的题目其 T 属性包含多个空位),答案请用空格分隔,并按题干中出现的顺序排列。\r\n示例: SA=\"× √ √\" (对应多" +
"个判断)\r\n示例: SA=\"“ 哪 ” 。\" (对应多个标点填空)\r\n选项列表容器: <Os> (仅用于原始文本中明确给出选项的选择题,如果无选项则省" +
"略此标签)\r\n选项: <O V=\"选项值\"/> (选项值应从原始文本中提取)\r\n子题组列表容器: <SQGs/> (如果原始文本中无嵌套题组,则为空标签)\r\n内容解" +
"析规则:\r\n准确识别大题与子题: 根据题号、标题和分值模式,准确识别并划分大题 (<QG>) 和其下的子题目 (<SQ>)。\r\n细致拆分独立" +
"考察点: 将文本中所有可独立评分或作答的部分,尽可能地拆分为独立的 <SQ> 标记块。但请注意,对于单个逻辑题但包含多个填空/选项/判断点(如填空" +
"题和判断题),请将所有相关内容合并到一个 <SQ> 的 T 属性中,并提供序列化的 SA。\r\n提取题目引用: 识别阅读理解等题型中的引用段落,并" +
"将其完整放入 <QR> 标签中。\r\n精确提取分值: 从原文中提取大题和子题的分值,并将其转换为阿拉伯数字。如果原文是百分比,请转换为具体分数。\r\n规范标" +
"点: 确保所有中文标点在输出中统一使用全角符号。";
public static string ExamToXML2 = "文本试卷解析请求模板 你只需要给出转换后的结果,不需要其他任何无关的输出 " +
" 1. 整体结构与根元素\r\n<EP> (试卷)\r\n\r\n根元素表示一份完整的试卷。\r\n作为最外层的容器所有试卷内容都" +
"将嵌套在其内部。\r\n没有属性其直接子元素必须是 <QGs>。\r\n<QGs> (题组集合)\r\n\r\n作为 <EP> 的直接子" +
"元素。\r\n没有属性其内容将是一个或多个 <QG> 标签。\r\n2. 大题/题组的标记 (<QG>)\r\n<QG Id=\"X\" T=\"标题\" S=\"分值\" " +
"SPM=\"分值问题标记\" QR=\"引用文本\"/>\r\n目的用于标记试卷中的每一个“大题”或“题组”。\r\n属性要求\r\nId必填。从文本" +
"中提取的大题题号例如“一”、“I”、“1.”)。\r\nT必填。从文本中提取的大题标题例如“选择题”、“填空题”。\r\nS必" +
"填。从文本中提取的大题总分。\r\n转换规则如果原始文本是百分比例如“20%”请转换为具体数值例如“20”。\r\nSPM可" +
"选。\r\n如果文本中没有特殊标记则为空字符串 \"\"。\r\n用途用于标记在分值分配上存在疑问或需要复核的大题。\r\nQR可选。\r\n用" +
"途:如果原始文本中包含阅读理解、材料分析等需要引用的段落,请将完整的引用文本内容放入此属性。\r\n省略规则如果原始文本中没有引用" +
"部分,则整个 QR 属性应被省略(不要保留空属性或空标签)。\r\n子元素\r\n一个 <QG> 标签内部可以包含 <SQs>(用于普通子题)或 <SQGs>(用" +
"于嵌套题组)。两者不能同时存在。\r\n3. 子题目与选项的标记 (<SQ> & <O>)\r\n<SQs> (子题目列表容器)\r\n\r\n目的作为 <QG> 的子元素,用" +
"于封装一个大题下的所有独立小题。\r\n没有属性其内容将是一个或多个 <SQ> 标签。\r\n<SQ Id=\"X\" T=\"题干\" S=\"分值\" SPM=\"分值问题标" +
"记\" SA=\"示例答案\"/> (子题目)\r\n\r\n目的用于标记试卷中的每一个“小题”。\r\n属性要求\r\nId必填。从文本中提取的子题号例" +
"如“1.1”、“2.3a”、“(1)”)。\r\nT必填。子题目的完整题干内容。\r\n核心解析规则除了明显的单个填空例如“C#是一个____语言。”和" +
"单个判断类题目(例如:“是/否判断题。”)外,所有子题的 T 属性必须包含完整的原始题目表述,包括所有选项文字、括号、多个空位等,以最大程度" +
"地保留原始文本结构。\r\nS必填。从文本中提取的子题分值。\r\nSPM可选。规则同 <QG> 中的 SPM 属性。\r\nSA必填。小题的示例答案。\r\n多" +
"空/多判断:对于包含多个空位或判断点的题目,答案请用空格分隔,并按照题干中出现的顺序排列。\r\n示例SA=\"× √ √\" (对应多个判断)\r\n示" +
"例SA=\"“ 哪 ” 。\" (对应多个标点填空)\r\n示例SA=\"北京 长城\" (对应两个填空)\r\n选择题填写正确选项的字母或编号例如 SA=\"A\" 或" +
" SA=\"A,C\"。\r\n单个填空/判断题:直接填写答案,例如 SA=\"C#\" 或 SA=\"正确\"。\r\n<Os> (选项列表容器)\r\n\r\n目的作为 <SQ> 的子" +
"元素,用于封装选择题的选项。\r\n限制仅用于原始文本中明确给出选项的选择题。\r\n省略规则如果原始文本中无选项例如填空题、简答题" +
"则整个 <Os> 标签应被省略。\r\n没有属性其内容将是一个或多个 <O> 标签。\r\n<O V=\"选项值\"/> (选项)\r\n\r\n目的标记单个选项。\r\n属" +
"性要求:\r\nV必填。选项的完整内容应从原始文本中提取包括选项的字母/编号例如“A. 选项A”、“B) 选项B”。\r\n4. 子题组的" +
"标记 (<SQGs>)\r\n<SQGs> (子题组列表容器)\r\n目的作为 <QG> 的子元素,用于表示嵌套的题组结构(例如:一个大题下面又包含几个小的大" +
"题组)。\r\n省略规则如果原始文本中无嵌套题组则整个 <SQGs> 标签应被省略。\r\n没有属性其内容将是一个或多个嵌套的 <QG> 元素。\r\n注意嵌" +
"套的 <QG> 结构与顶层的 <QG> 相同,可以递归包含 <SQs> 或 <SQGs>。\r\n5. 核心内容解析与转换规范\r\n识别与划分\r\n优先识别大题 (<QG>),然后" +
"识别其下的子题目 (<SQ>)。识别依据主要是题号、标题和分值模式。\r\n独立考察点\r\n原则将文本中所有可独立评分或作答的部分尽可能地拆分" +
"为独立的 <SQ> 标记块。\r\n例外对于单个逻辑题但包含多个填空/选项/判断点(如一个句子中有多个空需要填写,或一个问题下有多个判断题),请将所" +
"有相关内容合并到同一个 <SQ> 的 T 属性中,并提供序列化的 SA。\r\n分值提取\r\n从原文中精确提取大题和子题的分值并将其转换为阿拉伯数字。\r\n如果原" +
"文是百分比,务必转换为具体分数。\r\n标点规范\r\n确保所有中文标点在输出的XML文本中统一使用全角符号。";
public static string RecorrectXML = "按下面的要求校验XML文本,如果有错误修正他,没有错误则直接返回,你只需要给出修正或原来的结果,不需要其他任何无" +
"关的输出, 要求:请检查这段 XML 代码的语法是否正确,包括标签的开闭、嵌套和属性的引号。请检查 XML 数据中是否存在逻辑错误或不一" +
"致的地方。XML 中的数据类型是否符合预期例如某个字段应该是数字却包含了文本。是否存在缺失的必需字段XML 中的数据值是否在合理的范围内?检查" +
"属性值是否有效且完整。 XML结构为<EP> (根元素,必需) 包含 <QGs> (必需)。<QGs> (容器,必需) 包含一个或多个 <QG>。<QG> (问题组,必需) 包含" +
"在 <QGs> 或嵌套的 <SQGs> 内部,具有 Id (必需), T (必需), S (必需), SPM (可选) 属性,可包含子元素 <QR> (可选), <SQs> (可选,包含一" +
"个或多个 <SQ>), <SQGs> (可选,包含一个或多个嵌套的 <QG>)。<SQs> (子题目容器,必需) 包含在 <QG> 内部,包含一个或多个 <SQ>。<SQ> (子题目,必需) 包含" +
"在 <SQs> 内部,具有 Id (必需), T (必需), S (必需), SPM (可选), SA (可选) 属性,可包含子元素 <Os> (可选,包含一个或多个 <O>)。<Os> (选项容器,必需) 包" +
"含在 <SQ> 内部,包含一个或多个 <O>。<O> (选项,必需) 包含在 <Os> 内部,具有 V (必需) 属性。";
public static string BreakQuestions = "请识别以下文本中的每一道大题。将其转换为XML格式你只需要给出转换后的结果,不需要其他任何无关的输出,其中 <EP> 是XML的根元素。用<Q> 和</Q> 标记来包裹每一道大题。请确保标记后的文本保持原始格式和内容。";
public static string ParseSignelQuestion = "请将以下提供的一道大题内容,转换为符合以下 C# 类结构的 XML 格式,你只需要给出转换后的结果,不需要其他任何无关的输出:" +
"XML 的根元素为 <QG>。并填充以下属性Id对应 QuestionGroup.Id从大题开头的题号中提取。T对应 QuestionGroup.Title" +
"从大题的标题中提取。S对应 QuestionGroup.Score从大题中识别的分值。<QR> 元素:对应 QuestionGroup.QuestionReference。**子题目" +
"SubQuestion**将作为 <QG> 内部的 <SQs> 元素列表中的 <SQ> 元素。如果大题下有子题目,则将它们包裹在 <SQs> 元素中。对于每个 <SQ> 元素," +
"填充以下属性Id对应 SubQuestion.SubId从子题号中提取。T对应 SubQuestion.Stem从子题目的题干中提取。S对应 SubQuestion.Score" +
"从子题目中识别的分值。SPM对应 SubQuestion.ScoreProblemMarker。SA对应 SubQuestion.SampleAnswer。**选项Option**将作为" +
" <SQ> 内部的 <Os> 元素列表中的 <O> 元素。如果子题目有选项,则将它们包裹在 <Os> 元素中。对于每个 <O> 元素,填充 V 属性V对应 Option.Value" +
"(从选项内容中提取)。嵌套题组:如果大题内部包含其他大题,则作为 <QG> 内部的 <SQGs> 元素列表中的 <QG> 元素(即嵌套 <QG>)。" +
"请确保生成的 XML 严格遵循上述结构和命名约定,以方便 C# XmlSerializer 进行反序列化。";
}
}

View File

@@ -0,0 +1,41 @@
using System.ComponentModel;
using System.Reflection;
namespace TechHelper.Client.AI
{
public enum AIModelsEnum
{
[Description("glm-4v-flash")]
GLM4VFlash,
[Description("cogview-3-flash")]
Cogview3Flash,
[Description("cogvideox-flash")]
CogVideoXFlash,
[Description("glm-4-flash-250414")]
GLM4Flash25,
[Description("glm-z1-flash")]
GLMZ1Flash
}
public static class EnumExtensions
{
public static string GetDescription(this Enum value)
{
FieldInfo field = value.GetType().GetField(value.ToString());
DescriptionAttribute attribute = Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) as DescriptionAttribute;
return attribute == null ? value.ToString() : attribute.Description;
}
}
public class AIModels
{
}
}

View File

@@ -0,0 +1,58 @@
using Entities.Contracts;
using Newtonsoft.Json;
namespace TechHelper.Client.AI
{
public class AiService : IAIService
{
private readonly GLMZ1Client _glmClient;
public AiService()
{
_glmClient = new GLMZ1Client(AIConfiguration.APIkey);
}
public async Task<string> CallGLM(string userContent, string AnsConfig, AIModelsEnum aIModels/* = AIModelsEnum.GLMZ1Flash*/)
{
string model = aIModels.GetDescription();
var request = new ChatCompletionRequest
{
Model = model,
Messages = new List<Message>
{
new UserMessage(AnsConfig + userContent)
}
};
try
{
var response = await _glmClient.ChatCompletionsSync(request);
if (response?.Choices != null && response.Choices.Count > 0)
{
string content = response.Choices[0].Message?.Content;
if (!string.IsNullOrEmpty(content))
{
// 移除 <think>...</think> 标签及其内容
int startIndex = content.IndexOf("<think>");
int endIndex = content.IndexOf("</think>");
if (startIndex != -1 && endIndex != -1 && endIndex > startIndex)
{
content = content.Remove(startIndex, endIndex - startIndex + "</think>".Length);
}
return content.Trim();
}
}
}
catch (HttpRequestException ex)
{
Console.WriteLine($"API 请求错误:{ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"发生未知错误:{ex.Message}");
}
return null;
}
}
}

View File

@@ -0,0 +1,217 @@
using Newtonsoft.Json;
using System.Text;
namespace TechHelper.Client.AI
{
public class Message
{
[JsonProperty("role")]
public string Role { get; set; }
[JsonProperty("content")]
public string Content { get; set; }
}
// 系统消息
public class SystemMessage : Message
{
public SystemMessage(string content)
{
Role = "system";
Content = content;
}
}
// 用户消息
public class UserMessage : Message
{
public UserMessage(string content)
{
Role = "user";
Content = content;
}
}
// 助手消息
public class AssistantMessage : Message
{
public AssistantMessage(string content)
{
Role = "assistant";
Content = content;
}
}
// GLM-Z1 Chat Completions API 请求体
public class ChatCompletionRequest
{
[JsonProperty("model")]
public string Model { get; set; }
[JsonProperty("messages")]
public List<Message> Messages { get; set; }
[JsonProperty("request_id")]
public string RequestId { get; set; }
[JsonProperty("do_sample")]
public bool? DoSample { get; set; }
[JsonProperty("stream")]
public bool? Stream { get; set; }
[JsonProperty("temperature")]
public float? Temperature { get; set; }
[JsonProperty("top_p")]
public float? TopP { get; set; }
[JsonProperty("max_tokens")]
public int? MaxTokens { get; set; }
[JsonProperty("stop")]
public List<string> Stop { get; set; }
[JsonProperty("user_id")]
public string UserId { get; set; }
}
// GLM-Z1 Chat Completions API 响应体
public class ChatCompletionResponse
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("created")]
public long Created { get; set; }
[JsonProperty("model")]
public string Model { get; set; }
[JsonProperty("choices")]
public List<CompletionChoice> Choices { get; set; }
[JsonProperty("usage")]
public CompletionUsage Usage { get; set; }
}
public class CompletionChoice
{
[JsonProperty("index")]
public int Index { get; set; }
[JsonProperty("finish_reason")]
public string FinishReason { get; set; }
[JsonProperty("message")]
public CompletionMessage Message { get; set; }
// 流式响应中的delta
[JsonProperty("delta")]
public CompletionMessage Delta { get; set; }
}
public class CompletionMessage
{
[JsonProperty("role")]
public string Role { get; set; }
[JsonProperty("content")]
public string Content { get; set; }
}
public class CompletionUsage
{
[JsonProperty("prompt_tokens")]
public int PromptTokens { get; set; }
[JsonProperty("completion_tokens")]
public int CompletionTokens { get; set; }
[JsonProperty("total_tokens")]
public int TotalTokens { get; set; }
}
// 异步调用响应
public class AsyncTaskStatus
{
[JsonProperty("request_id")]
public string RequestId { get; set; }
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("model")]
public string Model { get; set; }
[JsonProperty("task_status")]
public string TaskStatus { get; set; }
}
// 异步查询结果响应
public class AsyncCompletion : AsyncTaskStatus
{
[JsonProperty("choices")]
public List<CompletionChoice> Choices { get; set; }
[JsonProperty("usage")]
public CompletionUsage Usage { get; set; }
}
// GLM-Z1 API 客户端
public class GLMZ1Client
{
private readonly HttpClient _httpClient;
private readonly string _apiKey;
private const string BaseUrl = "https://open.bigmodel.cn/api/paas/v4/";
public GLMZ1Client(string apiKey)
{
_apiKey = apiKey;
_httpClient = new HttpClient();
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_apiKey}");
_httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
}
// 同步调用 Chat Completions API
public async Task<ChatCompletionResponse> ChatCompletionsSync(ChatCompletionRequest request)
{
request.Stream = false; // 确保同步调用时 stream 为 false
var jsonContent = JsonConvert.SerializeObject(request);
var httpContent = new StringContent(jsonContent, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync($"{BaseUrl}chat/completions", httpContent);
response.EnsureSuccessStatusCode(); // 确保请求成功
var responseString = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<ChatCompletionResponse>(responseString);
}
// 异步调用 Chat Completions API
public async Task<AsyncTaskStatus> ChatCompletionsAsync(ChatCompletionRequest request)
{
var jsonContent = JsonConvert.SerializeObject(request);
var httpContent = new StringContent(jsonContent, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync($"{BaseUrl}async/chat/completions", httpContent);
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<AsyncTaskStatus>(responseString);
}
// 查询异步任务结果
public async Task<AsyncCompletion> RetrieveCompletionResult(string taskId)
{
var response = await _httpClient.GetAsync($"{BaseUrl}async-result/{taskId}");
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<AsyncCompletion>(responseString);
}
// TODO: 实现流式调用,这会涉及循环读取 HttpResponseMessage.Content.ReadAsStreamAsync()
// 并解析 SSE 事件,此处为简化暂不提供完整实现。
// public async IAsyncEnumerable<ChatCompletionResponse> ChatCompletionsStream(ChatCompletionRequest request) { ... }
}
}

View File

@@ -0,0 +1,7 @@
namespace TechHelper.Client.AI
{
public interface IAIService
{
public Task<string> CallGLM(string userContent, string AnsConfig, AIModelsEnum aIModels = AIModelsEnum.GLMZ1Flash);
}
}

View File

@@ -0,0 +1,25 @@
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
@if (context.User.Identity?.IsAuthenticated != true)
{
<RedirectToLogin />
}
else
{
<p role="alert">You are not authorized to access this resource.</p>
}
</NotAuthorized>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>

View File

@@ -0,0 +1,64 @@
using Microsoft.AspNetCore.Components.Authorization;
using System.Security.Claims;
using System.Net.Http.Headers;
using TechHelper.Features;
using Microsoft.JSInterop;
namespace TechHelper.Client.AuthProviders
{
public class AuthStateProvider : AuthenticationStateProvider
{
private readonly HttpClient _httpClient;
private readonly AuthenticationState _anonymous;
private readonly ILocalStorageService _localStorageService;
public AuthStateProvider(ILocalStorageService localStorageService, HttpClient httpClient)
{
_localStorageService = localStorageService;
_httpClient = httpClient;
_anonymous = new AuthenticationState(
new ClaimsPrincipal(new ClaimsIdentity()));
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
string? token = null;
try
{
token = _localStorageService.GetItem<string>("authToken");
}
catch (Exception ex)
{
Console.WriteLine($"Error accessing LocalStorage or parsing token: {ex.Message}");
return _anonymous;
}
if (string.IsNullOrEmpty(token))
return _anonymous;
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("bearer", token);
return new AuthenticationState(new ClaimsPrincipal(
new ClaimsIdentity(JWTParser.ParseClaimsFromJwt(token), "jwtAuthType")));
}
public void NotifyUserAuthentication(string token)
{
var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(
JWTParser.ParseClaimsFromJwt(token), "jwtAuthType"));
var authState = Task.FromResult(new AuthenticationState(authenticatedUser));
NotifyAuthenticationStateChanged(authState);
}
public void NotifyUserLogout()
{
var authState = Task.FromResult(_anonymous);
NotifyAuthenticationStateChanged(authState);
}
}
}

View File

@@ -0,0 +1,23 @@
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,258 @@
using Newtonsoft.Json;
using System.Xml.Serialization; // 确保引用此命名空间
using System.Collections.Generic;
using System.IO; // 用于 XML 反序列化
namespace TechHelper.Client.Exam
{
[XmlRoot("EP")]
public class StringsList
{
[XmlElement("Q")]
public List<string> Items { get; set; }
}
// XML 根元素 <EP>
[XmlRoot("EP")]
public class ExamPaper
{
// XML 特性:<QGs> 包含 <QG> 列表
[XmlArray("QGs")]
[XmlArrayItem("QG")]
[JsonProperty("QuestionGroups")]
public List<QuestionGroup> QuestionGroups { get; set; } = new List<QuestionGroup>();
}
[XmlRoot("QG")]
public class QuestionGroup
{
// JSON 特性
[JsonProperty("题号")]
// XML 特性:作为 <QG Id="X"> 属性
[XmlAttribute("Id")]
public string Id { get; set; }
[JsonProperty("标题")]
[XmlAttribute("T")] // T for Title
public string Title { get; set; }
[JsonProperty("分值")]
[XmlAttribute("S")] // S for Score
public int Score { get; set; }
[JsonProperty("分值问题标记")]
[XmlAttribute("SPM")] // SPM for ScoreProblemMarker
public string ScoreProblemMarker { get; set; } = ""; // 初始化为空字符串,避免 null
[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 string SubId { get; set; }
[JsonProperty("题干")]
[XmlAttribute("T")] // T for Text (Stem)
public string Stem { get; set; }
[JsonProperty("分值")]
[XmlAttribute("S")] // S for Score
public int Score { get; set; } // 分值通常为整数
[JsonProperty("分值问题标记")]
[XmlAttribute("SPM")] // SPM for ScoreProblemMarker
public string ScoreProblemMarker { get; set; } = "";
// 注意:这里的 Options 需要适配 XML 结构 <Os><O V="X"/></Os>
// 因此它不再是 List<string>,而是 List<Option>
[JsonProperty("选项")]
[XmlArray("Os")] // Os 包含 <O> 列表
[XmlArrayItem("O")]
public List<Option> Options { get; set; } = new List<Option>();
[JsonProperty("示例答案")]
[XmlAttribute("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

@@ -0,0 +1,57 @@
using System.Security.Claims;
using System.Text.Json;
namespace TechHelper.Features
{
public static class JWTParser
{
public static IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
{
var claims = new List<Claim>();
var payload = jwt.Split(".")[1];
var jsonBytes = ParseBase64WithoutPadding(payload);
var keyValuePairs = JsonSerializer
.Deserialize<Dictionary<string, object>>(jsonBytes);
ExtractRolesFromJWT(claims, keyValuePairs);
claims.AddRange(keyValuePairs
.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString())));
return claims;
}
private static byte[] ParseBase64WithoutPadding(string base64)
{
switch (base64.Length % 4)
{
case 2: base64 += "=="; break;
case 3: base64 += "="; break;
}
return Convert.FromBase64String(base64);
}
private static void ExtractRolesFromJWT(List<Claim> claims, Dictionary<string, object> keyValuePairs)
{
keyValuePairs.TryGetValue(ClaimTypes.Role, out object roles);
if (roles != null)
{
var parsedRoles = roles.ToString().Trim().TrimStart('[').TrimEnd(']').Split(',');
if (parsedRoles.Length > 1)
{
foreach (var parsedRole in parsedRoles)
{
claims.Add(new Claim(ClaimTypes.Role, parsedRole.Trim('"')));
}
}
else
{
claims.Add(new Claim(ClaimTypes.Role, parsedRoles[0]));
}
keyValuePairs.Remove(ClaimTypes.Role);
}
}
}
}

View File

@@ -0,0 +1,86 @@
using TechHelper.Client.HttpRepository;
using Microsoft.AspNetCore.Components;
using System.Net;
using System.Net.Http.Headers;
using MudBlazor;
using Microsoft.Extensions.DependencyInjection;
using System;
namespace BlazorProducts.Client.HttpInterceptor
{
public class HttpInterceptorHandlerService : DelegatingHandler
{
private readonly NavigationManager _navManager;
private readonly RefreshTokenService _refreshTokenService;
private readonly ISnackbar _serviceProvider;
private ISnackbar toastService = null;
public HttpInterceptorHandlerService(
NavigationManager navManager,
RefreshTokenService refreshTokenService,
ISnackbar serviceProvider)
{
_navManager = navManager;
_refreshTokenService = refreshTokenService;
_serviceProvider = serviceProvider;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var absolutePath = request.RequestUri?.AbsolutePath;
_serviceProvider.Add("HELLO");
if (absolutePath != null && !absolutePath.Contains("token") && !absolutePath.Contains("account"))
{
var token = await _refreshTokenService.TryRefreshToken();
if (!string.IsNullOrEmpty(token))
{
request.Headers.Authorization =
new AuthenticationHeaderValue("bearer", token);
}
}
var response = await base.SendAsync(request, cancellationToken);
HandleResponse(response);
return response;
}
private void HandleResponse(HttpResponseMessage response)
{
if (response is null)
{
_navManager.NavigateTo("/error");
throw new HttpResponseException("Server not available.");
}
var message = "";
if (!response.IsSuccessStatusCode)
{
switch (response.StatusCode)
{
case HttpStatusCode.NotFound:
_navManager.NavigateTo("/404");
message = "Resource not found.";
break;
case HttpStatusCode.BadRequest:
message = "Invalid request. Please try again.";
break;
case HttpStatusCode.Unauthorized:
_navManager.NavigateTo("/unauthorized");
message = "Unauthorized access";
break;
default:
_navManager.NavigateTo("/error");
message = "Something went wrong. Please contact the administrator.";
break;
}
}
}
}
}

View File

@@ -0,0 +1,27 @@
using System;
using System.Runtime.Serialization;
namespace BlazorProducts.Client.HttpInterceptor
{
[Serializable]
internal class HttpResponseException : Exception
{
public HttpResponseException()
{
}
public HttpResponseException(string message) : base(message)
{
}
public HttpResponseException(string message, Exception innerException)
: base(message, innerException)
{
}
protected HttpResponseException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}

View File

@@ -0,0 +1,198 @@
using TechHelper.Client.AuthProviders;
using Entities.DTO;
using Microsoft.AspNetCore.Components.Authorization;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Net;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.JSInterop;
namespace TechHelper.Client.HttpRepository
{
public class AuthenticationClientService : IAuthenticationClientService
{
private HttpClient _client;
private readonly IHttpClientFactory _clientFactory;
private readonly JsonSerializerOptions _options =
new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
private readonly AuthenticationStateProvider _stateProvider;
private readonly ILocalStorageService _localStorageService;
private readonly NavigationManager _navigationManager;
public AuthenticationClientService(IHttpClientFactory httpClientFactory,
AuthenticationStateProvider authenticationStateProvider,
ILocalStorageService localStorageService,
NavigationManager navigationManager)
{
_clientFactory = httpClientFactory;
//_client = httpClientFactory.CreateClient("Default");
_localStorageService = localStorageService;
_stateProvider = authenticationStateProvider;
_navigationManager = navigationManager;
}
public async Task<AuthResponseDto> LoginAsync(UserForAuthenticationDto userForAuthenticationDto)
{
using (_client = _clientFactory.CreateClient("Default"))
{
var reponse = await _client.PostAsJsonAsync("account/login",
userForAuthenticationDto);
var content = await reponse.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<AuthResponseDto>(content, _options);
if (!reponse.IsSuccessStatusCode || result.Is2StepVerificationRequired)
return result;
_localStorageService.SetItem("authToken", result.Token);
_localStorageService.SetItem("refreshToken", result.RefreshToken);
((AuthStateProvider)_stateProvider).NotifyUserAuthentication(
result.Token);
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(
"bearer", result.Token);
return new AuthResponseDto { IsAuthSuccessful = true };
}
}
public async Task LogoutAsync()
{
using (_client = _clientFactory.CreateClient("Default"))
{
_localStorageService.RemoveItem("authToken");
_localStorageService.RemoveItem("refreshToken");
((AuthStateProvider)_stateProvider).NotifyUserLogout();
_client.DefaultRequestHeaders.Authorization = null;
}
}
public async Task<string> RefreshTokenAsync()
{
using (_client = _clientFactory.CreateClient("Default"))
{
var token = _localStorageService.GetItem<string>("authToken");
var refreshToken = _localStorageService.GetItem<string>("refreshToken");
var response = await _client.PostAsJsonAsync("token/refresh",
new RefreshTokenDto
{
Token = token,
RefreshToken = refreshToken
});
var content = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<AuthResponseDto>(content, _options);
_localStorageService.SetItem("authToken", result.Token);
_localStorageService.SetItem("refreshToken", result.RefreshToken);
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", result.Token);
return result.Token;
}
}
public async Task<ResponseDto> RegisterUserAsync(UserForRegistrationDto userForRegistrationDto)
{
using (_client = _clientFactory.CreateClient("Default"))
{
userForRegistrationDto.ClientURI = Path.Combine(
_navigationManager.BaseUri, "emailconfirmation");
var reponse = await _client.PostAsJsonAsync("account/register",
userForRegistrationDto);
if (!reponse.IsSuccessStatusCode)
{
var content = await reponse.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<ResponseDto>(content, _options);
return result;
}
return new ResponseDto { IsSuccessfulRegistration = true };
}
}
public async Task<HttpStatusCode> ForgotPasswordAsync(ForgotPasswordDto forgotPasswordDto)
{
using (_client = _clientFactory.CreateClient("Default"))
{
forgotPasswordDto.ClientURI = Path.Combine(_navigationManager.BaseUri, "resetpassword");
var result = await _client.PostAsJsonAsync("account/forgotpassword",
forgotPasswordDto);
return result.StatusCode;
}
}
public async Task<ResetPasswordResponseDto> ResetPasswordAsync(ResetPasswordDto resetPasswordDto)
{
using (_client = _clientFactory.CreateClient("Default"))
{
var resetresult = await _client.PostAsJsonAsync("account/resetpassword",
resetPasswordDto);
var resetContent = await resetresult.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<ResetPasswordResponseDto>(resetContent, _options);
return result;
}
}
public async Task<HttpStatusCode> EmailConfirmationAsync(string email, string token)
{
var queryStringParam = new Dictionary<string, string>
{
["email"] = email,
["token"] = token
};
using (_client = _clientFactory.CreateClient("Default"))
{
var response = await _client.GetAsync(QueryHelpers.AddQueryString(
"account/emailconfirmation", queryStringParam));
return response.StatusCode;
}
}
public async Task<AuthResponseDto> LoginVerfication(TwoFactorVerificationDto twoFactorVerificationDto)
{
using (_client = _clientFactory.CreateClient("Default"))
{
var reponse = await _client.PostAsJsonAsync("account/twostepverification",
twoFactorVerificationDto);
var content = await reponse.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<AuthResponseDto>(content, _options);
if (!reponse.IsSuccessStatusCode)
return result;
_localStorageService.SetItem("authToken", result.Token);
_localStorageService.SetItem("refreshToken", result.RefreshToken);
((AuthStateProvider)_stateProvider).NotifyUserAuthentication(
result.Token);
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(
"bearer", result.Token);
return new AuthResponseDto { IsAuthSuccessful = true };
}
}
}
}

View File

@@ -0,0 +1,17 @@
using Entities.DTO;
using System.Net;
namespace TechHelper.Client.HttpRepository
{
public interface IAuthenticationClientService
{
Task<ResponseDto> RegisterUserAsync(UserForRegistrationDto userForRegistrationDto);
Task<AuthResponseDto> LoginAsync(UserForAuthenticationDto userForAuthenticationDto);
Task LogoutAsync();
Task<string> RefreshTokenAsync();
Task<HttpStatusCode> ForgotPasswordAsync(ForgotPasswordDto forgotPasswordDto);
Task<ResetPasswordResponseDto> ResetPasswordAsync(ResetPasswordDto resetPasswordDto);
Task<HttpStatusCode> EmailConfirmationAsync(string email, string token);
Task<AuthResponseDto> LoginVerfication(TwoFactorVerificationDto twoFactorVerificationDto);
}
}

View File

@@ -0,0 +1,34 @@
using Microsoft.AspNetCore.Components.Authorization;
namespace TechHelper.Client.HttpRepository
{
public class RefreshTokenService
{
private readonly AuthenticationStateProvider _authenticationStateProvider;
private readonly IAuthenticationClientService _authenticationClientService;
public RefreshTokenService(AuthenticationStateProvider authenticationStateProvider, IAuthenticationClientService authenticationClientService)
{
_authenticationStateProvider = authenticationStateProvider;
_authenticationClientService = authenticationClientService;
}
public async Task<string> TryRefreshToken()
{
var authState = await _authenticationStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
var expClaim = user.FindFirst(c => c.Type.Equals("exp")).Value;
var expTime = DateTimeOffset.FromUnixTimeSeconds(
Convert.ToInt64(expClaim));
var diff = expTime - DateTime.UtcNow;
if (diff.TotalMinutes <= 2)
return await _authenticationClientService.RefreshTokenAsync();
return string.Empty;
}
}
}

View File

@@ -0,0 +1,19 @@
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject NavigationManager Navigation
<AuthorizeView>
<Authorized>
Hello, @context.User.Identity?.Name!
<button class="nav-link btn btn-link" @onclick="BeginLogOut">Log out</button>
</Authorized>
<NotAuthorized>
<a href="authentication/login">Log in</a>
</NotAuthorized>
</AuthorizeView>
@code{
public void BeginLogOut()
{
Navigation.NavigateToLogout("authentication/logout");
}
}

View File

@@ -0,0 +1,42 @@
@inherits LayoutComponentBase
<MudThemeProvider />
<MudDialogProvider />
<MudSnackbarProvider />
<MudPopoverProvider />
<MudPaper Class="d-flex flex-column flex-grow-1" Style="height: 100vh;">
<MudPaper Class="d-flex flex-column flex-grow-1 overflow-hidden">
<MudPaper Height="5%" Class=" d-flex flex-grow-1" Style="background-color:mediumseagreen">
<MudSpacer> </MudSpacer>
<AuthLinks/>
</MudPaper>
<MudPaper Height="95%" Class="d-flex flex-row flex-grow-1 overflow-hidden">
<MudPaper Width="5%" Class="pa-2 mr-1 d-flex flex-column flex-grow-0 justify-content-between">
<NavBar Class="flex-column flex-grow-0 rounded-pill" />
<AccountView Class="flex-column flex-grow-0 rounded-pill" />
</MudPaper>
<MudPaper Class="d-flex flex-grow-1 pa-3 ma-1 ">
@Body
</MudPaper>
</MudPaper>
</MudPaper>
</MudPaper>

View File

@@ -0,0 +1,85 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.my-hover-paper:hover {
background-color: var(--mud-palette-primary); /* <20><>ͣʱ<CDA3><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϊ<EFBFBD><CEAA><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɫ */
border-color: var(--mud-palette-primary-lighten); /* <20><>ͣʱ<CDA3>߿<EFBFBD><DFBF><EFBFBD>ɫ<EFBFBD>仯 */
color: var(--mud-palette-primary-contrasttext); /* <20><>ͣʱ<CDA3><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɫ<EFBFBD>仯 */
transform: translateY(-3px); /* <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>΢<EFBFBD>ƶ<EFBFBD><C6B6><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ӷ<EFBFBD><D3B6><EFBFBD> */
box-shadow: var(--mud-elevation-4); /* <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ӱ<EFBFBD><D3B0><EFBFBD><EFBFBD> */
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}

View File

@@ -0,0 +1,13 @@
<MudPaper Class=@Class>
<MudNavLink Icon="@Icons.Material.Filled.Home" Class="py-5 px-3" Href="" Match="NavLinkMatch.All"></MudNavLink>
<MudNavLink Icon="@Icons.Material.Filled.Person" Class="py-5 px-3" Href="Account/Manage"></MudNavLink>
<MudNavLink Icon="@Icons.Material.Filled.Edit" Class="py-5 px-3" Href="Edit"></MudNavLink>
<MudNavLink Icon="@Icons.Material.Filled.Air" Class="py-5 px-3" Href="ai"></MudNavLink>
<MudNavLink Icon="@Icons.Material.Filled.SportsTennis" Class="py-5 px-3" Href="test"></MudNavLink>
</MudPaper>
@code {
[Parameter]
public string? Class { get; set; }
}

View File

@@ -0,0 +1,52 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">TechHelper.Client</a>
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>
<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="Account/Manage">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> 个人中心
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="Edit">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> 编辑器
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="test">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> 测试页面
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="ai">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> AI
</NavLink>
</div>
</nav>
</div>
@code {
private bool collapseNavMenu = true;
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
}

View File

@@ -0,0 +1,83 @@
.navbar-toggler {
background-color: rgba(255, 255, 255, 0.1);
}
.top-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep a {
color: #d7d7d7;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep a:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.collapse {
/* Never collapse the sidebar for wide screens */
display: block;
}
.nav-scrollable {
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}

View File

@@ -0,0 +1,9 @@
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject NavigationManager Navigation
@code {
protected override void OnInitialized()
{
Navigation.NavigateToLogin("authentication/login");
}
}

View File

@@ -0,0 +1,196 @@
@page "/ai"
@using Newtonsoft.Json
@using TechHelper.Client
@using TechHelper.Client.AI
@using TechHelper.Client.Exam
@inject IAIService AiService
@inject ISnackbar Snackbar
<MudPaper Elevation="3" Height="100%" Width="100%">
<MudContainer>
<MudAppBar Elevation="1">
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="@((e) => ToggleDrawer())" />
<MudText Typo="Typo.h6" Class="ml-4">AI 对话</MudText>
<MudSpacer />
<MudButton Variant="Variant.Text" Color="Color.Inherit" OnClick="ClearChat">清空对话</MudButton>
<MudIconButton Icon="@Icons.Material.Filled.Settings" Color="Color.Inherit" OnClick="@(() => IsSettingsOpen = !IsSettingsOpen)" />
</MudAppBar>
<MudDrawer @bind-Open="@_drawerOpen" Elevation="1" Variant="@DrawerVariant.Temporary">
<MudDrawerHeader>
<MudText Typo="Typo.h6">对话历史 / 设置</MudText>
</MudDrawerHeader>
<MudNavMenu>
<MudNavLink Href="/ai-chat" Icon="@Icons.Material.Filled.Chat">新对话</MudNavLink>
<MudDivider />
@* 假设这里可以显示对话历史,点击切换
@foreach (var history in ChatHistorySummaries)
{
<MudNavLink @onclick="@(() => LoadChat(history.Id))">@history.Title</MudNavLink>
}
*@
<MudExpansionPanel Text="模型设置" @bind-Expanded="@IsSettingsOpen" Class="my-2">
<MudCard>
<MudCardContent>
<MudSelect Label="模型选择" @bind-Value="SelectedModel" Variant="Variant.Filled" Margin="Margin.Dense">
@foreach (var model in Enum.GetValues<AIModelsEnum>())
{
<MudSelectItem Value="@model.GetDescription()">@model.ToString()</MudSelectItem>
}
</MudSelect>
<MudSlider @bind-Value="Temperature" Min="0.0m" Max="1.0m" Step="0.01m" T="decimal" Label="Temperature" Class="mt-4">
<ChildContent>
<MudText Typo="Typo.body2">@Temperature.ToString("F2")</MudText>
</ChildContent>
</MudSlider>
<MudNumericField @bind-Value="MaxTokens" Label="最大Token数" Min="1" Max="32000" Step="1" Variant="Variant.Filled" Margin="Margin.Dense" Class="mt-4" />
</MudCardContent>
</MudCard>
</MudExpansionPanel>
</MudNavMenu>
</MudDrawer>
<MudContainer MaxWidth="MaxWidth.Medium">
@foreach (var message in ChatMessages)
{
<MudChat Color="Color.Success" Dense="true" Elevation="0" Variant="Variant.Text" ChatPosition="@(message.Role == "user" ? ChatBubblePosition.End : ChatBubblePosition.Start)">
<MudChatHeader Name="@(message.Role == "user" ? "user" : "ai")" Time="12:46" />
<MudAvatar Size=" Size.Small">
<MudImage Src="images/toiletvisit.jpg" />
</MudAvatar>
<MudChatBubble> @message.Content </MudChatBubble>
<MudChatFooter Text="Seen at 12:46" />
</MudChat>
}
@if (IsLoading)
{
<MudPaper Class="ai-message mb-2 pa-3 loading-indicator" Elevation="1">
<MudText Typo="Typo.body2"><strong>AI</strong></MudText>
<MudProgressCircular Indeterminate="true" Size="Size.Small" />
<MudText Typo="Typo.body2" Class="ml-2">思考中...</MudText>
</MudPaper>
}
<div @ref="messagesEndRef" class="scroll-anchor"></div> @* 滚动锚点 *@
</MudContainer>
<MudPaper Class="input-area d-flex align-center pa-3" Elevation="8" Style="position: fixed; bottom: 0; left: 0; right: 0; z-index: 1000; width: 100%;">
<MudContainer MaxWidth="MaxWidth.Medium" Class="d-flex" Style="width: 100%;">
<MudTextField @bind-Value="UserInput"
Label="输入你的消息..."
Variant="Variant.Filled"
Lines="1"
Adornment="Adornment.End"
AdornmentIcon="@Icons.Material.Filled.Send"
OnAdornmentClick="SendMessage"
Class="flex-grow-1 mr-2"
Disabled="@IsLoading"
KeyDown="@HandleKeyDown" />
</MudContainer>
</MudPaper>
</MudContainer>
</MudPaper>
@code {
private bool _drawerOpen = false;
private bool IsSettingsOpen = false;
private bool IsExamCheckOpen = false;
private List<Message> ChatMessages = new List<Message>();
private string UserInput { get; set; } = "";
private bool IsLoading { get; set; } = false;
private ElementReference messagesEndRef; // 用于自动滚动到底部的引用
// AI 模型设置
private string SelectedModel { get; set; } = AIModelsEnum.GLMZ1Flash.GetDescription();
private decimal Temperature { get; set; } = 0.75m; // MudSlider 使用 decimal
private int MaxTokens { get; set; } = 1000;
// 试题检查相关
private string ExamJsonInput { get; set; } = "";
private string ExamCheckResult { get; set; } = "";
protected override void OnInitialized()
{
ChatMessages.Add(new SystemMessage("你是一个乐于助人的AI助手。"));
Snackbar.Add("欢迎使用AI对话", Severity.Info);
}
private void ToggleDrawer()
{
_drawerOpen = !_drawerOpen;
}
private void ClearChat()
{
ChatMessages.Clear();
ChatMessages.Add(new SystemMessage("你是一个乐于助人的AI助手。"));
Snackbar.Add("对话已清空。", Severity.Success);
}
private async Task HandleKeyDown(KeyboardEventArgs e)
{
if (e.Key == "Enter" && !e.ShiftKey)
{
await SendMessage();
}
}
private async Task SendMessage()
{
if (string.IsNullOrWhiteSpace(UserInput)) return;
IsLoading = true;
ChatMessages.Add(new UserMessage(UserInput));
var currentInput = UserInput; // 保存当前输入,因为会清空
try
{
// 构建请求消息列表不包括SystemMessage通常System Message只在API调用时作为参数传递不显示在聊天记录中
var requestMessages = new List<Message>();
requestMessages.Add(new SystemMessage("你是一个乐于助人的AI助手请简洁明了地回答问题。")); // 每次请求都包含系统提示
requestMessages.AddRange(ChatMessages.Where(m => m.Role != "system")); // 添加除系统消息外的所有历史对话
var request = new ChatCompletionRequest
{
Model = SelectedModel,
Messages = requestMessages,
Temperature = (float)Temperature, // MudSlider返回decimal需要转换为float
MaxTokens = MaxTokens
};
var response = await AiService.CallGLM(UserInput, AIConfiguration.ExamAnsConfig);
if (!string.IsNullOrEmpty(response))
{
// var exam = ExamParser.ParseExamJson(response);
ChatMessages.Add(new AssistantMessage(response));
}
}
catch (HttpRequestException httpEx)
{
ChatMessages.Add(new AssistantMessage($"API 请求失败: {httpEx.Message}. 请检查API Key和网络连接。"));
Snackbar.Add($"API 请求失败: {httpEx.Message}", Severity.Error);
Console.WriteLine($"HTTP Request Error: {httpEx.Message}");
}
catch (Exception ex)
{
ChatMessages.Add(new AssistantMessage($"发生错误: {ex.Message}"));
Snackbar.Add($"发生错误: {ex.Message}", Severity.Error);
Console.WriteLine($"Error: {ex.Message}");
}
finally
{
IsLoading = false;
UserInput = ""; // 清空输入框
StateHasChanged();
}
}
}

View File

@@ -0,0 +1,10 @@
<MudPaper Class=@Class>
<MudNavLink Icon="@Icons.Material.Filled.Settings" Href="" Match="NavLinkMatch.All" Class="py-5 px-3"></MudNavLink>
<MudNavLink Icon="@Icons.Material.Filled.Person4" Href="Account/Manage" Class="py-5 px-3"></MudNavLink>
</MudPaper>
@code {
[Parameter]
public string? Class { get; set; }
}

View File

@@ -0,0 +1,26 @@
@inject IAuthenticationClientService AuthenticationClientService
@inject NavigationManager NavigationManager
<AuthorizeView>
<Authorized>
<MudText>
Hello, @context.User.Identity.Name!
</MudText>
<MudButton OnClick="Logout"> LOGOUT </MudButton>
</Authorized>
<NotAuthorized>
<MudButton Class="" Href="Register"> Register </MudButton>
<MudButton Class="" Href="Login"> Login </MudButton>
</NotAuthorized>
</AuthorizeView>
@code {
private async Task Logout()
{
await AuthenticationClientService.LogoutAsync();
NavigationManager.NavigateTo("/");
}
}

View File

@@ -0,0 +1,7 @@
@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<RemoteAuthenticatorView Action="@Action" />
@code{
[Parameter] public string? Action { get; set; }
}

View File

@@ -0,0 +1,11 @@
@page "/emailconfirmation"
<h3>EmailConfirmation</h3>
<MudButton OnClick="ConfirmToEmail"> 点击确认 </MudButton>
<MudPaper>
@if (_showSuccess)
{
<h3> Successful </h3>
}
</MudPaper>

View File

@@ -0,0 +1,42 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.WebUtilities;
using System.Net;
using TechHelper.Client.HttpRepository;
namespace TechHelper.Client.Pages.Author
{
public partial class EmailConfirmation
{
private bool _showSuccess;
private bool _showError;
[Inject]
public IAuthenticationClientService AuthenticationClientService { get; set; }
[Inject]
public NavigationManager NavigationManager { get; set; }
protected async void ConfirmToEmail()
{
_showError = _showSuccess = false;
var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri);
var queryStrings = QueryHelpers.ParseQuery(uri.Query);
if (queryStrings.TryGetValue("email", out var email) &&
queryStrings.TryGetValue("token", out var token))
{
var result = await AuthenticationClientService.EmailConfirmationAsync(email, token);
if (result == HttpStatusCode.OK)
_showSuccess = true;
else
_showError = true;
}
else
NavigationManager.NavigateTo("/");
}
}
}

View File

@@ -0,0 +1,38 @@
@page "/forgotpassword"
<EditForm Model="@_forgotPassDto" OnValidSubmit="Submit" FormName="ForgotForm">
<DataAnnotationsValidator />
<MudGrid>
<MudItem xs="12" sm="7">
<MudCard>
<MudCardContent>
<MudTextField Label="Email" Class="mt-3"
@bind-Value="_forgotPassDto.Email" For="@(() => _forgotPassDto.Email)" />
</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 (_showSuccess)
{
<MudText Color="Color.Success">Success</MudText>
}
else if(_showError)
{
<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>

View File

@@ -0,0 +1,30 @@
using Entities.DTO;
using Microsoft.AspNetCore.Components;
using TechHelper.Client.HttpRepository;
namespace TechHelper.Client.Pages.Author
{
public partial class ForgotPassword
{
private ForgotPasswordDto _forgotPassDto = new ForgotPasswordDto();
private bool _showSuccess;
private bool _showError;
[Inject]
public IAuthenticationClientService AuthenticationClientService { get; set; }
private async Task Submit()
{
_showError = _showSuccess = false;
var result = await AuthenticationClientService.ForgotPasswordAsync(_forgotPassDto);
if (result == System.Net.HttpStatusCode.OK)
{
_showSuccess = true;
}
_showError = true;
}
}
}

View File

@@ -0,0 +1,48 @@
@page "/login"
<MudText Typo="Typo.h2"> Login Account </MudText>
<EditForm Model="@_userForAuth" OnValidSubmit="Logining" FormName="LoginingForm">
<DataAnnotationsValidator />
<MudGrid>
<MudItem xs="12" sm="7">
<MudCard>
<MudCardContent>
<MudTextField Label="Email" Class="mt-3"
@bind-Value="_userForAuth.Email" For="@(() => _userForAuth.Email)" />
<MudTextField Label="Password" HelperText="Choose a strong password" Class="mt-3"
@bind-Value="_userForAuth.Password" For="@(() => _userForAuth.Password)" InputType="InputType.Password" />
</MudCardContent>
<MudCardActions>
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary" Class="ml-auto">Register</MudButton>
</MudCardActions>
<div class="nav-item px-3">
<NavLink class="nav-link" href="forgotpassword">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span>Forgot Password
</NavLink>
</div>
</MudCard>
</MudItem>
<MudItem xs="12" sm="5">
<MudPaper Class="pa-4 mud-height-full">
<MudText Typo="Typo.subtitle2">Validation Summary</MudText>
@if (!ShowRegistrationErrors)
{
<MudText Color="Color.Success">Success</MudText>
}
else
{
<MudText Color="@Color.Error">
<ValidationSummary />
</MudText>
}
</MudPaper>
</MudItem>
<MudItem xs="12">
<MudText Typo="Typo.body2" Align="Align.Center">
Fill out the form correctly to see the success message.
</MudText>
</MudItem>
</MudGrid>
</EditForm>

View File

@@ -0,0 +1,51 @@
using TechHelper.Client.HttpRepository;
using Entities.DTO;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.WebUtilities;
namespace TechHelper.Client.Pages.Author
{
public partial class Login
{
private UserForAuthenticationDto _userForAuth = new UserForAuthenticationDto();
[Inject]
public IAuthenticationClientService AuthenticationService { get; set; }
[Inject]
public NavigationManager NavigationManager { get; set; }
public bool ShowRegistrationErrors { get; set; }
public string Error { get; set; }
public async Task Logining()
{
ShowRegistrationErrors = false;
var result = await AuthenticationService.LoginAsync(_userForAuth);
if (result.Is2StepVerificationRequired)
{
var queryParams = new Dictionary<string, object?>
{
["provider"] = result.Provider,
["Email"] = _userForAuth.Email
};
var uri = NavigationManager.GetUriWithQueryParameters("/twostepverification", queryParams);
NavigationManager.NavigateTo(uri);
}
else if (!result.IsAuthSuccessful)
{
Error = result.ErrorMessage;
ShowRegistrationErrors = true;
}
else
{
NavigationManager.NavigateTo("/");
}
}
}
}

View File

@@ -0,0 +1,100 @@
@page "/register"
@using System.ComponentModel.DataAnnotations
@using Entities.Contracts
@inject ISnackbar Snackbar
<MudButton Variant="Variant.Filled" Color="Color.Primary" @onclick="@(() => Snackbar.Add("Simple Snackbar"))">
Open Snackbar
</MudButton>
<MudText Typo="Typo.h2"> Create Account </MudText>
<EditForm Model="@_userForRegistration" OnValidSubmit="Register" FormName="RegistrationForm">
<DataAnnotationsValidator />
<MudGrid>
<MudItem xs="12" sm="7">
<MudCard>
<MudCardContent>
<MudTextField Label="Name" HelperText="Max. 8 characters"
@bind-Value="_userForRegistration.Name" For="@(() => _userForRegistration.Email)" />
<MudTextField Label="Email" Class="mt-3"
@bind-Value="_userForRegistration.Email" For="@(() => _userForRegistration.Email)" />
<MudTextField Label="Password" HelperText="Choose a strong password" Class="mt-3"
@bind-Value="_userForRegistration.Password" For="@(() => _userForRegistration.Password)" InputType="InputType.Password" />
<MudTextField Label="Password" HelperText="Repeat the password" Class="mt-3"
@bind-Value="_userForRegistration.ConfirmPassword" For="@(() => _userForRegistration.ConfirmPassword)" InputType="InputType.Password" />
<MudRadioGroup T="UserRoles" Label="Roles" @bind-Value="_userForRegistration.Roles">
@foreach (UserRoles item in Enum.GetValues(typeof(UserRoles)))
{
if (item != UserRoles.Administrator)
{
<MudRadio Value="@item">@item.ToString()</MudRadio>
}
}
</MudRadioGroup>
<MudStack Row="true">
<MudTextField Label="Class"
HelperText="Enter a class number between 1 and 14."
Class="mt-3"
@bind-Value="_userForRegistration.Class"
For="@(() => _userForRegistration.Class)"
InputType="InputType.Number"
Required="true"
RequiredError="Class is required." />
<MudTextField Label="Grade"
HelperText="Enter a grade number between 1 and 6."
Class="mt-3"
@bind-Value="_userForRegistration.Grade"
For="@(() => _userForRegistration.Grade)"
InputType="InputType.Number"
Required="true"
RequiredError="Grade is required." />
</MudStack>
<MudTextField Label="Phone Number"
HelperText="Enter your phone number (optional, 7-20 digits)."
Class="mt-3"
@bind-Value="_userForRegistration.PhoneNumber"
For="@(() => _userForRegistration.PhoneNumber)"
InputType="InputType.Telephone" /> <MudTextField Label="Home Address"
HelperText="Enter your home address (optional, max 200 characters)."
Class="mt-3"
@bind-Value="_userForRegistration.HomeAddress"
For="@(() => _userForRegistration.HomeAddress)"
Lines="3" />
</MudCardContent>
<MudCardActions>
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary" Class="ml-auto">Register</MudButton>
</MudCardActions>
</MudCard>
</MudItem>
<MudItem xs="12" sm="5">
<MudPaper Class="pa-4 mud-height-full">
<MudText Typo="Typo.subtitle2">Validation Summary</MudText>
@if (success)
{
<MudText Color="Color.Success">Success</MudText>
}
else
{
<MudText Color="@Color.Error">
<ValidationSummary />
</MudText>
}
</MudPaper>
</MudItem>
<MudItem xs="12">
<MudText Typo="Typo.body2" Align="Align.Center">
Fill out the form correctly to see the success message.
</MudText>
</MudItem>
</MudGrid>
</EditForm>

View File

@@ -0,0 +1,62 @@
using TechHelper.Client.HttpRepository;
using Entities.DTO;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using System.Text.RegularExpressions;
using TechHelper.Features;
using Entities.Contracts;
namespace TechHelper.Client.Pages.Author
{
public partial class Registration
{
private UserForRegistrationDto _userForRegistration = new UserForRegistrationDto();
[Inject]
public IAuthenticationClientService AuthenticationService { get; set; }
[Inject]
public NavigationManager NavigationManager { get; set; }
public bool ShowRegistrationErrors { get; set; }
public string[] Errors { get; set; }
private bool success;
public async Task Register()
{
ShowRegistrationErrors = false;
var result = await AuthenticationService.RegisterUserAsync(_userForRegistration);
if (!result.IsSuccessfulRegistration)
{
int index = 0;
foreach (var error in result.Errors)
{
Errors[index] = error;
index++;
}
ShowRegistrationErrors = true;
Snackbar.Add(Errors.ToString());
}
else
{
NavigationManager.NavigateTo("/");
}
}
[Inject]
public IEmailSender emailSender { get; set; }
public async void SendEmail()
{
string eamilTo = "1928360026@qq.com";
string authCode = "123456";
await emailSender.SendEmailAuthcodeAsync(eamilTo, authCode);
}
}
}

View File

@@ -0,0 +1,49 @@
@page "/resetpassword"
<MudText Typo="Typo.h2"> Reset Password Account </MudText>
<EditForm Model="@_resetPassDto" OnValidSubmit="Submit" FormName="ResetPasswordForm">
<DataAnnotationsValidator />
<MudGrid>
<MudItem xs="12" sm="7">
<MudCard>
<MudCardContent>
<MudTextField Label="Password" HelperText="Choose a strong password" Class="mt-3"
@bind-Value="_resetPassDto.Password" For="@(() => _resetPassDto.Password)" InputType="InputType.Password" />
<MudTextField Label="Confirm Password" HelperText="Repet Password" Class="mt-3"
@bind-Value="_resetPassDto.ConfirmPassword" For="@(() => _resetPassDto.ConfirmPassword)" InputType="InputType.Password" />
</MudCardContent>
<MudCardActions>
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary" Class="ml-auto">Register</MudButton>
</MudCardActions>
<div class="nav-item px-3">
<NavLink class="nav-link" href="forgotpassword">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span>Forgot Password
</NavLink>
</div>
</MudCard>
</MudItem>
<MudItem xs="12" sm="5">
<MudPaper Class="pa-4 mud-height-full">
<MudText Typo="Typo.subtitle2">Validation Summary</MudText>
@if (_showSuccess)
{
<MudText Color="Color.Success">Success</MudText>
}
else if (_showError)
{
<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>

View File

@@ -0,0 +1,55 @@
using Entities.DTO;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.WebUtilities;
using TechHelper.Client.HttpRepository;
namespace TechHelper.Client.Pages.Author
{
public partial class ResetPassword : ComponentBase
{
private readonly ResetPasswordDto _resetPassDto = new ResetPasswordDto();
private bool _showSuccess;
private bool _showError;
private IEnumerable<string> _errors;
[Inject]
public IAuthenticationClientService AuthenticationClientService { get; set; }
[Inject]
public NavigationManager NavigationManager { get; set; }
protected override void OnInitialized()
{
var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri);
var queryStrings = QueryHelpers.ParseQuery(uri.Query);
if (queryStrings.TryGetValue("email", out var email) &&
queryStrings.TryGetValue("token", out var token))
{
_resetPassDto.Email = email;
_resetPassDto.Token = token;
}
else
{
NavigationManager.NavigateTo("/");
}
}
private async Task Submit()
{
_showSuccess = _showError = false;
var result = await AuthenticationClientService.ResetPasswordAsync(_resetPassDto);
if(result.IsResetPasswordSuccessful)
_showSuccess = true;
else
{
_errors = result.Errors;
_showError = true;
}
NavigationManager.NavigateTo("/");
}
}
}

View File

@@ -0,0 +1,11 @@
@page "/rodetail"
<h3>RoleDetailInfo</h3>
<AuthorizeView>
<MudText> @context.User.Identity.Name </MudText>
</AuthorizeView>
@code {
}

View File

@@ -0,0 +1,47 @@
@page "/logout"
@inject IAuthenticationClientService AuthenticationClientService
@inject NavigationManager NavigationManager
<MudText>HELLO WORLD </MudText>
@code {
protected override async Task OnAfterRenderAsync(bool firstRender)
{
// 作用:这个方法在组件渲染到 UI 后被调用。
// 意义:这是执行 JS 互操作的安全时机,特别是在启用了预渲染的情况下,
// 此时客户端的 JS 运行时已经可用(在 Blazor Server 中通过 SignalR在 Blazor WASM 中是 WASM 环境本身)。
// firstRender 参数:
// 作用:指示本次调用是否是组件首次在客户端渲染完成。
// 意义:注销逻辑只需要执行一次。使用 firstRender = true 可以避免在组件后续状态变化触发重新渲染时重复执行。
if (firstRender)
{
Console.WriteLine("Signout: OnAfterRenderAsync - First Render. Executing Logout..."); // 可选日志
try
{
// === 在这里安全地调用依赖 JS Interop 的注销方法 ===
// AuthenticationClientService.Logout() 方法内部会调用 _localStorageService.RemoveItemAsync(),
// 现在是调用它的安全时机。
// 注销完成后,执行导航重定向
NavigationManager.NavigateTo("/");
}
catch (Exception ex)
{
// === 处理 Logout 中可能发生的异常 ===
// 例如,如果 Local Storage 操作失败,或者 Logout 方法内部有其他错误
Console.WriteLine($"Error during logout in Signout component: {ex.Message}");
// 你可能需要在这里显示错误信息给用户,或者决定是否依然重定向
// 即使注销失败,通常也希望将用户导航到某个页面(如首页或错误页)
// 例如:
NavigationManager.NavigateTo("/"); // 即使失败也重定向到首页
// 或者 NavigationManager.NavigateTo("/error"); // 重定向到错误页
}
Console.WriteLine("Signout: OnAfterRenderAsync - First Render. Logout execution finished."); // 可选日志
}
}
}

View File

@@ -0,0 +1,39 @@
@page "/twostepverification"
<EditForm Model="@_twoFactorVerificationDto" OnValidSubmit="Submit" FormName="ForgotForm">
<DataAnnotationsValidator />
<MudGrid>
<MudItem xs="12" sm="7">
<MudCard>
<MudCardContent>
<MudTextField Label="Email" Class="mt-3"
@bind-Value="_twoFactorVerificationDto.TwoFactorToken" For="@(() => _twoFactorVerificationDto.TwoFactorToken)" />
</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 (_showSuccess)
{
<MudText Color="Color.Success">Success</MudText>
}
else if (_showError)
{
<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>

View File

@@ -0,0 +1,60 @@
using Entities.DTO;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.WebUtilities;
using TechHelper.Client.HttpRepository;
namespace TechHelper.Client.Pages.Author
{
public partial class TwoStepVerification : ComponentBase
{
private TwoFactorVerificationDto _twoFactorVerificationDto = new TwoFactorVerificationDto();
private bool _showSuccess;
private string? _error;
private bool _showError;
[Inject]
public IAuthenticationClientService AuthenticationClientService { get; set; }
[Inject]
public NavigationManager NavigationManager { get; set; }
[SupplyParameterFromQuery]
[Parameter]
public string? Email { get; set; }
[SupplyParameterFromQuery]
[Parameter]
public string? Provider { get; set; }
protected override void OnInitialized()
{
if(string.IsNullOrEmpty(Email) || string.IsNullOrEmpty(Provider))
{
NavigationManager.NavigateTo("/");
}
else
{
_twoFactorVerificationDto.Email = Email;
_twoFactorVerificationDto.Provider = Provider;
}
}
private async Task Submit()
{
_showError = false;
var result = await AuthenticationClientService.LoginVerfication(_twoFactorVerificationDto);
if(result.IsAuthSuccessful)
NavigationManager.NavigateTo("/");
else
{
_error = result.ErrorMessage;
_showError = true;
}
}
}
}

View File

@@ -0,0 +1,81 @@
@rendermode InteractiveAuto
@inject NavigationManager NavigationManager
<MudPaper Class="pa-10 ma-5">
<MudText Typo="Typo.h1"> @user.Username</MudText>
<MudText Typo="Typo.h6"> Role :@user.Role</MudText>
<MudText Typo="Typo.h6"> Email : @user.Email</MudText>
<MudDataGrid T="Submission" Items="@Submissions" SortMode="@_sortMode">
<Columns>
<PropertyColumn Property="x => x.Assignment.Title" />
<PropertyColumn Property="x => x.Assignment.Description" />
<PropertyColumn Property="x => x.AttemptNumber" />
<PropertyColumn Property="x => x.GradedAt" />
<TemplateColumn CellClass="d-flex justify-end">
<CellTemplate>
<MudStack Row>
<MudButton Size="@Size.Small" Variant="@Variant.Filled" Color="@Color.Primary" OnClick="@(() => DetailsButtonClicked(context.Item))">详情</MudButton>
</MudStack>
</CellTemplate>
</TemplateColumn>
</Columns>
<PagerContent>
<MudDataGridPager T="Submission" />
</PagerContent>
</MudDataGrid>
@*
<MudDataGrid T="SubmissionDetail" Items="@questions" SortMode="@_sortMode">
<Columns>
<PropertyColumn Property="x => x.AssignmentQuestion.QuestionNumber" />
<PropertyColumn Property="x => x.AssignmentQuestion.Question.QuestionText" />
<PropertyColumn Property="x => x.AssignmentQuestion.Question.DifficultyLevel" />
<PropertyColumn Property="x => x.AssignmentQuestion.Question.CorrectAnswer" />
<PropertyColumn Property="x => x.IsCorrect" />
</Columns>
<PagerContent>
<MudDataGridPager T="SubmissionDetail" />
</PagerContent>
</MudDataGrid>
*@
</MudPaper>
@code {
[Inject]
ISubmissionService submissionService { get; set; }
[Parameter]
public int UserId { get; set; } = 0;
private SortMode _sortMode = SortMode.Multiple;
private User user = new User();
private User student = new User();
private IEnumerable<Submission> Submissions = new List<Submission>();
protected override async Task OnInitializedAsync()
{
var result = await submissionService.GetByUserId(14);
if (result.Successed)
{
Submissions = result.Data.Items;
}
else
{
}
}
private void DetailsButtonClicked(Submission submission)
{
if (submission != null)
{
NavigationManager.NavigateTo($"/submissiondetails/{submission.Id}");
}
}
}

View File

@@ -0,0 +1,45 @@
@rendermode InteractiveServer
<MudPaper Class="pa-5 ma-5 rounded-lg" Width="@Width">
<MudChart ChartType=@ChartType @bind-SelectedIndex="Index" InputData="@data" InputLabels="@labels" Width=@Width Height=@Height> </MudChart>
</MudPaper>
@code {
private int Index = -1;
private ChartOptions options = new ChartOptions();
public double[] data = { 50, 25, 20, 5 };
public string[] labels = { "Fossil", "Nuclear", "Solar", "Wind" };
[Parameter]
[Category("Behavior")]
public ChartType ChartType { get; set; } = ChartType.Donut;
[Parameter]
[Category("Appearance")]
public string Width { get; set; } = "80%";
[Parameter]
[Category("Appearance")]
public ChartOptions ChartOptions { get; set; } = new ChartOptions();
[Parameter]
[Category("Appearance")]
public string Height { get; set; } = "80%";
[Parameter]
public string XAxis { get; set; }
public string[] XAxisLabels = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep" };
protected override async Task OnInitializedAsync()
{
options.InterpolationOption = InterpolationOption.NaturalSpline;
options.YAxisFormat = "c2";
ChartOptions = options;
}
}

View File

@@ -0,0 +1,33 @@
<MudPaper Elevation="0" Class="ma-1 pa-1">
@if (IsSelected)
{
<MudStack>
<MudText Typo="Typo.caption" Color="Color.Primary">(选中状态,在此编辑具体题目内容)</MudText>
<MudTextField Label="Question" @bind-Value="QuestionItem.Question.QuestionText" AutoGrow="true"></MudTextField>
</MudStack>
<MudTextField Label="Answer" @bind-Value="QuestionItem.Question.CorrectAnswer"></MudTextField>
}
else
{
<MudTextField @bind-Value=QuestionItem.Question.QuestionText AutoGrow="true"> </MudTextField>
}
</MudPaper>
@code {
[Parameter]
public QuestionItem QuestionItem { get; set; }
[Parameter]
public bool IsSelected { get; set; }
}

View File

@@ -0,0 +1,27 @@

.hover-highlight-class
{
background-color: black; /* 使用 MudBlazor 的悬停背景颜色变量 */
cursor: pointer; /* 改变鼠标指针,提示可点击 */
}
/* 可选:如果您希望选中状态有特殊的背景色,可以添加这个 */
.selected-highlight-class {
/* 例如:浅蓝色背景 */
/* background-color: var(--mud-palette-primary-lighten); */
/* 或者仅是边框颜色变化 (已经在 Wrapper 中实现了) */
/* border-color: var(--mud-palette-primary) !important; */
/* border-width: 1px !important; */
}
/* 可选:当同时处于选中和悬停状态时的样式 */
.selected-highlight-class.hover-highlight-class {
/* 例如:比单独悬停颜色更深一点的背景 */
background-color: var(--mud-palette-primary-darken);
}
/* 确保 MudCard 的边框在选中时可见 */
.mud-card.selected-highlight-class {
border-style: solid; /* 确保边框样式为实线 */
}

View File

@@ -0,0 +1,198 @@
@rendermode InteractiveServer
@if (GroupSelected)
{
<MudPaper @onclick="HandleClick" Class="pa-1 ma-1" Outlined="false" Elevation="0">
<MudStack Row="true">
<MudButton Color="Color.Surface" Variant="Variant.Outlined" OnClick="OnAddText"> TEXT </MudButton>
<MudButton Color="Color.Surface" Variant="Variant.Outlined" OnClick="OnAddRadio"> Radio </MudButton>
@* <MudSelect @bind-Value="GropType" Variant="Variant.Outlined">
<MudSelectItem Value="@(GropType.Stack)" />
<MudSelectItem Value="@(GropType.Grid)" />
</MudSelect> *@
</MudStack>
<MudDivider/>
@switch (@GropType)
{
case GropType.Stack:
<MudStack Spacing="6">
@foreach (var question in QuestionGroupElement.GroupsQuestions)
{
<QuestionBase QuestionElement="question"
IsSelected="question.IsSelected"
MoveDown="HandleMoveDown"
MoveUp="HandleMoveUp"
OnDeleted="HandleQuestionDeleted"
OnSelected="HandleSelected" />
}
</MudStack>
break;
case GropType.Grid:
<MudGrid Spacing="6" xs="3">
@foreach (var question in QuestionGroupElement.GroupsQuestions)
{
<MudItem>
<QuestionBase QuestionElement="question"
IsSelected="question.IsSelected"
MoveDown="HandleMoveDown"
MoveUp="HandleMoveUp"
OnDeleted="HandleQuestionDeleted"
OnSelected="HandleSelected" />
</MudItem>
}
</MudGrid>
break;
default:
<MudText Typo="Typo.h6">布局类型: 未知</MudText>
break;
}
</MudPaper>
}
else
{
@switch (@GropType)
{
case GropType.Stack:
<MudStack Spacing="6">
@foreach (var question in QuestionGroupElement.GroupsQuestions)
{
<QuestionBase QuestionElement="question"
IsSelected="question.IsSelected"
MoveDown="HandleMoveDown"
MoveUp="HandleMoveUp"
OnDeleted="HandleQuestionDeleted"
OnSelected="HandleSelected" />
}
</MudStack>
break;
case GropType.Grid:
<MudGrid Spacing="6" xs="3">
@foreach (var question in QuestionGroupElement.GroupsQuestions)
{
<MudItem>
<QuestionBase QuestionElement="question"
IsSelected="question.IsSelected"
MoveDown="HandleMoveDown"
MoveUp="HandleMoveUp"
OnDeleted="HandleQuestionDeleted"
OnSelected="HandleSelected" />
</MudItem>
}
</MudGrid>
break;
default:
<MudText Typo="Typo.h6">布局类型: 未知</MudText>
break;
}
}
@code {
[Parameter]
public GropType GropType { get; set; } = GropType.Stack;
[Parameter]
public bool GroupSelected { get; set; }
[Parameter]
public QuestionGroupElement QuestionGroupElement { get; set; }
[Parameter]
public bool IsSelected { get; set; }
private int preSelected = 0;
protected override void OnInitialized()
{
QuestionGroupElement = new QuestionGroupElement();
QuestionGroupElement.GroupsQuestions = new List<QuestionElement>();
}
private void HandleMoveUp(int index)
{
if (index >= QuestionGroupElement.GroupsQuestions.Count) return;
QuestionGroupElement.GroupsQuestions.MoveUp(QuestionGroupElement.GroupsQuestions[index]);
ReOrderIndex();
StateHasChanged();
}
private void HandleMoveDown(int index)
{
if (index >= QuestionGroupElement.GroupsQuestions.Count) return;
QuestionGroupElement.GroupsQuestions.MoveDown(QuestionGroupElement.GroupsQuestions[index]);
ReOrderIndex();
StateHasChanged();
}
private void HandleQuestionDeleted(int questionId)
{
var questionToRemove = QuestionGroupElement.GroupsQuestions.FirstOrDefault(q => q.Index == questionId);
if (questionToRemove != null)
{
QuestionGroupElement.GroupsQuestions.Remove(questionToRemove);
StateHasChanged();
}
ReOrderIndex();
}
private void ReOrderIndex()
{
foreach (var que in QuestionGroupElement.GroupsQuestions)
{
que.Index = QuestionGroupElement.GroupsQuestions.IndexOf(que);
}
}
public void OnAddText()
{
QuestionGroupElement.GroupsQuestions.Add(new QuestionElement { Index = QuestionGroupElement.GroupsQuestions.Count });
StateHasChanged();
}
public void OnAddRadio()
{
QuestionGroupElement.GroupsQuestions.Add(new QuestionElement { Index = QuestionGroupElement.GroupsQuestions.Count, QuestionType = BaseQuestionType.Radio });
StateHasChanged();
}
private void HandleClick(MouseEventArgs e)
{
HandleSelected(-1);
}
private void HandleSelected(int id)
{
var ques = QuestionGroupElement.GroupsQuestions.FirstOrDefault(x => x.Index == preSelected);
if (ques != null) ques.IsSelected = false;
if (id < 0) return;
var ques2 = QuestionGroupElement.GroupsQuestions.FirstOrDefault(x => x.Index == id);
if (ques2 != null) ques2.IsSelected = true;
preSelected = id;
StateHasChanged();
}
}

View File

@@ -0,0 +1,98 @@
<MudPaper @onclick:stopPropagation>
<MudPaper Outlined="false" Elevation="0"
@onclick="HandleClick"
@onmouseover="HandleMouseOver"
@onmouseout="HandleMouseOut">
@if (IsSelected)
{
<MudIconButton Icon="@Icons.Material.Filled.MoveUp" Color="Color.Success" Size="Size.Small" OnClick="HandleMoveUp" />
<MudIconButton Icon="@Icons.Material.Filled.MoveDown" Color="Color.Success" Size="Size.Small" OnClick="HandleMoveDown" />
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" Size="Size.Small" OnClick="HandleDelete" />
}
@switch (QuestionElement.QuestionType)
{
case BaseQuestionType.Text:
<Blank IsSelected="IsSelected" QuestionItem="QuestionElement.QuestionItem" />
break;
case BaseQuestionType.Radio:
<RadioChoice IsSelected="IsSelected" QuestionItem="QuestionElement.QuestionItem" />
break;
}
</MudPaper>
</MudPaper>
@code {
[Parameter]
public QuestionElement QuestionElement { get; set; }
[Parameter]
public bool IsSelected { get; set; }
[Parameter]
public EventCallback<int> OnSelected { get; set; }
[Parameter]
public EventCallback<int> OnDeleted { get; set; }
[Parameter]
public EventCallback<int> MoveUp { get; set; }
[Parameter]
public EventCallback<int> MoveDown { get; set; }
private bool _isHovered = false;
private async Task HandleDelete()
{
await OnDeleted.InvokeAsync(QuestionElement.Index);
}
private async Task HandleMoveUp()
{
await MoveUp.InvokeAsync(QuestionElement.Index);
}
private async Task HandleMoveDown()
{
await MoveDown.InvokeAsync(QuestionElement.Index);
}
private async Task HandleClick(MouseEventArgs args)
{
if (QuestionElement.QuestionItem != null && OnSelected.HasDelegate)
{
await OnSelected.InvokeAsync(QuestionElement.Index);
}
}
private void HandleMouseOver()
{
_isHovered = true;
StateHasChanged();
}
private void HandleMouseOut()
{
_isHovered = false;
StateHasChanged();
}
private void OnCompeli()
{
IsSelected = false;
StateHasChanged();
}
}

View File

@@ -0,0 +1,124 @@
@rendermode InteractiveServer
<MudPaper @onclick="HandleClick" Outlined="true" Elevation="@(IsSelected ? 8 : 2)" Class="ma-5 pa-2">
@if (IsSelected)
{
<MudPaper Elevation="0" Class="my-2">
<MudIconButton Icon="@Icons.Material.Filled.MoveUp" Color="Color.Success" Size="Size.Small" OnClick="HandleMoveUp" />
<MudIconButton Icon="@Icons.Material.Filled.MoveDown" Color="Color.Success" Size="Size.Small" OnClick="HandleMoveDown" />
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" Size="Size.Small" OnClick="HandleDelete" />
<MudDivider />
<MudStack Row="true">
<MudText> @QuestionGroupElement.Number </MudText>
<MudTextField @bind-Value=QuestionGroupElement.Title></MudTextField>
</MudStack>
<MudDivider />
<MudTextField Label="Descript" @bind-Value=QuestionGroupElement.Descript AutoGrow></MudTextField>
</MudPaper>
}
else
{
<MudStack>
<MudStack Row="true">
<MudText> @QuestionGroupElement.Number </MudText>
<MudText Typo="Typo.h6">@QuestionGroupElement.Title</MudText>
</MudStack>
<MudDivider />
@if (!string.IsNullOrEmpty(QuestionGroupElement.Descript))
{
<MudTextField ReadOnly="true" @bind-Value=QuestionGroupElement.Descript AutoGrow></MudTextField>
}
</MudStack>
}
@switch (QuestionGroupElement.QuestionType)
{
case QuestionType.Spelling:
<CommonGroup QuestionGroupElement="QuestionGroupElement" IsSelected="QuestionGroupElement.IsSelected" GroupSelected="IsSelected" />
break;
default:
<MudText Color="Color.Warning">未知或未实现的编辑器类型: @QuestionGroupElement.QuestionType</MudText>
@if (IsSelected)
{
<MudText Typo="Typo.body2">选中此题,但无对应编辑器可编辑内容。</MudText>
}
break;
}
<MudDivider />
</MudPaper>
@code {
[Parameter]
public QuestionGroupElement QuestionGroupElement { get; set; }
[Parameter]
public bool IsSelected { get; set; }
[Parameter]
public EventCallback<int> OnSelected { get; set; }
[Parameter]
public EventCallback<int> OnDeleted { get; set; }
[Parameter]
public EventCallback<int> MoveUp { get; set; }
[Parameter]
public EventCallback<int> MoveDown { get; set; }
private async Task HandleClick()
{
if (!IsSelected)
{
await OnSelected.InvokeAsync(QuestionGroupElement.Number);
}
}
private async Task HandleDelete()
{
await OnDeleted.InvokeAsync(QuestionGroupElement.Number);
}
private async Task HandleMoveUp()
{
await MoveUp.InvokeAsync(QuestionGroupElement.Number);
}
private async Task HandleMoveDown()
{
await MoveDown.InvokeAsync(QuestionGroupElement.Number);
}
private string GetQuestionTypeName(QuestionType type)
{
return type switch
{
QuestionType.Spelling => "拼写题",
QuestionType.Pronunciation => "读音选择题",
QuestionType.WordFormation => "组词题",
QuestionType.FillInTheBlanks => "选词填空/补充词语",
QuestionType.SentenceDictation => "默写句子",
QuestionType.SentenceRewriting => "仿句/改写",
QuestionType.ReadingComprehension => "阅读理解",
QuestionType.Composition => "作文题",
_ => "未知类型"
};
}
}

View File

@@ -0,0 +1,165 @@
@if (IsSelected)
{
<MudPaper Outlined="true" Class="ma-1 pa-1">
<MudGrid>
<MudItem xs="12" md="8">
<MudStack>
<MudStack Row="true" Spacing="2">
<MudText Typo="Typo.caption" Color="Color.Primary">(选中状态,在此编辑具体题目内容)</MudText>
</MudStack>
<MudTextField @bind-Value="QuestionItem.Question.QuestionText" AutoGrow="true"></MudTextField>
<MudNumericField @bind-Value="Num" Label="选项数量" Variant="Variant.Outlined" Dense="true" Min="0" Max="byte.MaxValue" />
<MudTextField @bind-Value="QuestionItem.Question.CorrectAnswer" AutoGrow="true"></MudTextField>
</MudStack>
</MudItem>
<MudItem xs="12" md="4">
<MudStack>
@for (int index = 0; index < QuestionItem.Radio.Count; index++)
{
var tempIndex = index;
<MudTextField @bind-Value="QuestionItem.Radio[tempIndex]"
Label="@($"选项 {tempIndex + 1}")"
Color="Color.Primary"
Variant="Variant.Outlined" Dense="true">
</MudTextField>
}
</MudStack>
@if (QuestionItem.Radio.Count > 0)
{
<MudRadioGroup T="string" @bind-Value="QuestionItem.Question.CorrectAnswer">
<MudText Typo="Typo.body2">设置正确选项:</MudText>
@foreach (var optionText in QuestionItem.Radio)
{
<MudRadio Value="@optionText" Color="Color.Success">@optionText</MudRadio>
}
</MudRadioGroup>
}
</MudItem>
</MudGrid>
</MudPaper>
}
<MudStack Spacing="1">
<MudText> @QuestionItem.Title </MudText>
@if (QuestionItem.Radio.Count > 0)
{
<MudRadioGroup T="string" ReadOnly="true">
@foreach (var optionText in QuestionItem.Radio)
{
<MudRadio Value="@optionText">@optionText</MudRadio>
}
</MudRadioGroup>
}
<MudText Typo="Typo.body2">
正确答案: @(QuestionItem?.Question.CorrectAnswer ?? "(未设置)")
</MudText>
</MudStack>
@using Microsoft.AspNetCore.Components.Web
@code {
private bool _oldIsSelected;
private const string OptionsDelimiter = "[OPTIONS]";
private const string ItemDelimiter = "[SEP]";
public string SelectedOption { get; set; }
public int Num
{
get { return QuestionItem.Radio.Count; }
set
{
if (QuestionItem.Radio == null)
{
QuestionItem.Radio = new List<string>();
}
int targetCount = value;
while (QuestionItem.Radio.Count < targetCount)
{
QuestionItem.Radio.Add($"选项 {QuestionItem.Radio.Count + 1}");
}
while (QuestionItem.Radio.Count > targetCount)
{
QuestionItem.Radio.RemoveAt(QuestionItem.Radio.Count - 1);
}
StateHasChanged();
}
}
[Parameter]
public QuestionItem QuestionItem { get; set; }
[Parameter]
public bool IsSelected { get; set; }
protected override async Task OnParametersSetAsync()
{
if (IsSelected && !_oldIsSelected)
{
ParseCombinedString(QuestionItem.Question.QuestionText);
}
else if (!IsSelected && _oldIsSelected)
{
CombineDataIntoString(QuestionItem.Question);
}
_oldIsSelected = IsSelected;
await base.OnParametersSetAsync();
}
private void ParseCombinedString(string combinedText)
{
QuestionItem.Radio.Clear();
if (!string.IsNullOrEmpty(combinedText) && combinedText.Contains(OptionsDelimiter))
{
var parts = combinedText.Split(new[] { OptionsDelimiter }, 2, StringSplitOptions.None);
if (parts.Length == 2)
{
QuestionItem.Question.QuestionText = parts[0];
var optionStrings = parts[1].Split(new[] { ItemDelimiter }, StringSplitOptions.None);
foreach (var opt in optionStrings)
{
QuestionItem.Radio.Add(opt);
}
}
else
{
QuestionItem.Question.QuestionText = combinedText;
}
}
else
{
}
StateHasChanged();
}
private void CombineDataIntoString(Question questionToUpdate)
{
QuestionItem.Title = QuestionItem.Question.QuestionText;
string combinedText = questionToUpdate.QuestionText + OptionsDelimiter + string.Join(ItemDelimiter, QuestionItem.Radio);
questionToUpdate.QuestionText = combinedText;
}
}

View File

@@ -0,0 +1,41 @@
<MudPaper Class="pa-5 ma-5 rounded-lg" Width="@Width" >
<MudChart ChartType=@ChartType ChartSeries="@Series" XAxisLabels="@XAxisLabels" Width=@Width Height=@Height ChartOptions=@ChartOptions></MudChart>
</MudPaper>
@code {
private ChartOptions options = new ChartOptions();
public List<ChartSeries> Series = new List<ChartSeries>()
{
new ChartSeries() { Name = "Series 1", Data = new double[] { 90, 79, 72, 69, 62, 62, 55, 65, 70 } },
new ChartSeries() { Name = "Series 2", Data = new double[] { 35, 41, 35, 51, 49, 62, 69, 91, 148 } },
};
[Parameter]
[Category("Behavior")]
public ChartType ChartType { get; set; } = ChartType.Line;
[Parameter]
[Category("Appearance")]
public string Width { get; set; } = "80%";
[Parameter]
[Category("Appearance")]
public ChartOptions ChartOptions { get; set; } = new ChartOptions();
[Parameter]
[Category("Appearance")]
public string Height { get; set; } = "80%";
[Parameter]
public string XAxis { get; set; }
public string[] XAxisLabels = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep" };
protected override async Task OnInitializedAsync()
{
options.InterpolationOption = InterpolationOption.NaturalSpline;
options.YAxisFormat = "c2";
ChartOptions = options;
}
}

View File

@@ -0,0 +1,5 @@

@code {
}

View File

@@ -0,0 +1,33 @@

<MudPaper Class="pa-10 ma-5">
<MudText Typo="Typo.h1"> @user.Username</MudText>
<MudText Typo="Typo.h6"> Role :@user.Role</MudText>
<MudText Typo="Typo.h6"> Email : @user.Email</MudText>
</MudPaper>
@code {
[Inject]
IUserService userService { get; set; }
[Parameter]
public int UserId { get; set; } = 0;
private User user = new User();
protected override async Task OnInitializedAsync()
{
var result = await userService.GetByIDAsync((uint)UserId);
if (result.Successed)
{
user = result.Data;
}
else
{
}
}
}

View File

@@ -0,0 +1,18 @@
@page "/counter"
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}

View File

@@ -0,0 +1,113 @@
@page "/Edit"
@using Blazored.TextEditor
@using System.Text.RegularExpressions
@using TechHelper.Client.Pages.Exam
<MudPaper Class="d-flex flex-column flex-grow-1">
<MudPaper class="d-flex flex-grow-0 flex-column">
@if (@lode == true)
{
<MudStack Row="true">
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
</MudStack>
}
<MudButtonGroup Color="Color.Primary" Variant="Variant.Filled">
<MudButton OnClick="GetHTML">One</MudButton>
<MudButton OnClick="ParseQuestions">ParseQuestions</MudButton>
<MudButton OnClick="ParseXML">ParseXML</MudButton>
<MudButton OnClick="ParseWithAI">ParseWithAI</MudButton>
<MudButton OnClick="ReCorrectXMLAsync">ReCorrectXML</MudButton>
<MudButton OnClick="ReCorrectXMLAsync">AplyAIResult</MudButton>
<MudButton OnClick="ReCorrectXMLAsync">Save</MudButton>
<MudButton OnClick="ReCorrectXMLAsync">Public</MudButton>
</MudButtonGroup>
</MudPaper>
<MudPaper Class="d-flex flex-row flex-grow-1 overflow-hidden">
<MudPaper Width="33%" Class="d-flex flex-column flex-grow-1 overflow-auto">
@if (QuestionS != null && QuestionS.Any())
{
@foreach (var item in QuestionS)
{
<QuestionGroupDisplay QuestionGroup="item" IsNested="false" />
}
}
else
{
<MudText Typo="Typo.body1">暂无试题内容。</MudText>
}
</MudPaper>
<MudPaper Width="33%" Class="d-flex flex-column flex-grow-1 justify-content-between overflow-auto">
<MudText Typo="Typo.body1">@ProgStatues</MudText>
@for (int i = 0; i < ParseResult.Count; i++)
{
int index = i;
<MudTextField Class="ma-3" AutoGrow="true" @bind-Value="ParseResult[index]"></MudTextField>
}
<MudText>@Error</MudText>
</MudPaper>
<MudPaper Width="33%" Class="d-flex flex-column flex-grow-1 overflow-auto">
<BlazoredTextEditor @ref="@QuillHtml">
<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>
</MudPaper>

View File

@@ -0,0 +1,214 @@
using Blazored.TextEditor;
using Entities.Contracts;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using System.Text.RegularExpressions;
using TechHelper.Client.AI;
using TechHelper.Client.Exam;
using static Org.BouncyCastle.Crypto.Engines.SM2Engine;
namespace TechHelper.Client.Pages.Editor
{
public enum ProgEnum
{
AIPrase,
AIRectify
}
public partial class EditorMain
{
private List<QuestionGroup> QuestionS = new List<QuestionGroup>();
private bool lode = false;
BlazoredTextEditor QuillHtml;
string QuillHTMLContent;
string AIParseResult;
string Error;
string ProgStatues = string.Empty;
List<string> ParseResult = new List<string>();
public async Task GetHTML()
{
QuillHTMLContent = await this.QuillHtml.GetHTML();
}
public async Task GetText()
{
QuillHTMLContent = await this.QuillHtml.GetText();
}
private string EditorHtmlContent { get; set; } = string.Empty;
private List<ParsedQuestion>? ParsedQuestions { get; set; }
private bool _parseAttempted = false;
public class ParsedQuestion
{
public int Id { get; set; } // <20><>Ŀ<EFBFBD><C4BF><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
public string? Content { get; set; } // <20><>Ŀ<EFBFBD><C4BF><EFBFBD><EFBFBD><EFBFBD><EFBFBD> HTML <20><><EFBFBD><EFBFBD>
public string? Title { get; set; } // <20><>Ŀ<EFBFBD>ı<EFBFBD><C4B1>ⲿ<EFBFBD>֣<EFBFBD><D6A3><EFBFBD>ѡ<EFBFBD><D1A1><EFBFBD><EFBFBD><EFBFBD>Դ<EFBFBD> Content <20><><EFBFBD><EFBFBD>ȡ<EFBFBD><C8A1>
}
[Inject]
public IAIService aIService { get; set; }
[Inject]
public ISnackbar Snackbar { get; set; }
private async void ParseWithAI()
{
QuestionS.Clear();
ParseResult.Clear();
await GetText();
lode = true;
StateHasChanged();
ProgStatues = ProgEnum.AIPrase.ToString();
ProgStatues = $"<22><><EFBFBD>ڽ<EFBFBD><DABD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,<2C><><EFBFBD>ȴ<EFBFBD>";
Snackbar.Add("<22><><EFBFBD>ڽ<EFBFBD><DABD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,<2C><><EFBFBD>ȴ<EFBFBD>");
StateHasChanged();
string respon = await aIService.CallGLM(QuillHTMLContent, AIConfiguration.BreakQuestions);
if (respon == null)
{
lode = false;
Snackbar.Add("<22><><EFBFBD>˵<EFBFBD><CBB5><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD>°<EFBFBD>");
return;
}
var ParRespon = ExamParser.ParseExamXml<StringsList>(respon);
if (ParRespon != null)
{
int i = 1;
foreach (var item in ParRespon.Items)
{
ProgStatues = $"<22><><EFBFBD>ڽ<EFBFBD><DABD><EFBFBD><EFBFBD><EFBFBD>{i}<7D><>, <20><><EFBFBD>ȴ<EFBFBD>";
Snackbar.Add($"<22><><EFBFBD>ڽ<EFBFBD><DABD><EFBFBD><EFBFBD><EFBFBD>{i}<7D><>, <20><><EFBFBD>ȴ<EFBFBD>");
StateHasChanged();
i++;
try
{
var parResult = await aIService.CallGLM(item, AIConfiguration.ParseSignelQuestion);
ParseResult.Add(parResult);
}
catch (Exception ex)
{
Snackbar.Add($"<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>{i}<7D><>ʱ<EFBFBD><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>, <20><><EFBFBD>Ժ<EFBFBD><D4BA><EFBFBD><EFBFBD><EFBFBD>. <20><><EFBFBD><EFBFBD>Ϊ:{ex.Message}");
}
}
}
AIParseResult = respon;
ProgStatues = ProgEnum.AIRectify.ToString();
//await ReCorrectXMLAsync();
ProgStatues = string.Empty;
lode = false;
StateHasChanged();
}
private async Task ReCorrectXMLAsync()
{
string respon = string.Empty;
try
{
foreach (var item in ParseResult)
{
//respon = await aIService.CallGLM(AIParseResult, AIConfiguration.ParseSignelQuestion);
var xmlResult = ExamParser.ParseExamXml<QuestionGroup>(item);
QuestionS.Add(xmlResult);
}
}
catch (Exception ex)
{
Snackbar.Add("<22><><EFBFBD>˵<EFBFBD><CBB5><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD>°<EFBFBD>" + ex.Message);
}
if (string.IsNullOrEmpty(respon))
{
lode = false;
Snackbar.Add("<22><><EFBFBD>˵<EFBFBD><CBB5><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD>°<EFBFBD>");
}
AIParseResult = respon;
}
private void ParseXML()
{
try
{
var paper = ExamParser.ParseExamXml<QuestionGroup>(AIParseResult);
//QuestionS = paper.QuestionGroups;
Error = string.Empty;
}
catch (InvalidOperationException ex)
{
Snackbar.Add("<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>" + ex.Message);
}
catch (Exception ex)
{
Snackbar.Add("<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>" + ex.Message);
Error = ex.Message;
}
}
private void ParseQuestions()
{
ParsedQuestions = new List<ParsedQuestion>();
_parseAttempted = true;
if (string.IsNullOrWhiteSpace(QuillHTMLContent))
{
return;
}
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>ʼ<EFBFBD>ͽ<EFBFBD><CDBD><EFBFBD><EFBFBD>ı<EFBFBD><C4B1><EFBFBD>
string startTag = "[<5B><>Ŀ<EFBFBD><C4BF>ʼ]";
string endTag = "[<5B><>Ŀ<EFBFBD><C4BF><EFBFBD><EFBFBD>]";
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʽ<EFBFBD><CABD>ƥ<EFBFBD><C6A5><EFBFBD>ӿ<EFBFBD>ʼ<EFBFBD><CABC><EFBFBD>ǵ<EFBFBD><C7B5><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֮<EFBFBD><D6AE><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ݣ<EFBFBD><DDA3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>У<EFBFBD>
// (?s) <20><><EFBFBD>õ<EFBFBD><C3B5><EFBFBD>ģʽ<C4A3><CABD><EFBFBD><EFBFBD> . ƥ<><EFBFBD>з<EFBFBD>
string pattern = Regex.Escape(startTag) + "(.*?)" + Regex.Escape(endTag);
var matches = Regex.Matches(QuillHTMLContent, pattern, RegexOptions.Singleline);
int questionId = 1;
foreach (Match match in matches)
{
// <20><>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֮<EFBFBD><D6AE><EFBFBD>Ĵ<EFBFBD><C4B4><EFBFBD><EFBFBD><EFBFBD>
string rawQuestionHtml = match.Groups[1].Value;
// <20>Ƴ<EFBFBD><C6B3><EFBFBD><EFBFBD>ܲ<EFBFBD><DCB2><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>еı<D0B5><C4B1>ǣ<EFBFBD><C7A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʽ<EFBFBD>Ѿ<EFBFBD><D1BE>ų<EFBFBD><C5B3><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ǣ<EFBFBD><C7A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD><D2BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
string cleanedQuestionContent = rawQuestionHtml
.Replace(startTag, "")
.Replace(endTag, "")
.Trim();
// <20><><EFBFBD>Դ<EFBFBD><D4B4><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD><E2A3A8><EFBFBD>磬ƥ<E7A3AC>һ<E4A1B0><D2BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>1<EFBFBD><31><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͷ<EFBFBD><CDB7><EFBFBD>У<EFBFBD>
string? questionTitle = null;
try
{
var firstLineMatch = Regex.Match(cleanedQuestionContent, @"^(<p>)?\s*([һ<><D2BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>߰˾<DFB0>ʮ]+\s*[<5B><><EFBFBD><EFBFBD>]|\d+\s*[<5B><>\.]).*?</p>", RegexOptions.IgnoreCase | RegexOptions.Singleline);
if (firstLineMatch.Success)
{
// <20><> HTML <20><><EFBFBD><EFBFBD>ȡ<EFBFBD><C8A1><EFBFBD>ı<EFBFBD><C4B1><EFBFBD>Ϊ<EFBFBD><CEAA><EFBFBD><EFBFBD>
questionTitle = Regex.Replace(firstLineMatch.Value, "<[^>]*>", "").Trim();
}
}
catch (Exception ex) { }
ParsedQuestions.Add(new ParsedQuestion
{
Id = questionId++,
Content = cleanedQuestionContent,
Title = questionTitle
});
}
}
}
}

View File

@@ -0,0 +1,50 @@
@page "/test"
<MudPaper Class="d-flex flex-column justify-space-around flex-grow-1 overflow-scroll">
<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>
</MudPaper>
@code {
}

View File

@@ -0,0 +1,4 @@
@page "/errorpage"
<MudText> 出错了,请联系管理员 </MudText>

View File

@@ -0,0 +1,8 @@
@page "/404"
@page "/notfound"
<MudText>
404 Not Found
</MudText>

View File

@@ -0,0 +1,8 @@
@page "/401"
@page "/unauthorized"
<MudText Typo="Typo.h1">
Unauthorized
</MudText>

View File

@@ -0,0 +1 @@
<h3>ChoiceQuestion</h3>

View File

@@ -0,0 +1,11 @@
<MudText> @Title </MudText>
@code {
[Parameter]
public string Title { get; set; }
[Parameter]
public string Answer { get; set; }
}

View File

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

@code {
}

View File

@@ -0,0 +1,73 @@
@using TechHelper.Client.Exam
<MudCard Class="@(IsNested ? "mb-3 pa-2" : "my-4")" Outlined="@IsNested">
@* 嵌套时添加边框和内边距 *@
<MudCardHeader>
<MudStack>
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudText Typo="@(IsNested ? Typo.h6 : Typo.h5)">@QuestionGroup.Id. </MudText> @* 嵌套时字号稍小 *@
<MudText Typo="@(IsNested ? Typo.h6 : Typo.h5)">@QuestionGroup.Title</MudText>
@if (!string.IsNullOrEmpty(QuestionGroup.ScoreProblemMarker))
{
<MudText Typo="Typo.caption" Color="Color.Warning" Class="ml-2">( @QuestionGroup.ScoreProblemMarker )</MudText>
}
</MudStack>
@if (!string.IsNullOrEmpty(QuestionGroup.QuestionReference))
{
<MudText Class="mt-2" Style="white-space: pre-wrap;">@QuestionGroup.QuestionReference</MudText>
}
</MudStack>
</MudCardHeader>
<MudCardContent>
@* 渲染直接子题目 *@
@if (QuestionGroup.SubQuestions != null && QuestionGroup.SubQuestions.Any())
{
@if (!IsNested) // 只有顶级大题才显示“子题目”标题
{
<MudText Typo="Typo.subtitle1" Class="mb-2">题目详情:</MudText>
}
@foreach (var qitem in QuestionGroup.SubQuestions)
{
<MudStack Row="true" AlignItems="AlignItems.Baseline" Class="mb-2">
<MudText Typo="Typo.body1">@qitem.SubId. </MudText>
<MudText Typo="Typo.body1">@qitem.Stem</MudText>
@if (!string.IsNullOrEmpty(qitem.ScoreProblemMarker))
{
<MudText Typo="Typo.caption" Color="Color.Warning" Class="ml-1">( @qitem.ScoreProblemMarker )</MudText>
}
</MudStack>
@if (!string.IsNullOrEmpty(qitem.SampleAnswer))
{
<MudText Typo="Typo.body2" Color="Color.Tertiary" Class="ml-6 mb-2">示例答案: @qitem.SampleAnswer</MudText>
}
@if (qitem.Options != null && qitem.Options.Any())
{
}
}
}
@* 递归渲染子题组 *@
@if (QuestionGroup.SubQuestionGroups != null && QuestionGroup.SubQuestionGroups.Any())
{
<MudDivider Class="my-4" />
@if (!IsNested) // 只有顶级大题才显示“嵌套题组”标题
{
<MudText Typo="Typo.subtitle1" Class="mb-2">相关题组:</MudText>
}
@foreach (var subGroup in QuestionGroup.SubQuestionGroups)
{
<QuestionGroupDisplay QuestionGroup="subGroup" IsNested="true" /> @* 递归调用自身 *@
}
}
</MudCardContent>
</MudCard>
@code {
[Parameter]
public TechHelper.Client.Exam.QuestionGroup QuestionGroup { get; set; } = new TechHelper.Client.Exam.QuestionGroup();
[Parameter]
public bool IsNested { get; set; } = false; // 判断是否是嵌套的题组,用于调整样式和显示标题
}

View File

@@ -0,0 +1,21 @@
@page "/"
@using Microsoft.AspNetCore.Authorization
<AuthorizeView Roles="Administrator">
<MudText> Hello @context.User.Identity.Name</MudText>
@foreach (var item in context.User.Claims)
{
<MudPaper class="ma-2 pa-2">
<MudText> @item.Value </MudText>
<MudText> @item.Issuer </MudText>
<MudText> @item.Subject </MudText>
<MudText> @item.Properties </MudText>
<MudText> @item.ValueType </MudText>
</MudPaper>
}
Welcome to your new app.
</AuthorizeView>
<MudText>Hello </MudText>

View File

@@ -0,0 +1,58 @@
@page "/Account/Manage/Class"
@using System.ComponentModel.DataAnnotations
@using Entities.Contracts
@using Microsoft.AspNetCore.Identity
<PageTitle>Profile</PageTitle>
@if (authenticationStateTask.Result.User.FindFirst("Class")?.Value == null)
{
<EditForm Model="@_userRegistrationToClassDto" OnValidSubmit="Register" FormName="ClassForm">
<DataAnnotationsValidator />
<MudGrid>
<MudItem xs="12" sm="7">
<MudCard>
<MudCardContent>
<MudRadioGroup T="UserRoles" Label="Roles" @bind-Value="_userRegistrationToClassDto.Roles">
@foreach (UserRoles item in Enum.GetValues(typeof(UserRoles)))
{
if (item != UserRoles.Administrator)
{
<MudRadio Value="@item">@item.ToString()</MudRadio>
}
}
</MudRadioGroup>
<MudTextField Label="Grade" Class="mt-3"
@bind-Value="_userRegistrationToClassDto.GradeId" For="@(() => _userRegistrationToClassDto.GradeId)" />
<MudTextField Label="Class" HelperText="请输入你的班级" Class="mt-3"
@bind-Value="_userRegistrationToClassDto.ClassId" For="@(() => _userRegistrationToClassDto.ClassId)" />
@if (_userRegistrationToClassDto.Roles == UserRoles.Teacher)
{
<MudSelect @bind-Value="_userRegistrationToClassDto.SubjectArea" Label="Select Subject" AdornmentColor="Color.Secondary">
@foreach (SubjectAreaEnum item in Enum.GetValues(typeof(SubjectAreaEnum)))
{
<MudSelectItem Value="@item">@item</MudSelectItem>
}
</MudSelect>
}
</MudCardContent>
<MudCardActions>
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary" Class="ml-auto">Register</MudButton>
</MudCardActions>
</MudCard>
</MudItem>
</MudGrid>
</EditForm>
}
else
{
<MudPaper>
<MudText Class="ma-3 pa-3"> 年级 : @authenticationStateTask.Result.User.FindFirst("Grade")?.Value.ToString() </MudText>
<MudText Class="ma-3 pa-3"> 班级 : @authenticationStateTask.Result.User.FindFirst("Class")?.Value.ToString() </MudText>
</MudPaper>
}

View File

@@ -0,0 +1,62 @@
using Entities.DTO;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using MudBlazor;
using System.Collections.Frozen;
using System.ComponentModel.DataAnnotations;
using TechHelper.Client.HttpRepository;
using TechHelper.Client.Services;
namespace TechHelper.Client.Pages.Manage
{
public partial class Class
{
private UserRegistrationToClassDto _userRegistrationToClassDto = new UserRegistrationToClassDto();
private string? username;
private string? phoneNumber;
[Inject]
public ISnackbar Snackbar { get; set; }
[Inject]
public IAuthenticationClientService Authentication { get; set; }
[Inject]
public IClassServices ClassServices { get; set; }
[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
protected override async Task OnInitializedAsync()
{
username = authenticationStateTask.Result.User.Identity.Name;
phoneNumber = authenticationStateTask.Result.User.Identity.IsAuthenticated.ToString();
Input.PhoneNumber ??= phoneNumber;
}
private async Task Register()
{
_userRegistrationToClassDto.User = authenticationStateTask.Result.User.Identity.Name?? "";
var result = await ClassServices.UserRegister(_userRegistrationToClassDto);
if (result.IsSuccessfulRegistration)
{
var token = await Authentication.RefreshTokenAsync();
Snackbar.Add("<22>༶ע<E0BCB6><D7A2><EFBFBD>ɹ<EFBFBD>", Severity.Info);
}
else
{
Snackbar.Add(result.Errors.First(), Severity.Error);
}
}
private sealed class InputModel
{
[Phone]
[Display(Name = "Phone number")]
public string? PhoneNumber { get; set; }
}
}
}

View File

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

View File

@@ -0,0 +1,30 @@
@page "/Account/Manage/PersonalData"
<PageTitle>Personal Data</PageTitle>
<h3>Personal Data</h3>
<div class="row">
<div class="col-md-6">
<p>Your account contains personal data that you have given us. This page allows you to download or delete that data.</p>
<p>
<strong>Deleting this data will permanently remove your account, and this cannot be recovered.</strong>
</p>
<form action="Account/Manage/DownloadPersonalData" method="post">
<AntiforgeryToken />
<button class="btn btn-primary" type="submit">Download</button>
</form>
<p>
<a href="Account/Manage/DeletePersonalData" class="btn btn-danger">Delete</a>
</p>
</div>
</div>
@code {
protected override async Task OnInitializedAsync()
{
}
}

View File

@@ -0,0 +1,2 @@
@using TechHelper.Client.Shared
@layout ManageLayout

View File

@@ -0,0 +1,57 @@
@page "/weather"
@inject HttpClient Http
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>This component demonstrates fetching data from the server.</p>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");
}
public class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}

View File

@@ -0,0 +1,2 @@
@using TechHelper.Client.Shared
@layout AccountLayout

View File

@@ -0,0 +1,65 @@
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using TechHelper.Client;
using MudBlazor;
using BlazorProducts.Client.HttpInterceptor;
using Entities.Configuration;
using Microsoft.AspNetCore.Components.Authorization;
using MudBlazor.Services;
using TechHelper.Client.AuthProviders;
using TechHelper.Client.HttpRepository;
using TechHelper.Features;
using TechHelper.Client.Services;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using TechHelper.Client.AI;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddOidcAuthentication(options =>
{
builder.Configuration.Bind("Local", options.ProviderOptions);
});
builder.Services.AddAuthorizationCore();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddLocalStorageServices();
builder.Services.AddScoped<IAuthenticationClientService, AuthenticationClientService>();
builder.Services.AddScoped<AuthenticationStateProvider, AuthStateProvider>();
builder.Services.Configure<ApiConfiguration>(builder.Configuration.GetSection("ApiConfiguration"));
builder.Services.AddScoped<RefreshTokenService>();
builder.Services.AddScoped<IClassServices, ClasssServices>();
builder.Services.AddScoped<IEmailSender, QEmailSender>();
builder.Services.AddTransient<HttpInterceptorHandlerService>();
builder.Services.AddHttpClient("Default", (sp, cl) =>
{
var apiConfiguration = sp.GetRequiredService<IOptions<ApiConfiguration>>().Value;
cl.BaseAddress = new Uri(apiConfiguration.BaseAddress + "/api/");
})
.AddHttpMessageHandler<HttpInterceptorHandlerService>();
builder.Services.AddScoped<IAIService, AiService>();
//builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
//builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("Default"));
builder.Services.AddMudServices(config =>
{
config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomLeft;
config.SnackbarConfiguration.PreventDuplicates = false;
config.SnackbarConfiguration.NewestOnTop = false;
config.SnackbarConfiguration.ShowCloseIcon = true;
config.SnackbarConfiguration.VisibleStateDuration = 1000;
config.SnackbarConfiguration.HideTransitionDuration = 500;
config.SnackbarConfiguration.ShowTransitionDuration = 500;
config.SnackbarConfiguration.SnackbarVariant = Variant.Filled;
});
await builder.Build().RunAsync();

View File

@@ -0,0 +1,31 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:46812",
"sslPort": 44359
}
},
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://localhost:7047;http://localhost:5190",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,45 @@
using Entities.DTO;
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
namespace TechHelper.Client.Services
{
public class ClasssServices : IClassServices
{
private HttpClient _client;
private IHttpClientFactory _httpClientFactory;
public ClasssServices(HttpClient client, IHttpClientFactory httpClientFactory)
{
_client = client;
_httpClientFactory = httpClientFactory;
}
public Task<ResponseDto> CreateClass(UserRegistrationToClassDto userClass)
{
throw new NotImplementedException();
}
public async Task<ResponseDto> UserRegister(UserRegistrationToClassDto userRegistrationToClassDto)
{
using (_client = _httpClientFactory.CreateClient("Default"))
{
try
{
var result = await _client.PostAsJsonAsync("class/userRegiste",
userRegistrationToClassDto);
var data = await result.Content.ReadAsStringAsync();
return new ResponseDto { IsSuccessfulRegistration = result.IsSuccessStatusCode, Errors = new string[] { data } };
}
catch (Exception ex)
{
return new ResponseDto { IsSuccessfulRegistration = false, Errors = new string[] { ex.Message } };
}
}
}
}
}

View File

@@ -0,0 +1,12 @@
using Entities.DTO;
using System.Net;
using TechHelper.Services;
namespace TechHelper.Client.Services
{
public interface IClassServices
{
public Task<ResponseDto> UserRegister(UserRegistrationToClassDto userRegistrationToClassDto);
public Task<ResponseDto> CreateClass(UserRegistrationToClassDto userClass);
}
}

View File

@@ -0,0 +1,26 @@
@inherits LayoutComponentBase
@layout TechHelper.Client.Layout.MainLayout
@inject NavigationManager NavigationManager
@if (authenticationStateTask is null)
{
<p>Loading...</p>
}
else
{
@Body
}
@code {
[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }
protected override void OnParametersSet()
{
if (authenticationStateTask is null)
{
NavigationManager.Refresh(forceReload: true);
}
}
}

View File

@@ -0,0 +1,14 @@
@inherits LayoutComponentBase
@layout AccountLayout
<MudPaper Class="d-flex flex-column flex-grow-1">
<h1>Manage your account</h1>
<h2>Change your account settings</h2>
<MudDivider Class="flex-grow-0" />
<MudStack Row="true">
<ManageNavMenu />
@Body
</MudStack>
</MudPaper>

View File

@@ -0,0 +1,22 @@
@using Microsoft.AspNetCore.Identity
<ul class="nav nav-pills flex-column">
<li class="nav-item">
<NavLink class="nav-link" href="Account/Manage" Match="NavLinkMatch.All">Profile</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link" href="Account/Manage/Class">Class</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link" href="Account/Manage/ChangePassword">Password</NavLink>
</li>
@* <li class="nav-item">
<NavLink class="nav-link" href="Account/Manage/TwoFactorAuthentication">Two-factor authentication</NavLink>
</li> *@
</ul>
@code {
private bool hasExternalLogins;
}

View File

@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Pages\Components\**" />
<Content Remove="Pages\Components\**" />
<EmbeddedResource Remove="Pages\Components\**" />
<None Remove="Pages\Components\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Blazor.LocalStorage.WebAssembly" Version="8.0.0" />
<PackageReference Include="Blazored.TextEditor" Version="1.1.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.12" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.12" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.12" />
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="8.0.16" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.5" />
<PackageReference Include="MudBlazor" Version="8.6.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EmailLib\EmailLib.csproj" />
<ProjectReference Include="..\Entities\Entities.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,22 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using TechHelper.Client
@using TechHelper.Client.Layout
@using TechHelper.Client.Shared
@using MudBlazor.Services
@using MudBlazor
@using TechHelper.Client.AuthProviders
@using TechHelper.Client.HttpRepository
@using TechHelper.Client.Pages.Author
@using TechHelper.Client.Pages
@using Blazored.TextEditor

View File

@@ -0,0 +1,6 @@
{
"Local": {
"Authority": "https://login.microsoftonline.com/",
"ClientId": "33333333-3333-3333-33333333333333333"
}
}

View File

@@ -0,0 +1,9 @@
{
"Local": {
"Authority": "https://login.microsoftonline.com/",
"ClientId": "33333333-3333-3333-33333333333333333"
},
"ApiConfiguration": {
"BaseAddress": "http://localhost:5099"
}
}

View File

@@ -0,0 +1,103 @@
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
h1:focus {
outline: none;
}
a, .btn-link {
color: #0071c1;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
.content {
padding-top: 1.1rem;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid red;
}
.validation-message {
color: red;
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
.loading-progress {
position: relative;
display: block;
width: 8rem;
height: 8rem;
margin: 20vh auto 1rem auto;
}
.loading-progress circle {
fill: none;
stroke: #e0e0e0;
stroke-width: 0.6rem;
transform-origin: 50% 50%;
transform: rotate(-90deg);
}
.loading-progress circle:last-child {
stroke: #1b6ec2;
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
transition: stroke-dasharray 0.05s ease-in-out;
}
.loading-progress-text {
position: absolute;
text-align: center;
font-weight: bold;
inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
}
.loading-progress-text:after {
content: var(--blazor-load-percentage-text, "Loading");
}
code {
color: #c02d76;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TechHelper.Client</title>
<base href="/" />
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="css/app.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<link href="TechHelper.Client.styles.css" rel="stylesheet" />
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
<link href="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.snow.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.bubble.css" rel="stylesheet">
</head>
<body>
<div id="app">
<svg class="loading-progress">
<circle r="40%" cx="50%" cy="50%" />
<circle r="40%" cx="50%" cy="50%" />
</svg>
<div class="loading-progress-text"></div>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>
<script src="_framework/blazor.webassembly.js"></script>
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.js"></script>
<script src="_content/Blazored.TextEditor/quill-blot-formatter.min.js"></script>
<script src="_content/Blazored.TextEditor/Blazored-BlazorQuill.js"></script>
</body>
</html>

View File

@@ -0,0 +1,27 @@
[
{
"date": "2022-01-06",
"temperatureC": 1,
"summary": "Freezing"
},
{
"date": "2022-01-07",
"temperatureC": 14,
"summary": "Bracing"
},
{
"date": "2022-01-08",
"temperatureC": -13,
"summary": "Freezing"
},
{
"date": "2022-01-09",
"temperatureC": -16,
"summary": "Balmy"
},
{
"date": "2022-01-10",
"temperatureC": -2,
"summary": "Chilly"
}
]