上一篇完成了標籤模塊和友情連接模塊的全部功能,本篇來繼續完成博客最後的模塊,文章的管理。javascript
先將分頁查詢的列表給整出來,這塊和首頁的分頁列表是相似的,就是多了個Id字段。css
先添加兩條路由規則。html
@page "/admin/posts" @page "/admin/posts/{page:int}"
新建返回數據默認QueryPostForAdminDto.cs
。java
//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; } } }
而後添加所需的參數:當前頁碼、限制條數、總頁碼、文章列表返回數據模型。git
/// <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獲取文章數據.github
/// <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返回數據,並計算出總頁碼值。緩存
最後在頁面上進行數據綁定。markdown
<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 />
。app
在頁面上循環遍歷文章數據和翻頁頁碼,每篇文章標題前面添加兩個按鈕刪除和編輯,同時單獨加了一個新增文章的按鈕。異步
刪除文章調用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篇的樣子,感謝各位的支持。