基于 abp vNext 和 .NET Core 開發博客專案 - Blazor 實戰系列(八)
2020-09-12 02:37:20 .NET開發
系列文章
- 基于 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 開發博客專案 - 終結篇之發布專案
上一篇完成了標簽模塊和友情鏈接模塊的所有功能,本篇來繼續完成博客最后的模塊,文章的管理,
文章串列 & 洗掉

先將分頁查詢的串列給整出來,這塊和首頁的分頁串列是類似的,就是多了個Id欄位,
先添加兩條路由規則,
@page "/admin/posts"
@page "/admin/posts/{page:int}"
新建回傳資料默認QueryPostForAdminDto.cs,
//QueryPostForAdminDto.cs
using System.Collections.Generic;
namespace Meowv.Blog.BlazorApp.Response.Blog
{
public class QueryPostForAdminDto
{
/// <summary>
/// 年份
/// </summary>
public int Year { get; set; }
/// <summary>
/// Posts
/// </summary>
public IEnumerable<PostBriefForAdminDto> Posts { get; set; }
}
}
//PostBriefForAdminDto.cs
namespace Meowv.Blog.BlazorApp.Response.Blog
{
public class PostBriefForAdminDto : PostBriefDto
{
/// <summary>
/// 主鍵
/// </summary>
public int Id { get; set; }
}
}
然后添加所需的引數:當前頁碼、限制條數、總頁碼、文章串列回傳資料模型,
/// <summary>
/// 當前頁碼
/// </summary>
[Parameter]
public int? page { get; set; }
/// <summary>
/// 限制條數
/// </summary>
private int Limit = 15;
/// <summary>
/// 總頁碼
/// </summary>
private int TotalPage;
/// <summary>
/// 文章串列資料
/// </summary>
private ServiceResult<PagedList<QueryPostForAdminDto>> posts;
然后在初始化函式OnInitializedAsync()中呼叫API獲取文章資料.
/// <summary>
/// 初始化
/// </summary>
protected override async Task OnInitializedAsync()
{
var token = await Common.GetStorageAsync("token");
Http.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");
// 設定默認值
page = page.HasValue ? page : 1;
await RenderPage(page);
}
/// <summary>
/// 點擊頁碼重新渲染資料
/// </summary>
/// <param name="page"></param>
/// <returns></returns>
private async Task RenderPage(int? page)
{
// 獲取資料
posts = await Http.GetFromJsonAsync<ServiceResult<PagedList<QueryPostForAdminDto>>>($"/blog/admin/posts?page={page}&limit={Limit}");
// 計算總頁碼
TotalPage = (int)Math.Ceiling((posts.Result.Total / (double)Limit));
}
在初始化中判斷page引數,如果沒有值給他設定一個默認值1,RenderPage(int? page)方法是呼叫API回傳資料,并計算出總頁碼值,
最后在頁面上進行資料系結,
<AdminLayout>
@if (posts == null)
{
<Loading />
}
else
{
<div class="post-wrap archive">
<NavLink style="float:right" href=https://www.cnblogs.com/meowv/p/"/admin/post">??~~~ 新增文章 ~~~??
@if (posts.Success && posts.Result.Item.Any())
{
@foreach (var item in posts.Result.Item)
{
@item.Year
@foreach (var post in item.Posts)
{
await DeleteAsync(post.Id))">?
await Common.NavigateTo($"/admin/post/{post.Id}"))">??
@post.Title
@post.CreationTime
}
}
}
else
{
}
}
HTML內容放在組件AdminLayout中,當 posts 沒加載完資料的時候顯示加載組件<Loading />,
在頁面上回圈遍歷文章資料和翻頁頁碼,每篇文章標題前面添加兩個按鈕洗掉和編輯,同時單獨加了一個新增文章的按鈕,
洗掉文章呼叫DeleteAsync(int id)方法,需要傳遞引數,當前文章的id,
新增和編輯按鈕都跳轉到"/admin/post"頁面,當編輯的時候將id也傳過去即可,路由規則為:"/admin/post/{id}",
洗掉文章``方法如下:
/// <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/post?id={id}");
var result = await response.Content.ReadFromJsonAsync<ServiceResult>();
if (result.Success)
{
await RenderPage(page);
}
}
}
洗掉之前進行二次確認,避免誤刪,當確認洗掉之后呼叫洗掉文章API,最后重新渲染資料即可,

