上一篇完成了博客文章詳情頁面的數據展現和基於JWT方式的簡單身份驗證,本篇繼續推動,完成後臺分類管理的全部增刪改查等功能。css
在 Admin 文件夾下新建Razor組件,Categories.razor
,設置路由,@page "/admin/categories"
。將具體的展現內容放在組件AdminLayout
中。html
@page "/admin/categories" <AdminLayout> <Loading /> </AdminLayout>
在這裏我會將全部分類展現出來,新增、更新、刪除都會放在一個頁面上去完成。git
先將列表查出來,添加API的返回參數,private ServiceResult<IEnumerable<QueryCategoryForAdminDto>> categories;
,而後再初始化中去獲取數據。github
//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
,跟了一個空格不能夠省略。app
獲取數據單獨提成了一個方法FetchData()
,由於會頻繁用到,如今在頁面上將數據綁定進行展現。async
@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
中進行綁定數據。ide
在每條數據最前面,加了刪除和編輯兩個按鈕,刪除的時候調用DeleteAsync
方法,將當前分類的Id傳給他便可。新增和編輯的時候調用ShowBox
方法,他接受一個參數,當前循環到的分類對象item,即QueryCategoryForAdminDto
。post
同時這裏考慮到複用性,我寫了一個彈窗組件,Box.Razor
,放在Shared文件夾下面,能夠先看一下標題爲彈窗組件的內容再回來繼續往下看。spa
接下來看看刪除方法。
/// <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%; }
好了,分類模塊的功能都完成了,標籤和友情連接的管理界面還會遠嗎?這兩個模塊的作法和分類是同樣的,有興趣的能夠本身動手完成,今天到這吧,未完待續...