系列文章
- 基於 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 實戰系列(七)
上一篇完成了標籤模組和友情連結模組的所有功能,本篇來繼續完成部落格最後的模組,文章的管理。
文章列表 & 刪除
先將分頁查詢的列表給整出來,這塊和首頁的分頁列表是類似的,就是多了個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="/admin/post"><h3>?~~~ 新增文章 ~~~?</h3></NavLink>
@if (posts.Success && posts.Result.Item.Any())
{
@foreach (var item in posts.Result.Item)
{
<h3>@item.Year</h3>
@foreach (var post in item.Posts)
{
<article class="archive-item">
<NavLink title="❌刪除" @onclick="@(async () => await DeleteAsync(post.Id))">❌</NavLink>
<NavLink title="?編輯" @onclick="@(async () => await Common.NavigateTo($"/admin/post/{post.Id}"))">?</NavLink>
<NavLink target="_blank" class="archive-item-link" href="@("/post" + post.Url)">@post.Title</NavLink>
<span class="archive-item-date">@post.CreationTime</span>
</article>
}
}
<nav class="pagination">
@for (int i = 1; i <= TotalPage; i++)
{
var _page = i;
if (page == _page)
{
<span class="page-number current">@_page</span>
}
else
{
<a class="page-number" @onclick="@(() => RenderPage(_page))" href="/admin/posts/@_page">@_page</a>
}
}
</nav>
}
else
{
<ErrorTip />
}
</div>
}
</AdminLayout>
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="./editor.md/css/editormd.css" rel="stylesheet" />
<AdminLayout>
...
</AdminLayout>
把具體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();
}
},
onload: 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="@item.Id" @onchange="@(() => { input.CategoryId = item.Id; })" checked="@(item.Id == input.CategoryId)" />@item.CategoryName</label>
}
}
</div>
<div class="box-item"></div>
<div class="box-item">
<b>標籤:</b>
<input type="text" @bind="@tags" @bind:event="oninput" />
</div>
</Box>
</div>
}
</AdminLayout>
新增了四個input框,分別用來繫結標題、作者、URL、時間,<div id="editor"></div>
中為編輯器所需。
然後我這裡還是把之前的彈窗元件搞出來了,執行邏輯不介紹了,在彈窗元件中自定義顯示分類和標籤的內容,將獲取到的分類和標籤繫結到具體位置。
每個分類都是一個radio標籤,並且對應一個點選事件,點哪個就把當前分類的Id賦值給PostForAdminDto
物件。
所有的input框都使用@bind
和@bind:event
繫結資料和獲取資料。
Box
彈窗元件這裡自定義了按鈕文字,ButtonText="釋出"
。
/// <summary>
/// 改變Open狀態,通知元件渲染
/// </summary>
private async Task ChangeOpenStatus()
{
Open = true;
var markdown = await Common.InvokeAsync<string>("editor.getMarkdown");
var html = await Common.InvokeAsync<string>("editor.getHTML");
if (string.IsNullOrEmpty(input.Title) || string.IsNullOrEmpty(input.Url) ||
string.IsNullOrEmpty(input.Author) || string.IsNullOrEmpty(markdown) ||
string.IsNullOrEmpty(html))
{
await Alert();
}
input.Html = html;
input.Markdown = markdown;
StateHasChanged();
}
/// <summary>
/// 暴漏給JS執行,彈窗確認框
/// </summary>
[JSInvokable("showbox")]
public static void ShowBox()
{
action.Invoke();
}
/// <summary>
/// alert提示
/// </summary>
/// <returns></returns>
private async Task Alert()
{
Open = false;
await Common.InvokeAsync("alert", "\n??好像漏了點什麼吧??");
return;
}
現在可以來看看ChangeOpenStatus
方法了,這個是改變當前彈窗狀態的一個方法。為什麼需要這個方法呢?
因為在Blazor中JavaScript想要呼叫元件內的方法,方法必須是靜態的,那麼只能通過這種方式去實現了,在靜態方法是不能夠直接改變彈窗的狀態值的。
其實也可以不用這麼麻煩,因為我在編輯器上自定義了一個按鈕,為了好看一些所以只能曲折一點,嫌麻煩的可以直接在頁面上搞個按鈕執行儲存資料邏輯也是一樣的。
使用JSInvokable
Attribute需要在_Imports.razor
中新增名稱空間@using Microsoft.JSInterop
。
ChangeOpenStatus
中獲取到文章內容:HTML和markdown,賦值給PostForAdminDto
物件,要先進行判斷頁面上的幾個引數是否有值,沒值的話給出提示執行Alert()
方法,最後使用StateHasChanged()
通知元件其狀態已更改。
Alert
方法就是呼叫原生的JavaScriptalert
方法,給出一個提示。
ShowBox
就是暴漏給JavaScript的方法,使用DotNet.invokeMethodAsync('Meowv.Blog.BlazorApp', 'showbox');
進行呼叫。
那麼現在一切都正常進行的情況下,點選編輯器上自定義的儲存按鈕,頁面上值不為空的情況下就會彈出我們的彈窗元件Box
。
最後在彈窗元件的回撥方法中執行新增文章還是更新文章。
/// <summary>
/// 確認按鈕點選事件
/// </summary>
/// <returns></returns>
private async Task SubmitAsync()
{
if (string.IsNullOrEmpty(tags) || input.CategoryId == 0)
{
await Alert();
}
input.Tags = tags.Split(",");
var responseMessage = new HttpResponseMessage();
if (Id.HasValue)
responseMessage = await Http.PutAsJsonAsync($"/blog/post?id={Id}", input);
else
responseMessage = await Http.PostAsJsonAsync("/blog/post", input);
var result = await responseMessage.Content.ReadFromJsonAsync<ServiceResult>();
if (result.Success)
{
await Common.NavigateTo("/admin/posts");
}
}
開啟彈窗後執行回撥事件之前還是要判斷值是否為空,為空的情況下還是給出alert
提示,此時將tags標籤還是轉換成List列表,根據Id是否有值去執行新增資料或者更新資料,最終成功後跳轉到文章列表頁。
本片到這裡就結束了,主要攻克了在Blazor中使用Markdown編輯器實現新增和更新文章,這個系列差不多就快結束了,預計還有2篇的樣子,感謝各位的支援。