系列文章

前言

前面 (6) 页面开发之博客文章列表 介绍了文章列表的开发,页面中左侧是分类列表,右侧是该分类下的文章,这个布局乍看还是不错的,不过考虑到本项目支持多级分类,但分类列表只会机械式的把所有分类都显示出来,无法体现分类的层级结构且占用了很大的页面纵向空间,因此本文将对分类列表进行改造,使之能够体现多级分类、节省页面空间。

关于树形结构组件,我找了一圈,适配bootstrap(基于jQuery)的组件很难找,大都是很老的,只找到了bootstrap-treeview这个稍微好用一点的,看了下GitHub项目主页,同样是好久没更新了,它适配的甚至是3.x版本的bootstrap,现在都已经2022年了,bootstrap都更新到5.x版本了,然而没找到更好的,凑合用吧~ (实在不行还能把它代码clone下来魔改)

安装

这个组件是比较老的

依赖bower,如果没有bower的话需要先安装

  1. npm install -g bower

然后在StarBlog.Web目录下执行以下命令安装依赖

  1. npm install bootstrap-treeview

因为我们的静态资源都在wwwroot下,所以npm安装的前端资源还需要通过gulp工具自动复制到wwwroot里,这一点在前面的文章中有介绍过,忘记的同学可以看一下前面这篇:基于.NetCore开发博客项目 StarBlog - (5) 开始搭建Web项目

编辑gulpfile.js文件,在const libs配置中增加一行

  1. //使用 npm 下载的前端组件包
  2. const libs = [
  3. // ...
  4. {name: "bootstrap-treeview", dist: "./node_modules/bootstrap-treeview/dist/**/*.*"},
  5. ];

然后执行gulp任务即可

  1. gulp move

完成之后可以看到wwwroot/lib下已经多了一个bootstrap-treeview目录了

接下来我们就可以在页面中引用

用法

正式开始前,先来了解一下这个组件的用法

引入依赖

  1. <script src="~/lib/jquery/dist/jquery.min.js"></script>
  2. <script src="~/lib/bootstrap-treeview/dist/bootstrap-treeview.min.js"></script>

在网页里放一个容器

  1. <div id="categories">

根据官方例子,使用js激活组件

  1. const instance = $('#categories').treeview({
  2. data: collections,
  3. });

collections格式如下

  1. const collections = [
  2. {
  3. text: 'Parent 1',
  4. href: '#parent1',
  5. nodes: [
  6. {
  7. text: 'Child 1',
  8. href: '#child1',
  9. nodes: [
  10. {
  11. text: 'Grandchild 1',
  12. href: '#grandchild1',
  13. },
  14. {
  15. text: 'Grandchild 2',
  16. href: '#grandchild2',
  17. }
  18. ]
  19. },
  20. {
  21. text: 'Child 2',
  22. href: '#child2',
  23. }
  24. ]
  25. },
  26. {
  27. text: 'Parent 2',
  28. href: '#parent2',
  29. },
  30. {
  31. text: 'Parent 3',
  32. href: '#parent3',
  33. },
  34. {
  35. text: 'Parent 4',
  36. href: '#parent4',
  37. },
  38. {
  39. text: 'Parent 5',
  40. href: '#parent5',
  41. }
  42. ];

官网的默认效果

不过经过我的测试,官网这个例子在bootstrap5下是有些问题的,默认的图标都显示不出来。需要我们自定义一下,加上图标配置就行,用到的图标是我们之前的安装的FontAwesome Icons

  1. const instance = $('#categories').treeview({
  2. data: collections,
  3. collapseIcon: "fa fa-caret-down",
  4. expandIcon: "fa fa-caret-right",
  5. emptyIcon: 'fa fa-circle-o',
  6. });

处理分类数据

为了方便使用这个组件,我们需要在后端把分类层级包装成这个组件需要的形式。

首先定义一个节点类

  1. public class CategoryNode {
  2. public string text { get; set; } = "";
  3. public string href { get; set; } = "";
  4. public List<CategoryNode>? nodes { get; set; }
  5. }

然后在Services/CategoryyService.cs里新增一个方法,用来生成分类的树结构,为了代码编写方便,我直接用递归来实现。

  1. public List<CategoryNode>? GetNodes(int parentId = 0) {
  2. var categories = _cRepo.Select
  3. .Where(a => a.ParentId == parentId).ToList();
  4. if (categories.Count == 0) return null;
  5. return categories.Select(category => new CategoryNode {
  6. text = category.Name,
  7. nodes = GetNodes(category.Id)
  8. }).ToList();
  9. }

