添加项目文件。
This commit is contained in:
196
TechHelper.Client/Pages/AI/AIDialog.razor
Normal file
196
TechHelper.Client/Pages/AI/AIDialog.razor
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
10
TechHelper.Client/Pages/AccountView.razor
Normal file
10
TechHelper.Client/Pages/AccountView.razor
Normal 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; }
|
||||
}
|
26
TechHelper.Client/Pages/AuthLinks.razor
Normal file
26
TechHelper.Client/Pages/AuthLinks.razor
Normal 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("/");
|
||||
}
|
||||
}
|
7
TechHelper.Client/Pages/Authentication.razor
Normal file
7
TechHelper.Client/Pages/Authentication.razor
Normal file
@@ -0,0 +1,7 @@
|
||||
@page "/authentication/{action}"
|
||||
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||
<RemoteAuthenticatorView Action="@Action" />
|
||||
|
||||
@code{
|
||||
[Parameter] public string? Action { get; set; }
|
||||
}
|
11
TechHelper.Client/Pages/Author/EmailConfirmation.razor
Normal file
11
TechHelper.Client/Pages/Author/EmailConfirmation.razor
Normal file
@@ -0,0 +1,11 @@
|
||||
@page "/emailconfirmation"
|
||||
|
||||
<h3>EmailConfirmation</h3>
|
||||
<MudButton OnClick="ConfirmToEmail"> 点击确认 </MudButton>
|
||||
|
||||
<MudPaper>
|
||||
@if (_showSuccess)
|
||||
{
|
||||
<h3> Successful </h3>
|
||||
}
|
||||
</MudPaper>
|
42
TechHelper.Client/Pages/Author/EmailConfirmation.razor.cs
Normal file
42
TechHelper.Client/Pages/Author/EmailConfirmation.razor.cs
Normal 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("/");
|
||||
}
|
||||
}
|
||||
}
|
38
TechHelper.Client/Pages/Author/ForgotPassword.razor
Normal file
38
TechHelper.Client/Pages/Author/ForgotPassword.razor
Normal 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>
|
30
TechHelper.Client/Pages/Author/ForgotPassword.razor.cs
Normal file
30
TechHelper.Client/Pages/Author/ForgotPassword.razor.cs
Normal 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;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
48
TechHelper.Client/Pages/Author/Login.razor
Normal file
48
TechHelper.Client/Pages/Author/Login.razor
Normal 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>
|
51
TechHelper.Client/Pages/Author/Login.razor.cs
Normal file
51
TechHelper.Client/Pages/Author/Login.razor.cs
Normal 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("/");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
100
TechHelper.Client/Pages/Author/Registration.razor
Normal file
100
TechHelper.Client/Pages/Author/Registration.razor
Normal 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>
|
||||
|
||||
|
62
TechHelper.Client/Pages/Author/Registration.razor.cs
Normal file
62
TechHelper.Client/Pages/Author/Registration.razor.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
49
TechHelper.Client/Pages/Author/ResetPassword.razor
Normal file
49
TechHelper.Client/Pages/Author/ResetPassword.razor
Normal 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>
|
55
TechHelper.Client/Pages/Author/ResetPassword.razor.cs
Normal file
55
TechHelper.Client/Pages/Author/ResetPassword.razor.cs
Normal 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("/");
|
||||
}
|
||||
}
|
||||
}
|
11
TechHelper.Client/Pages/Author/RoleDetailInfo.razor
Normal file
11
TechHelper.Client/Pages/Author/RoleDetailInfo.razor
Normal file
@@ -0,0 +1,11 @@
|
||||
@page "/rodetail"
|
||||
<h3>RoleDetailInfo</h3>
|
||||
|
||||
<AuthorizeView>
|
||||
|
||||
<MudText> @context.User.Identity.Name </MudText>
|
||||
|
||||
</AuthorizeView>
|
||||
@code {
|
||||
|
||||
}
|
47
TechHelper.Client/Pages/Author/Signout.razor
Normal file
47
TechHelper.Client/Pages/Author/Signout.razor
Normal 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."); // 可选日志
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
39
TechHelper.Client/Pages/Author/TwoStepVerification.razor
Normal file
39
TechHelper.Client/Pages/Author/TwoStepVerification.razor
Normal 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>
|
60
TechHelper.Client/Pages/Author/TwoStepVerification.razor.cs
Normal file
60
TechHelper.Client/Pages/Author/TwoStepVerification.razor.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
81
TechHelper.Client/Pages/Components/AssignmentView.razor
Normal file
81
TechHelper.Client/Pages/Components/AssignmentView.razor
Normal 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}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
45
TechHelper.Client/Pages/Components/ErrorDis.razor
Normal file
45
TechHelper.Client/Pages/Components/ErrorDis.razor
Normal 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;
|
||||
}
|
||||
|
||||
}
|
33
TechHelper.Client/Pages/Components/Exam/Blank.razor
Normal file
33
TechHelper.Client/Pages/Components/Exam/Blank.razor
Normal 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; }
|
||||
|
||||
}
|
27
TechHelper.Client/Pages/Components/Exam/Blank.razor.css
Normal file
27
TechHelper.Client/Pages/Components/Exam/Blank.razor.css
Normal 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; /* 确保边框样式为实线 */
|
||||
}
|
||||
|
198
TechHelper.Client/Pages/Components/Exam/CommonGroup.razor
Normal file
198
TechHelper.Client/Pages/Components/Exam/CommonGroup.razor
Normal 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();
|
||||
}
|
||||
}
|
98
TechHelper.Client/Pages/Components/Exam/QuestionBase.razor
Normal file
98
TechHelper.Client/Pages/Components/Exam/QuestionBase.razor
Normal 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();
|
||||
}
|
||||
}
|
124
TechHelper.Client/Pages/Components/Exam/QuestionTemplate.razor
Normal file
124
TechHelper.Client/Pages/Components/Exam/QuestionTemplate.razor
Normal 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 => "作文题",
|
||||
_ => "未知类型"
|
||||
};
|
||||
}
|
||||
}
|
165
TechHelper.Client/Pages/Components/Exam/RadioChoice.razor
Normal file
165
TechHelper.Client/Pages/Components/Exam/RadioChoice.razor
Normal 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;
|
||||
}
|
||||
|
||||
}
|
41
TechHelper.Client/Pages/Components/GrageView.razor
Normal file
41
TechHelper.Client/Pages/Components/GrageView.razor
Normal 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;
|
||||
}
|
||||
|
||||
}
|
5
TechHelper.Client/Pages/Components/HeaderLine.razor
Normal file
5
TechHelper.Client/Pages/Components/HeaderLine.razor
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
|
||||
@code {
|
||||
|
||||
}
|
33
TechHelper.Client/Pages/Components/UserBaseView.razor
Normal file
33
TechHelper.Client/Pages/Components/UserBaseView.razor
Normal 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
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
18
TechHelper.Client/Pages/Counter.razor
Normal file
18
TechHelper.Client/Pages/Counter.razor
Normal 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++;
|
||||
}
|
||||
}
|
113
TechHelper.Client/Pages/Editor/EditorMain.razor
Normal file
113
TechHelper.Client/Pages/Editor/EditorMain.razor
Normal 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>
|
214
TechHelper.Client/Pages/Editor/EditorMain.razor.cs
Normal file
214
TechHelper.Client/Pages/Editor/EditorMain.razor.cs
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
50
TechHelper.Client/Pages/Editor/Test.razor
Normal file
50
TechHelper.Client/Pages/Editor/Test.razor
Normal 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 {
|
||||
|
||||
}
|
4
TechHelper.Client/Pages/ErrorHandle/Error.razor
Normal file
4
TechHelper.Client/Pages/ErrorHandle/Error.razor
Normal file
@@ -0,0 +1,4 @@
|
||||
@page "/errorpage"
|
||||
|
||||
|
||||
<MudText> 出错了,请联系管理员 </MudText>
|
8
TechHelper.Client/Pages/ErrorHandle/Notfound.razor
Normal file
8
TechHelper.Client/Pages/ErrorHandle/Notfound.razor
Normal file
@@ -0,0 +1,8 @@
|
||||
@page "/404"
|
||||
@page "/notfound"
|
||||
|
||||
|
||||
|
||||
<MudText>
|
||||
404 Not Found
|
||||
</MudText>
|
8
TechHelper.Client/Pages/ErrorHandle/Unauthorized.razor
Normal file
8
TechHelper.Client/Pages/ErrorHandle/Unauthorized.razor
Normal file
@@ -0,0 +1,8 @@
|
||||
@page "/401"
|
||||
@page "/unauthorized"
|
||||
|
||||
|
||||
|
||||
<MudText Typo="Typo.h1">
|
||||
Unauthorized
|
||||
</MudText>
|
1
TechHelper.Client/Pages/Exam/ChoiceQuestion.razor
Normal file
1
TechHelper.Client/Pages/Exam/ChoiceQuestion.razor
Normal file
@@ -0,0 +1 @@
|
||||
<h3>ChoiceQuestion</h3>
|
11
TechHelper.Client/Pages/Exam/QuestionBase.razor
Normal file
11
TechHelper.Client/Pages/Exam/QuestionBase.razor
Normal file
@@ -0,0 +1,11 @@
|
||||
<MudText> @Title </MudText>
|
||||
|
||||
|
||||
@code {
|
||||
|
||||
[Parameter]
|
||||
public string Title { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string Answer { get; set; }
|
||||
}
|
4
TechHelper.Client/Pages/Exam/QuestionGroup.razor
Normal file
4
TechHelper.Client/Pages/Exam/QuestionGroup.razor
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
@code {
|
||||
|
||||
}
|
73
TechHelper.Client/Pages/Exam/QuestionGroupDisplay.razor
Normal file
73
TechHelper.Client/Pages/Exam/QuestionGroupDisplay.razor
Normal 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; // 判断是否是嵌套的题组,用于调整样式和显示标题
|
||||
}
|
21
TechHelper.Client/Pages/Home.razor
Normal file
21
TechHelper.Client/Pages/Home.razor
Normal 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>
|
58
TechHelper.Client/Pages/Manage/Class.razor
Normal file
58
TechHelper.Client/Pages/Manage/Class.razor
Normal 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>
|
||||
}
|
62
TechHelper.Client/Pages/Manage/Class.razor.cs
Normal file
62
TechHelper.Client/Pages/Manage/Class.razor.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
}
|
60
TechHelper.Client/Pages/Manage/Index.razor
Normal file
60
TechHelper.Client/Pages/Manage/Index.razor
Normal 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; }
|
||||
}
|
||||
}
|
30
TechHelper.Client/Pages/Manage/PersonalData.razor
Normal file
30
TechHelper.Client/Pages/Manage/PersonalData.razor
Normal 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()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
2
TechHelper.Client/Pages/Manage/_Imports.razor
Normal file
2
TechHelper.Client/Pages/Manage/_Imports.razor
Normal file
@@ -0,0 +1,2 @@
|
||||
@using TechHelper.Client.Shared
|
||||
@layout ManageLayout
|
57
TechHelper.Client/Pages/Weather.razor
Normal file
57
TechHelper.Client/Pages/Weather.razor
Normal 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);
|
||||
}
|
||||
}
|
2
TechHelper.Client/Pages/_Imports.razor
Normal file
2
TechHelper.Client/Pages/_Imports.razor
Normal file
@@ -0,0 +1,2 @@
|
||||
@using TechHelper.Client.Shared
|
||||
@layout AccountLayout
|
Reference in New Issue
Block a user