新增 & 更新文章
完成了后臺文章串列的查詢和洗掉,現在整個博客模塊功能就差新增和更新文章了,勝利就在前方,沖啊,
這塊的開發作業耗費了我太多時間,因為想使用 markdown 來寫文章,找了一圈下來沒有一個合適的組件,所以退而求次只能選擇現有的markdown編輯器來實作了,
我這里選擇了開源的編輯器Editor.md,有需要的可以去 Github 自己下載,https://github.com/pandao/editor.md ,
將下載的資源包解壓放在 wwwroot 檔案夾下,默認是比較大的,而且還有很多示例檔案,我已經將其精簡了一番,可以去我 Github 下載使用,
先來看下最終的成品效果吧,

是不是感徑訓可以,廢話不多說,接下里告訴大家如何實作,
在 Admin 檔案夾下添加post.razor組件,設定路由,并且參考一個樣式檔案,在頁面中參考樣式檔案好像不太符合標準,不過無所謂了,這個后臺就自己用,而且還就這一個頁面用得到,
@page "/admin/post"
@page "/admin/post/{id:int}"
<link href=https://www.cnblogs.com/meowv/p/"./editor.md/css/editormd.css" rel="stylesheet" />
...
把具體HTML內容放在組件AdminLayout中,
因為新增和編輯放在同一個頁面上,所以當id引數不為空的時候需要添加一個id引數,同時默認一進來就讓頁面顯示加載中的組件,當頁面和資料加載完成后在顯示具體的內容,所以在指定一個布爾型別的是否加載引數isLoading,
我們的編輯器主要依賴JavaScript實作的,所以這里不可避免要使用到JavaScript了,
在app.js中添加幾個全域函式,
switchEditorTheme: function () {
editor.setTheme(localStorage.editorTheme || 'default');
editor.setEditorTheme(localStorage.editorTheme === 'dark' ? 'pastel-on-dark' : 'default');
editor.setPreviewTheme(localStorage.editorTheme || 'default');
},
renderEditor: async function () {
await this._loadScript('./editor.md/lib/zepto.min.js').then(function () {
func._loadScript('./editor.md/editormd.js').then(function () {
editor = editormd("editor", {
width: "100%",
height: 700,
path: './editor.md/lib/',
codeFold: true,
saveHTMLToTextarea: true,
emoji: true,
atLink: false,
emailLink: false,
theme: localStorage.editorTheme || 'default',
editorTheme: localStorage.editorTheme === 'dark' ? 'pastel-on-dark' : 'default',
previewTheme: localStorage.editorTheme || 'default',
toolbarIcons: function () {
return ["bold", "del", "italic", "quote", "ucwords", "uppercase", "lowercase", "h1", "h2", "h3", "h4", "h5", "h6", "list-ul", "list-ol", "hr", "link", "image", "code", "preformatted-text", "code-block", "table", "datetime", "html-entities", "emoji", "watch", "preview", "fullscreen", "clear", "||", "save"]
},
toolbarIconsClass: {
save: "fa-check"
},
toolbarHandlers: {
save: function () {
func._shoowBox();
}
},
onl oad: function () {
this.addKeyMap({
"Ctrl-S": function () {
func._shoowBox();
}
});
}
});
});
});
},
_shoowBox: function () {
DotNet.invokeMethodAsync('Meowv.Blog.BlazorApp', 'showbox');
},
_loadScript: async function (url) {
let response = await fetch(url);
var js = await response.text();
eval(js);
}
renderEditor主要實作了動態加載JavaScript代碼,將markdown編輯器渲染出來,這里不多說,都是Editor.md示例里面的代碼,
為了兼容暗黑色主題,這里還加了一個切換編輯器主題的JavaScript方法,switchEditorTheme,
_shoowBox就厲害了,這個方法是呼叫的.NET組件中的方法,前面我們用過了在Blazor中呼叫JavaScript,這里演示了JavaScript中呼叫Blazor中的組件方法,
現在將所需的幾個引數都添加到代碼中,
/// <summary>
/// 定義一個委托方法,用于組件實體方法呼叫
/// </summary>
private static Func<Task> action;
/// <summary>
/// 默認隱藏Box
/// </summary>
private bool Open { get; set; } = false;
/// <summary>
/// 修改時的文章Id
/// </summary>
[Parameter]
public int? Id { get; set; }
/// <summary>
/// 格式化的標簽
/// </summary>
private string tags { get; set; }
/// <summary>
/// 默認顯示加載中
/// </summary>
private bool isLoading = true;
/// <summary>
/// 文章新增或者修改輸入引數
/// </summary>
private PostForAdminDto input;
/// <summary>
/// API回傳的分類串列資料
/// </summary>
private ServiceResult<IEnumerable<QueryCategoryForAdminDto>> categories;
大家看看注釋就知道引數是做什么的了,
現在我們在初始化函式中將所需的資料通過API獲取到,
/// <summary>
/// 初始化
/// </summary>
/// <returns></returns>
protected override async Task OnInitializedAsync()
{
action = ChangeOpenStatus;
var token = await Common.GetStorageAsync("token");
Http.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");
if (Id.HasValue)
{
var post = await Http.GetFromJsonAsync<ServiceResult<PostForAdminDto>>($"/blog/admin/post?id={Id}");
if (post.Success)
{
var _post = post.Result;
input = new PostForAdminDto
{
Title = _post.Title,
Author = _post.Author,
Url = _post.Url,
Html = _post.Html,
Markdown = _post.Markdown,
CategoryId = _post.CategoryId,
Tags = _post.Tags,
CreationTime = _post.CreationTime
};
tags = string.Join(",", input.Tags);
}
}
else
{
input = new PostForAdminDto()
{
Author = "阿星Plus",
CreationTime = DateTime.Now
};
}
categories = await Http.GetFromJsonAsync<ServiceResult<IEnumerable<QueryCategoryForAdminDto>>>("/blog/admin/categories");
// 渲染編輯器
await Common.InvokeAsync("window.func.renderEditor");
// 關閉加載
isLoading = !isLoading;
}
action是一個異步的委托,在初始化中執行了ChangeOpenStatus方法,這個方法等會說,然后獲取localStorage中token的值,
通過引數Id是否有值來判斷當前是新增文章還是更新文章,如果有值就是更新文章,這時候需要根據id去將文章的資料拿到賦值給PostForAdminDto物件展示在頁面上,如果沒有可以添加幾個默認值給PostForAdminDto物件,
因為文章需要分類和標簽的資料,同時這里將分類的資料也查出來,標簽默認是List串列,將其轉換成字串型別,
但完成上面操作后,呼叫JavaScript方法renderEditor渲染渲染編輯器,最后關閉加載,顯示頁面,
現在來看看頁面,
<AdminLayout>
@if (isLoading)
{
<Loading />
}
else
{
<div class="post-box">
<div class="post-box-item">
<input type="text" placeholder="標題" autocomplete="off" @bind="@input.Title" @bind:event="oninput" @onclick="@(() => { Open = false; })" />
<input type="text" placeholder="作者" autocomplete="off" @bind="@input.Author" @bind:event="oninput" @onclick="@(() => { Open = false; })" />
</div>
<div class="post-box-item">
<input type="text" placeholder="URL" autocomplete="off" @bind="@input.Url" @bind:event="oninput" @onclick="@(() => { Open = false; })" />
<input type="text" placeholder="時間" autocomplete="off" @bind="@input.CreationTime" @bind:format="yyyy-MM-dd HH:mm:sss" @bind:event="oninput" @onclick="@(() => { Open = false; })" />
</div>
<div id="editor">
<textarea style="display:none;">@input.Markdown</textarea>
</div>
<Box OnClickCallback="@SubmitAsync" Open="@Open" ButtonText="發布">
<div class="box-item">
<b>分類:</b>
@if (categories.Success && categories.Result.Any())
{
@foreach (var item in categories.Result)
{
<label><input type="radio" name="category" value=https://www.cnblogs.com/meowv/p/"@item.Id" @onchange="@(() => { input.CategoryId = item.Id; })" checked="@(item.Id == input.CategoryId)" />@item.CategoryName
}
}