前面 (6) 頁面開發之部落格文章列表 介紹了文章列表的開發,頁面中左側是分類列表,右側是該分類下的文章,這個佈局乍看還是不錯的,不過考慮到本專案支援多級分類,但分類列表只會機械式的把所有分類都顯示出來,無法體現分類的層級結構且佔用了很大的頁面縱向空間,因此本文將對分類列表進行改造,使之能夠體現多級分類、節省頁面空間。
關於樹形結構元件,我找了一圈,適配bootstrap(基於jQuery)的元件很難找,大都是很老的,只找到了bootstrap-treeview這個稍微好用一點的,看了下GitHub專案主頁,同樣是好久沒更新了,它適配的甚至是3.x版本的bootstrap,現在都已經2022年了,bootstrap都更新到5.x版本了,然而沒找到更好的,湊合用吧~ (實在不行還能把它程式碼clone下來魔改)
這個元件是比較老的
依賴bower,如果沒有bower的話需要先安裝
npm install -g bower
然後在StarBlog.Web
目錄下執行以下命令安裝依賴
npm install bootstrap-treeview
因為我們的靜態資源都在wwwroot
下,所以npm安裝的前端資源還需要通過gulp工具自動複製到wwwroot
裡,這一點在前面的文章中有介紹過,忘記的同學可以看一下前面這篇:基於.NetCore開發部落格專案 StarBlog - (5) 開始搭建Web專案
編輯gulpfile.js
檔案,在const libs
設定中增加一行
//使用 npm 下載的前端元件包
const libs = [
// ...
{name: "bootstrap-treeview", dist: "./node_modules/bootstrap-treeview/dist/**/*.*"},
];
然後執行gulp任務即可
gulp move
完成之後可以看到wwwroot/lib
下已經多了一個bootstrap-treeview
目錄了
接下來我們就可以在頁面中參照
正式開始前,先來了解一下這個元件的用法
引入依賴
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap-treeview/dist/bootstrap-treeview.min.js"></script>
在網頁裡放一個容器
<div id="categories">
根據官方例子,使用js啟用元件
const instance = $('#categories').treeview({
data: collections,
});
collections
格式如下
const collections = [
{
text: 'Parent 1',
href: '#parent1',
nodes: [
{
text: 'Child 1',
href: '#child1',
nodes: [
{
text: 'Grandchild 1',
href: '#grandchild1',
},
{
text: 'Grandchild 2',
href: '#grandchild2',
}
]
},
{
text: 'Child 2',
href: '#child2',
}
]
},
{
text: 'Parent 2',
href: '#parent2',
},
{
text: 'Parent 3',
href: '#parent3',
},
{
text: 'Parent 4',
href: '#parent4',
},
{
text: 'Parent 5',
href: '#parent5',
}
];
官網的預設效果
不過經過我的測試,官網這個例子在bootstrap5下是有些問題的,預設的圖示都顯示不出來。需要我們自定義一下,加上圖示設定就行,用到的圖示是我們之前的安裝的FontAwesome Icons
const instance = $('#categories').treeview({
data: collections,
collapseIcon: "fa fa-caret-down",
expandIcon: "fa fa-caret-right",
emptyIcon: 'fa fa-circle-o',
});
為了方便使用這個元件,我們需要在後端把分類層級包裝成這個元件需要的形式。
首先定義一個節點類
public class CategoryNode {
public string text { get; set; } = "";
public string href { get; set; } = "";
public List<CategoryNode>? nodes { get; set; }
}
然後在Services/CategoryyService.cs
裡新增一個方法,用來生成分類的樹結構,為了程式碼編寫方便,我直接用遞回來實現。
public List<CategoryNode>? GetNodes(int parentId = 0) {
var categories = _cRepo.Select
.Where(a => a.ParentId == parentId).ToList();
if (categories.Count == 0) return null;
return categories.Select(category => new CategoryNode {
text = category.Name,
nodes = GetNodes(category.Id)
}).ToList();
}
這樣輸出來的資料就是這樣
[
{
"text": "Android開發",
"href": "",
"nodes": null
},
{
"text": "AspNetCore",
"href": "",
"nodes": [
{
"text": "Asp-Net-Core學習筆記",
"href": "",
"nodes": null
},
{
"text": "Asp-Net-Core開發筆記",
"href": "",
"nodes": null
}
]
}
]
哦差點忘了還得給每個節點加上href
引數
寫死是不可能寫死的,ControllerBase
範例預設帶有一個IUrlHelper
型別的Url
屬性,可以用其Link()
方法實現地址路由解析。
不過我們這個方法是寫在Service裡,並沒有ControllerBase
範例,這時只能用依賴注入的方式,不過我在Stack Overflow上看到一個說法是,AspNetCore3.x之後,用LinkGenerator
更好。
上程式碼,先註冊服務
builder.Services.AddHttpContextAccessor();
然後依賴注入
private readonly IHttpContextAccessor _accessor;
private readonly LinkGenerator _generator;
public CategoryService(IHttpContextAccessor accessor, LinkGenerator generator) {
_accessor = accessor;
_generator = generator;
}
修改上面那個GetNodes
方法,在CategoryNode
初始化器里加上
href = _generator.GetUriByAction(
_accessor.HttpContext!,
nameof(BlogController.List),
"Blog",
new {categoryId = category.Id}
)
具體程式碼可以看GitHub:https://github.com/Deali-Axy/StarBlog/blob/master/StarBlog.Web/Services/CategoryService.cs
生成的連結形式是這樣的:
{
"text": "Android開發",
"href": "http://localhost:5038/Blog/List?categoryId=2",
"nodes": null
}
資料準備好了,這時遇到一個問題,資料是要放到js中處理的,那我要用fetch
之類的非同步請求來獲取分類資料再顯示樹形分類嗎?這樣的好處是寫起來比較直觀,然而我們專案的部落格網站是後端渲染,現在部落格列表頁面混入了非同步請求,會導致割裂感,右邊部分的文章列表伺服器端渲染出來在瀏覽器上展示了,左側的分類還要非同步去請求。
斟酌了一下,我決定這個分類也使用後端渲染,雖然有點反直覺,但根據bootstrap-treeview
元件的檔案,它可以使用json方式渲染分類,那我只需要在後端把分類資料序列化成json格式,然後在view中渲染到js程式碼中就行。
開始吧~
編輯StarBlog.Web/ViewModels/BlogListViewModel.cs
檔案,新增倆欄位
public List<CategoryNode> CategoryNodes { get; set; }
// 將上面的分類層級資料轉換成Json字串
public string CategoryNodesJson => JsonSerializer.Serialize(
CategoryNodes,
new JsonSerializerOptions {Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping}
);
然後修改一下Controller,StarBlog.Web/Controllers/BlogController.cs
,先依賴注入CategoryService
然後修改List
方法
public IActionResult List(int categoryId = 0, int page = 1, int pageSize = 5) {
var categories = _categoryRepo.Where(a => a.Visible)
.IncludeMany(a => a.Posts).ToList();
categories.Insert(0, new Category {Id = 0, Name = "All", Posts = _postRepo.Select.ToList()});
return View(new BlogListViewModel {
CurrentCategory = categoryId == 0 ? categories[0] : categories.First(a => a.Id == categoryId),
CurrentCategoryId = categoryId,
Categories = categories,
// 增加這一行
CategoryNodes = _categoryService.GetNodes(),
Posts = _postService.GetPagedList(new PostQueryParameters {
CategoryId = categoryId,
Page = page,
PageSize = pageSize,
OnlyPublished = true
})
});
}
最後一步,修改View,StarBlog.Web/Views/Blog/List.cshtml
,在底部加入js參照和一些js程式碼,treeview元件的設定我已經封裝成initTreeView
方法,可以直接使用。
@section bottom {
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap-treeview/dist/bootstrap-treeview.min.js"></script>
<script src="~/js/blog-list.js"></script>
<script>
const categories = '@Html.Raw(Model.CategoryNodesJson)'
initTreeView(categories);
</script>
}
View的關鍵程式碼就這幾行,完整程式碼可見:https://github.com/Deali-Axy/StarBlog/blob/master/StarBlog.Web/Views/Blog/List.cshtml
完成之後的最終效果如下,算是支援了分類層級了,不過仍然不完美,存在幾個問題:
list-group-item
樣式,存在下劃線不美觀這幾個問題留著後面優化吧~ 暫時先折騰到這裡…
部落格專案的開發已經基本完成,專案程式碼完全開源,有興趣的朋友可以點個star~