这样输出来的数据就是这样

  1. [
  2. {
  3. "text": "Android开发",
  4. "href": "",
  5. "nodes": null
  6. },
  7. {
  8. "text": "AspNetCore",
  9. "href": "",
  10. "nodes": [
  11. {
  12. "text": "Asp-Net-Core学习笔记",
  13. "href": "",
  14. "nodes": null
  15. },
  16. {
  17. "text": "Asp-Net-Core开发笔记",
  18. "href": "",
  19. "nodes": null
  20. }
  21. ]
  22. }
  23. ]

哦差点忘了还得给每个节点加上href参数

写死是不可能写死的,ControllerBase实例默认带有一个IUrlHelper类型的Url属性,可以用其Link()方法实现地址路由解析。

不过我们这个方法是写在Service里,并没有ControllerBase实例,这时只能用依赖注入的方式,不过我在Stack Overflow上看到一个说法是,AspNetCore3.x之后,用LinkGenerator更好。

上代码,先注册服务

  1. builder.Services.AddHttpContextAccessor();

然后依赖注入

  1. private readonly IHttpContextAccessor _accessor;
  2. private readonly LinkGenerator _generator;
  3. public CategoryService(IHttpContextAccessor accessor, LinkGenerator generator) {
  4. _accessor = accessor;
  5. _generator = generator;
  6. }

修改上面那个GetNodes方法,在CategoryNode初始化器里加上

  1. href = _generator.GetUriByAction(
  2. _accessor.HttpContext!,
  3. nameof(BlogController.List),
  4. "Blog",
  5. new {categoryId = category.Id}
  6. )

具体代码可以看GitHub:https://github.com/Deali-Axy/StarBlog/blob/master/StarBlog.Web/Services/CategoryService.cs

生成的链接形式是这样的:

  1. {
  2. "text": "Android开发",
  3. "href": "http://localhost:5038/Blog/List?categoryId=2",
  4. "nodes": null
  5. }

前端渲染

数据准备好了,这时遇到一个问题,数据是要放到js中处理的,那我要用fetch之类的异步请求来获取分类数据再显示树形分类吗?这样的好处是写起来比较直观,然而我们项目的博客网站是后端渲染,现在博客列表页面混入了异步请求,会导致割裂感,右边部分的文章列表服务端渲染出来在浏览器上展示了,左侧的分类还要异步去请求。

斟酌了一下,我决定这个分类也使用后端渲染,虽然有点反直觉,但根据bootstrap-treeview组件的文档,它可以使用json方式渲染分类,那我只需要在后端把分类数据序列化成json格式,然后在view中渲染到js代码中就行。

开始吧~

编辑StarBlog.Web/ViewModels/BlogListViewModel.cs文件,添加俩字段

  1. public List<CategoryNode> CategoryNodes { get; set; }
  2. // 将上面的分类层级数据转换成Json字符串
  3. public string CategoryNodesJson => JsonSerializer.Serialize(
  4. CategoryNodes,
  5. new JsonSerializerOptions {Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping}
  6. );

然后修改一下Controller,StarBlog.Web/Controllers/BlogController.cs,先依赖注入CategoryService

然后修改List方法

  1. public IActionResult List(int categoryId = 0, int page = 1, int pageSize = 5) {
  2. var categories = _categoryRepo.Where(a => a.Visible)
  3. .IncludeMany(a => a.Posts).ToList();
  4. categories.Insert(0, new Category {Id = 0, Name = "All", Posts = _postRepo.Select.ToList()});
  5. return View(new BlogListViewModel {
  6. CurrentCategory = categoryId == 0 ? categories[0] : categories.First(a => a.Id == categoryId),
  7. CurrentCategoryId = categoryId,
  8. Categories = categories,
  9. // 增加这一行
  10. CategoryNodes = _categoryService.GetNodes(),
  11. Posts = _postService.GetPagedList(new PostQueryParameters {
  12. CategoryId = categoryId,
  13. Page = page,
  14. PageSize = pageSize,
  15. OnlyPublished = true
  16. })
  17. });
  18. }

最后一步,修改View,StarBlog.Web/Views/Blog/List.cshtml,在底部加入js引用和一些js代码,treeview组件的配置我已经封装成initTreeView方法,可以直接使用。

  1. @section bottom {
  2. <script src="~/lib/jquery/dist/jquery.min.js"></script>
  3. <script src="~/lib/bootstrap-treeview/dist/bootstrap-treeview.min.js"></script>
  4. <script src="~/js/blog-list.js"></script>
  5. <script>
  6. const categories = '@Html.Raw(Model.CategoryNodesJson)'
  7. initTreeView(categories);
  8. </script>
  9. }

