基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(六)

58{icon} {views}

系列文章

  1. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用 abp cli 搭建項目
  2. 基於 abp vNext 和 .NET Core 開發博客項目 – 給項目瘦身,讓它跑起來
  3. 基於 abp vNext 和 .NET Core 開發博客項目 – 完善與美化,Swagger登場
  4. 基於 abp vNext 和 .NET Core 開發博客項目 – 數據訪問和代碼優先
  5. 基於 abp vNext 和 .NET Core 開發博客項目 – 自定義倉儲之增刪改查
  6. 基於 abp vNext 和 .NET Core 開發博客項目 – 統一規範API,包裝返回模型
  7. 基於 abp vNext 和 .NET Core 開發博客項目 – 再說Swagger,分組、描述、小綠鎖
  8. 基於 abp vNext 和 .NET Core 開發博客項目 – 接入GitHub,用JWT保護你的API
  9. 基於 abp vNext 和 .NET Core 開發博客項目 – 異常處理和日誌記錄
  10. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用Redis緩存數據
  11. 基於 abp vNext 和 .NET Core 開發博客項目 – 集成Hangfire實現定時任務處理
  12. 基於 abp vNext 和 .NET Core 開發博客項目 – 用AutoMapper搞定對象映射
  13. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(一)
  14. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(二)
  15. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(三)
  16. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(一)
  17. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(二)
  18. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(三)
  19. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(四)
  20. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(五)
  21. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(一)
  22. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(二)
  23. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(三)
  24. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(四)
  25. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(五)
  26. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(六)
  27. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(七)
  28. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(八)
  29. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(九)
  30. 基於 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">-&nbsp;Categories&nbsp;-</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判斷是新增還是更新操作,調用不同的方法PutAsJsonAsyncPostAsJsonAsync去請求API,同樣返回到是HttpResponseMessage對象,最後如果操作成功,重新請求一個數據,刷新分類列表,將彈窗關閉掉。

分類管理頁面的全部代碼如下:

點擊查看代碼

@page "/admin/categories"

<AdminLayout>
    @if (categories == null)
    {
        <Loading />
    }
    else
    {
        <div class="post-wrap categories">
            <h2 class="post-title">-&nbsp;Categories&nbsp;-</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%;
}

好了,分類模塊的功能都完成了,標籤和友情鏈接的管理界面還會遠嗎?這兩個模塊的做法和分類是一樣的,有興趣的可以自己動手完成,今天到這吧,未完待續…

開源地址:https://github.com/Meowv/Blog/tree/blog_tutorial

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※產品缺大量曝光嗎?你需要的是一流包裝設計!