系列文章
- 基於 abp vNext 和 .NET Core 開發博客項目 - 使用 abp cli 搭建項目
- 基於 abp vNext 和 .NET Core 開發博客項目 - 給項目瘦身,讓它跑起來
- 基於 abp vNext 和 .NET Core 開發博客項目 - 完善與美化,Swagger登場
- 基於 abp vNext 和 .NET Core 開發博客項目 - 數據訪問和代碼優先
- 基於 abp vNext 和 .NET Core 開發博客項目 - 自定義倉儲之增刪改查
- 基於 abp vNext 和 .NET Core 開發博客項目 - 統一規范API,包裝返回模型
- 基於 abp vNext 和 .NET Core 開發博客項目 - 再說Swagger,分組、描述、小綠鎖
- 基於 abp vNext 和 .NET Core 開發博客項目 - 接入GitHub,用JWT保護你的API
- 基於 abp vNext 和 .NET Core 開發博客項目 - 異常處理和日志記錄
- 基於 abp vNext 和 .NET Core 開發博客項目 - 使用Redis緩存數據
- 基於 abp vNext 和 .NET Core 開發博客項目 - 集成Hangfire實現定時任務處理
- 基於 abp vNext 和 .NET Core 開發博客項目 - 用AutoMapper搞定對象映射
- 基於 abp vNext 和 .NET Core 開發博客項目 - 定時任務最佳實戰(一)
- 基於 abp vNext 和 .NET Core 開發博客項目 - 定時任務最佳實戰(二)
- 基於 abp vNext 和 .NET Core 開發博客項目 - 定時任務最佳實戰(三)
- 基於 abp vNext 和 .NET Core 開發博客項目 - 博客接口實戰篇(一)
- 基於 abp vNext 和 .NET Core 開發博客項目 - 博客接口實戰篇(二)
- 基於 abp vNext 和 .NET Core 開發博客項目 - 博客接口實戰篇(三)
- 基於 abp vNext 和 .NET Core 開發博客項目 - 博客接口實戰篇(四)
- 基於 abp vNext 和 .NET Core 開發博客項目 - 博客接口實戰篇(五)
- 基於 abp vNext 和 .NET Core 開發博客項目 - Blazor 實戰系列(一)
- 基於 abp vNext 和 .NET Core 開發博客項目 - Blazor 實戰系列(二)
- 基於 abp vNext 和 .NET Core 開發博客項目 - Blazor 實戰系列(三)
- 基於 abp vNext 和 .NET Core 開發博客項目 - Blazor 實戰系列(四)
- 基於 abp vNext 和 .NET Core 開發博客項目 - Blazor 實戰系列(五)
- 基於 abp vNext 和 .NET Core 開發博客項目 - Blazor 實戰系列(六)
- 基於 abp vNext 和 .NET Core 開發博客項目 - Blazor 實戰系列(七)
- 基於 abp vNext 和 .NET Core 開發博客項目 - Blazor 實戰系列(八)
- 基於 abp vNext 和 .NET Core 開發博客項目 - Blazor 實戰系列(九)
- 基於 abp vNext 和 .NET Core 開發博客項目 - 終結篇之發布項目
上一篇完成了博客文章詳情頁面的數據展示和基於JWT方式的簡單身份驗證,本篇繼續推進,完成后台分類管理的所有增刪改查等功能。
分類管理
在 Admin 文件夾下新建Razor組件,Categories.razor
,設置路由,@page "/admin/categories"
。將具體的展示內容放在組件AdminLayout
中。
@page "/admin/categories"
<AdminLayout>
<Loading />
</AdminLayout>
在這里我會將所有分類展示出來,新增、更新、刪除都會放在一個頁面上去完成。
先將列表查出來,添加API的返回參數,private ServiceResult<IEnumerable<QueryCategoryForAdminDto>> categories;
,然后再初始化中去獲取數據。
//QueryCategoryForAdminDto.cs
namespace Meowv.Blog.BlazorApp.Response.Blog
{
public class QueryCategoryForAdminDto : QueryCategoryDto
{
/// <summary>
/// 主鍵
/// </summary>
public int Id { get; set; }
}
}
/// <summary>
/// API返回的分類列表數據
/// </summary>
private ServiceResult<IEnumerable<QueryCategoryForAdminDto>> categories;
/// <summary>
/// 初始化
/// </summary>
/// <returns></returns>
protected override async Task OnInitializedAsync()
{
var token = await Common.GetStorageAsync("token");
Http.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");
categories = await FetchData();
}
/// <summary>
/// 獲取數據
/// </summary>
/// <returns></returns>
private async Task<ServiceResult<IEnumerable<QueryCategoryForAdminDto>>> FetchData()
{
return await Http.GetFromJsonAsync<ServiceResult<IEnumerable<QueryCategoryForAdminDto>>>("/blog/admin/categories");
}
初始化的時候,需要將我們存在localStorage
中的token讀取出來,因為我們后台的API都需要添加 Authorization
Header 請求頭才能成功返回數據。
在Blazor添加請求頭也是比較方便的,直接Http.DefaultRequestHeaders.Add(...)
即可,要注意的是 token值前面需要加 Bearer
,跟了一個空格不可以省略。
獲取數據單獨提成了一個方法FetchData()
,因為會頻繁用到,現在在頁面上將數據綁定進行展示。
@if (categories == null)
{
<Loading />
}
else
{
<div class="post-wrap categories">
<h2 class="post-title">- Categories -</h2>
@if (categories.Success && categories.Result.Any())
{
<div class="categories-card">
@foreach (var item in categories.Result)
{
<div class="card-item">
<div class="categories">
<NavLink title="❌刪除" @onclick="@(async () => await DeleteAsync(item.Id))">❌</NavLink>
<NavLink title="📝編輯" @onclick="@(() => ShowBox(item))">📝</NavLink>
<NavLink target="_blank" href="@($"/category/{item.DisplayName}")">
<h3>@item.CategoryName</h3>
<small>(@item.Count)</small>
</NavLink>
</div>
</div>
}
<div class="card-item">
<div class="categories">
<NavLink><h3 @onclick="@(() => ShowBox())">📕~~~ 新增分類 ~~~📕</h3></NavLink>
</div>
</div>
</div>
}
else
{
<ErrorTip />
}
</div>
}
同樣的當categories還沒成功獲取到數據的時候,我們直接在展示 <Loading />
組件。然后就是循環列表數據在foreach
中進行綁定數據。
在每條數據最前面,加了刪除和編輯兩個按鈕,刪除的時候調用DeleteAsync
方法,將當前分類的Id傳給他即可。新增和編輯的時候調用ShowBox
方法,他接受一個參數,當前循環到的分類對象item,即QueryCategoryForAdminDto
。
同時這里考慮到復用性,我寫了一個彈窗組件,Box.Razor
,放在Shared文件夾下面,可以先看一下標題為彈窗組件的內容再回來繼續往下看。
刪除分類
接下來看看刪除方法。
/// <summary>
/// 刪除分類
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
private async Task DeleteAsync(int id)
{
// 彈窗確認
bool confirmed = await Common.InvokeAsync<bool>("confirm", "\n💥💢真的要干掉這個該死的分類嗎💢💥");
if (confirmed)
{
var response = await Http.DeleteAsync($"/blog/category?id={id}");
var result = await response.Content.ReadFromJsonAsync<ServiceResult>();
if (result.Success)
{
categories = await FetchData();
}
}
}
刪除之前搞個原生的confirm
進行提示,避免手殘誤刪。因為API那邊使用的是HttpDelete
,所有我們調用API時候要用Http.DeleteAsync
,返回的是HttpResponseMessage
對象,需要我們手動處理接收返回數據,將其轉換為ServiceResult
對象,如果判斷刪除成功后重新調用FetchData()
刷新分類數據。
新增/更新分類
新增和更新數據選擇使用彈窗的方式來進行(彈窗組件在下方),首先是需要一個參數判斷彈窗是否打開,因為是將新增和更新放在一起,所以如何判斷是新增還是更新呢?這里使用Id來進行判斷,當編輯的時候肯定會有Id參數。新增的時候是沒有參數傳遞的。
當我們打開彈窗后里面需要展示兩個input框,用來供輸入要保存的數據,同樣是添加兩個變量。
添加所需的這幾個參數。
/// <summary>
/// 默認隱藏Box
/// </summary>
private bool Open { get; set; } = false;
/// <summary>
/// 新增或者更新時候的分類字段值
/// </summary>
private string categoryName, displayName;
/// <summary>
/// 更新分類的Id值
/// </summary>
private int id;
現在可以將Box組件添加到頁面上。
<div class="post-wrap categories">
...
</div>
<Box OnClickCallback="@SubmitAsync" Open="@Open">
<div class="box-item">
<b>DisplayName:</b><input type="text" @bind="@displayName" @bind:event="oninput" />
</div>
<div class="box-item">
<b>CategoryName:</b><input type="text" @bind="@categoryName" @bind:event="oninput" />
</div>
</Box>
確定按鈕回調事件執行SubmitAsync()
方法,打開狀態參數為上面添加的Open
,按鈕文字ButtonText
為默認值不填。
添加了兩個input,將兩個分類字段分別綁定上去,使用@bind
和@bind:event
。前者等價於設置其value值,后者等價於一個change事件當值改變后會重新賦給綁定的字段參數。
現在可以來看看點擊了新增或者編輯按鈕的方法ShowBox(...)
,接收一個參數QueryCategoryForAdminDto
讓其默認值為null。
/// <summary>
/// 顯示box,綁定字段
/// </summary>
/// <param name="dto"></param>
private void ShowBox(QueryCategoryForAdminDto dto = null)
{
Open = true;
id = 0;
// 新增
if (dto == null)
{
displayName = null;
categoryName = null;
}
else // 更新
{
id = dto.Id;
displayName = dto.DisplayName;
categoryName = dto.CategoryName;
}
}
執行ShowBox()
方法,將彈窗打開,設置Open = true;
和初始化id的值id = 0;
。
通過參數是否null進行判斷是新增還是更新,這樣打開彈窗就搞定了,剩下的就交給彈窗來處理了。
因為新增和更新API需要還對應的輸入參數EditCategoryInput
,去添加它不要忘了。
那么現在就只差按鈕回調事件SubmitAsync()
了,主要是給輸入參數進行賦值調用API,執行新增或者更新即可。
/// <summary>
/// 確認按鈕點擊事件
/// </summary>
/// <returns></returns>
private async Task SubmitAsync()
{
var input = new EditCategoryInput()
{
DisplayName = displayName.Trim(),
CategoryName = categoryName.Trim()
};
if (string.IsNullOrEmpty(input.DisplayName) || string.IsNullOrEmpty(input.CategoryName))
{
return;
}
var responseMessage = new HttpResponseMessage();
if (id > 0)
responseMessage = await Http.PutAsJsonAsync($"/blog/category?id={id}", input);
else
responseMessage = await Http.PostAsJsonAsync("/blog/category", input);
var result = await responseMessage.Content.ReadFromJsonAsync<ServiceResult>();
if (result.Success)
{
categories = await FetchData();
Open = false;
}
}
當參數為空時,直接return
什么都不執行。通過當前Id判斷是新增還是更新操作,調用不同的方法PutAsJsonAsync
和PostAsJsonAsync
去請求API,同樣返回到是HttpResponseMessage
對象,最后如果操作成功,重新請求一個數據,刷新分類列表,將彈窗關閉掉。
分類管理頁面的全部代碼如下:
點擊查看代碼
@page "/admin/categories"
<AdminLayout>
@if (categories == null)
{
<Loading />
}
else
{
<div class="post-wrap categories">
<h2 class="post-title">- Categories -</h2>
@if (categories.Success && categories.Result.Any())
{
<div class="categories-card">
@foreach (var item in categories.Result)
{
<div class="card-item">
<div class="categories">
<NavLink title="❌刪除" @onclick="@(async () => await DeleteAsync(item.Id))">❌</NavLink>
<NavLink title="📝編輯" @onclick="@(() => ShowBox(item))">📝</NavLink>
<NavLink target="_blank" href="@($"/category/{item.DisplayName}")">
<h3>@item.CategoryName</h3>
<small>(@item.Count)</small>
</NavLink>
</div>
</div>
}
<div class="card-item">
<div class="categories">
<NavLink><h3 @onclick="@(() => ShowBox())">📕~~~ 新增分類 ~~~📕</h3></NavLink>
</div>
</div>
</div>
}
else
{
<ErrorTip />
}
</div>
<Box OnClickCallback="@SubmitAsync" Open="@Open">
<div class="box-item">
<b>DisplayName:</b><input type="text" @bind="@displayName" @bind:event="oninput" />
</div>
<div class="box-item">
<b>CategoryName:</b><input type="text" @bind="@categoryName" @bind:event="oninput" />
</div>
</Box>
}
</AdminLayout>
@code {
/// <summary>
/// 默認隱藏Box
/// </summary>
private bool Open { get; set; } = false;
/// <summary>
/// 新增或者更新時候的分類字段值
/// </summary>
private string categoryName, displayName;
/// <summary>
/// 更新分類的Id值
/// </summary>
private int id;
/// <summary>
/// API返回的分類列表數據
/// </summary>
private ServiceResult<IEnumerable<QueryCategoryForAdminDto>> categories;
/// <summary>
/// 初始化
/// </summary>
/// <returns></returns>
protected override async Task OnInitializedAsync()
{
var token = await Common.GetStorageAsync("token");
Http.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");
categories = await FetchData();
}
/// <summary>
/// 獲取數據
/// </summary>
/// <returns></returns>
private async Task<ServiceResult<IEnumerable<QueryCategoryForAdminDto>>> FetchData()
{
return await Http.GetFromJsonAsync<ServiceResult<IEnumerable<QueryCategoryForAdminDto>>>("/blog/admin/categories");
}
/// <summary>
/// 刪除分類
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
private async Task DeleteAsync(int id)
{
Open = false;
// 彈窗確認
bool confirmed = await Common.InvokeAsync<bool>("confirm", "\n💥💢真的要干掉這個該死的分類嗎💢💥");
if (confirmed)
{
var response = await Http.DeleteAsync($"/blog/category?id={id}");
var result = await response.Content.ReadFromJsonAsync<ServiceResult>();
if (result.Success)
{
categories = await FetchData();
}
}
}
/// <summary>
/// 顯示box,綁定字段
/// </summary>
/// <param name="dto"></param>
private void ShowBox(QueryCategoryForAdminDto dto = null)
{
Open = true;
id = 0;
// 新增
if (dto == null)
{
displayName = null;
categoryName = null;
}
else // 更新
{
id = dto.Id;
displayName = dto.DisplayName;
categoryName = dto.CategoryName;
}
}
/// <summary>
/// 確認按鈕點擊事件
/// </summary>
/// <returns></returns>
private async Task SubmitAsync()
{
var input = new EditCategoryInput()
{
DisplayName = displayName.Trim(),
CategoryName = categoryName.Trim()
};
if (string.IsNullOrEmpty(input.DisplayName) || string.IsNullOrEmpty(input.CategoryName))
{
return;
}
var responseMessage = new HttpResponseMessage();
if (id > 0)
responseMessage = await Http.PutAsJsonAsync($"/blog/category?id={id}", input);
else
responseMessage = await Http.PostAsJsonAsync("/blog/category", input);
var result = await responseMessage.Content.ReadFromJsonAsync<ServiceResult>();
if (result.Success)
{
categories = await FetchData();
Open = false;
}
}
}
彈窗組件
考慮到新增和更新數據的時候需要彈窗,這里就簡單演示一下寫一個小組件。
在 Shared 文件夾下新建一個Box.razor
。
在開始之前分析一下彈窗組件所需的元素,彈窗肯定有一個確認和取消按鈕,右上角需要有一個關閉按鈕,關閉按鈕和取消按鈕一個意思。他還需要一個打開或者關閉的狀態,判斷是否打開彈窗,還有就是彈窗內需要自定義展示內容。
確定按鈕的文字可以自定義,所以差不多就需要3個參數,組件內容RenderFragment ChildContent
,是否打開彈窗bool Open
默認隱藏,按鈕文字string ButtonText
默認值給"確定"。然后最重要的是確定按鈕需要一個回調事件,EventCallback<MouseEventArgs> OnClickCallback
用於執行不同的事件。
/// <summary>
/// 組件內容
/// </summary>
[Parameter]
public RenderFragment ChildContent { get; set; }
/// <summary>
/// 是否隱藏
/// </summary>
[Parameter]
public bool Open { get; set; } = true;
/// <summary>
/// 按鈕文字
/// </summary>
[Parameter]
public string ButtonText { get; set; } = "確定";
/// <summary>
/// 確認按鈕點擊事件回調
/// </summary>
[Parameter]
public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
/// <summary>
/// 關閉Box
/// </summary>
private void Close() => Open = false;
右上角關閉和取消按鈕直接在內部進行處理,執行Close()
方法,將參數Open
值設置為false即可。
對應的html如下。
@if (Open)
{
<div class="shadow"></div>
<div class="box">
<div class="close" @onclick="Close">❌</div>
<div class="box-content">
@ChildContent
<div class="box-item box-item-btn">
<button class="box-btn" @onclick="OnClickCallback">@ButtonText</button>
<button class="box-btn btn-primary" @onclick="Close">取消</button>
</div>
</div>
</div>
}
關於樣式
下面是彈窗組件所需的樣式代碼,大家需要的自取,也可以直接去GitHub實時獲取最新的樣式文件。
.box {
width: 600px;
height: 300px;
border-radius: 5px;
background-color: #fff;
position: fixed;
top: 50%;
left: 50%;
margin-top: -150px;
margin-left: -300px;
z-index: 997;
}
.close {
position: absolute;
right: 3px;
top: 2px;
cursor: pointer;
}
.shadow {
width: 100%;
height: 100%;
position: fixed;
left: 0;
top: 0;
z-index: 996;
background-color: #000;
opacity: 0.3;
}
.box-content {
width: 90%;
margin: 20px auto;
}
.box-item {
margin-top: 10px;
height: 30px;
}
.box-item b {
width: 130px;
display: inline-block;
}
.box-item input[type=text] {
padding-left: 5px;
width: 300px;
height: 30px;
}
.box-item label {
width: 100px;
white-space: nowrap;
}
.box-item input[type=radio] {
width: auto;
height: auto;
visibility: initial;
display: initial;
margin-right: 2px;
}
.box-item button {
height: 30px;
width: 100px;
}
.box-item-btn {
position: absolute;
right: 20px;
bottom: 20px;
}
.box-btn {
display: inline-block;
height: 30px;
line-height: 30px;
padding: 0 18px;
background-color: #5A9600;
color: #fff;
white-space: nowrap;
text-align: center;
font-size: 14px;
border: none;
border-radius: 2px;
cursor: pointer;
}
button:focus {
outline: 0;
}
.box-btn:hover {
opacity: .8;
filter: alpha(opacity=80);
color: #fff;
}
.btn-primary {
border: 1px solid #C9C9C9;
background-color: #fff;
color: #555;
}
.btn-primary:hover {
border-color: #5A9600;
color: #333;
}
.post-box {
width: 98%;
margin: 27px auto 0;
}
.post-box-item {
width: 100%;
height: 30px;
margin-bottom: 5px;
}
.post-box-item input {
width: 49.5%;
height: 30px;
padding-left: 5px;
border: 1px solid #ddd;
}
.post-box-item input:nth-child(1) {
float: left;
margin-right: 1px;
}
.post-box-item input:nth-child(2) {
float: right;
margin-left: 1px;
}
.post-box .box-item b {
width: auto;
}
.post-box .box-item input[type=text] {
width: 90%;
}
好了,分類模塊的功能都完成了,標簽和友情鏈接的管理界面還會遠嗎?這兩個模塊的做法和分類是一樣的,有興趣的可以自己動手完成,今天到這吧,未完待續...