View的关键代码就这几行,完整代码可见:https://github.com/Deali-Axy/StarBlog/blob/master/StarBlog.Web/Views/Blog/List.cshtml

最终效果

完成之后的最终效果如下,算是支持了分类层级了,不过仍然不完美,存在几个问题:

  • 不能高亮显示当前所选分类
  • 没有实现分类文章数量显示(原来的版本是有的)
  • 无法自定义list-group-item样式,存在下划线不美观
  • ...

这几个问题留着后面优化吧~ 暂时先折腾到这里…

博客项目的开发已经基本完成,项目代码完全开源,有兴趣的朋友可以点个star~

参考资料

基于.NetCore开发博客项目 StarBlog - (8) 分类层级结构展示的更多相关文章

  1. 基于.NetCore开发博客项目 StarBlog - (9) 图片批量导入

    系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...

  2. 基于.NetCore开发博客项目 StarBlog - (10) 图片瀑布流

    系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...

  3. 基于.NetCore开发博客项目 StarBlog - (11) 实现访问统计

    系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...

  4. 基于.NetCore开发博客项目 StarBlog - (12) Razor页面动态编译

    系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...

  5. 基于.NetCore开发博客项目 StarBlog - (13) 加入友情链接功能

    系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...

  6. 基于.NetCore开发博客项目 StarBlog - (14) 实现主题切换功能

    系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...

  7. 基于.NetCore开发博客项目 StarBlog - (15) 生成随机尺寸图片

    系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...

  8. 基于.NetCore开发博客项目 StarBlog - (16) 一些新功能 (监控/统计/配置/初始化)

    系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...

  9. 基于.NetCore开发博客项目 StarBlog - (17) 自动下载文章里的外部图片

    系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...

随机推荐

  1. 来扯点ionic3[3] 页面的生命周期事件,也就是凡间所说的钩子

    首先要做一个诚挚的道歉,作为大四狗,因为升学的事情,断更两个月,所以要感谢各位仁慈的读者没有脱粉(好像也就50个粉丝).这一节,我们延续上一节制作的页面,来讨论声明周期钩子的事情. 以我的经验来看,多 ...

  2. 基于Vue实现关键词实时搜索高亮显示关键词

    最近在做移动real-time-search于实时搜索和关键词高亮显示的功能,通过博客的方式总结一下,同时希望能够帮助到别人~~~ 如果不喜欢看文字的朋友我写了一个demo方便已经上传到了github ...

  3. JavaScript 中 empty、remove 和 detach的区别

    内容 empty.remove 和 detach的区别 jQuery 操作 DOM 之删除节点 方法名 元素所绑定的事件及数据是否也被移除 作用 $(selector).empty() 是 从被选元素 ...

  4. No origin bean specified和 No destination bean specified

    Beanutils.copyProperties 异常一: No origin bean specifiedBeanutils.copyProperties 异常二: No destination b ...

  5. in a frame because it set 'X-Frame-Options' to 'sameorigin'

    不是所有网站都给  iframe嵌套的,  有的网站设置了  禁止嵌套!!! 浏览器会依据X-Frame-Options的值来控制iframe框架的页面是否允许加载显示出来, 看你们公司这么设置了!! ...

  6. [ SOS ] 版本控制工具 笔记

    https://www.cnblogs.com/yeungchie/ soscmd 创建工作区 soscmd newworkarea $serverName $projectName [$path] ...

  7. SpringMVC的数据响应-回写数据

    1.直接返回字符串 其他具体代码请访问chilianjie @RequestMapping("/report5") public String save5(HttpServletR ...

  8. 时间篇之centos6下修复的ntp操作(ntpd和ntpdate两个服务区别)

    系统采样,本采样和命令都是在centos6.4的系统中进行 主要比较centos7和centos6之间的差异,因为大部分都开始采用centos7但是有些老系统还采用centos6,这样我们就需要熟悉c ...

  9. 帝国CMS怎样删除清空数据库记录?

    我用的帝国CMS,删除已经发表的文章和栏目后,后面新发的栏目和文章ID并不会重新从1开始,而是接着已经删除的文章和栏目ID,那么,怎样让后面发的文章和栏目ID重新从1开始呢? 首先,做任何重要修改前先 ...

  10. drf过滤和排序及异常处理的包装

    过滤和排序(4星) 查询所有才需要过滤(根据过滤条件),排序(按某个规律排序) 使用前提: 必须继承的顶层类是GenericAPIView 内置过滤类 内置过滤类使用,在视图类中配置,是模糊查询 使用 ...