前面我们介绍了文章详情页面的展示的逻辑代码实现,这一节,我们将继续讲解文章列表的读取和展示、文章根据分类进行筛选、最新文章、热门文章等的调用处理逻辑。
首先,我们先编写文章列表页的前端代码。这里,我们文章采用类似WordPress博客的形式,直接将首页作为文章列表页面的展示。因此我们在template文件夹下创建index.html:
博客首页/文章列表html代码
{% include "partial/header.html" %}
<div class="layui-container index">
<div class="layui-row layui-col-space15">
<div class="layui-col-md8">
<div class="layui-card">
<div class="layui-card-body">
<ul class="article-list">
{% for item in articles %}
<li class="item">
<a href="/article/{{item.Id}}" class="link">
<h5 class="title">{{item.Title}}h5>
<div class="description">{{item.Description}}div>
<div class="meta">
{% if item.Category %}<span>{{item.Category.Title}}span>{% endif %}
<span>{{stampToDate(item.CreatedTime, "2006-01-02")}}span>
<span>{{item.Views}} 阅读span>
div>
a>
li>
{% endfor %}
ul>
div>
{% if prevPage || nextPage %}
<div class="layui-card-body text-center">
<div class="layui-box layui-laypage">
<a href="{{prevPage}}" class="layui-laypage-prev{% if !prevPage %} layui-disabled{% endif %}">上一页a>
<a href="{{nextPage}}" class="layui-laypage-next{% if !nextPage %} layui-disabled{% endif %}">下一页a>
div>
div>
{% endif %}
div>
div>
<div class="layui-col-md4">
{% include "partial/author.html" %}
<div class="layui-card">
<div class="layui-card-header">文章分类div>
<div class="layui-card-body">
<ul class="aside-list">
{% for item in categories %}
<li class="item">
<a href="/?category_id={{item.Id}}" class="link">
<h5 class="title">{{item.Title}}h5>
a>
li>
{% endfor %}
ul>
div>
div>
<div class="layui-card">
<div class="layui-card-header">热门文章div>
<div class="layui-card-body">
<ul class="aside-list">
{% for item in populars %}
<li class="item">
<a href="/article/{{item.Id}}" class="link">
<h5 class="title">{{item.Title}}h5>
<span class="extra">{{item.Views}}阅读span>
a>
li>
{% endfor %}
ul>
div>
div>
div>
div>
div>
{% include "partial/footer.html" %}
列表页中,我们将页面分割成两栏,左边栏大约占2/3,右边栏大约占1/3。左边栏中为文章的列表、上下页信息。文章列表中,我们将展示包括文章标题、文章简介、文章分类、文章发布时间、文章浏览量等信息。右边栏中,用来展示分类列表、热门文章等内容。
左边显示的文章分类信息中,我们注意到显示文章分类使用的是{{item.Category.Title}}
,这是因为我们定义文章模型的时候,article.Category 它指向的是文章分类的模型,article.Category.Title 就能访问到文章分类的名称了。并且文章并不一定会存在分类,因此我们需要先判断分类是否存在{% if item.Category %}{{item.Category.Title}}{% endif %}
,即文章存在分类的时候,我们才输出分类信息。
同样,这里的文章发布时间,我们使用了{{stampToDate(article.CreatedTime, "2006-01-02")}}
来显示。stampToDate是我们前面自定义的模板函数,它可以将时间戳按照给定的格式格式化输出。这里我们将文章发布的时间戳按照"2006-01-02"的格式来输出显示。
这里,我们还注意到,输出上下页信息的时候,先判断是否存在上下页{% if prevPage || nextPage %} ... {% endif %}
,只要上一页存在,或下一页存在,我们才输出上下页的标签,否则这一整块都不显示。当这一块显示的时候,如果没有上一页,则上一页按钮不可点击{% if !prevPage %} layui-disabled{% endif %}
,同样,没有下一页的时候,下一页按钮也不能点击{% if !nextPage %} layui-disabled{% endif %}
。
博客首页/文章列表控制器函数
文章博客首页/文章列表页面控制器我们写在controller/index.go index.go中修改IndexPage()
函数:
func IndexPage(ctx iris.Context) {
currentPage := ctx.URLParamIntDefault("page", 1)
categoryId := uint(ctx.URLParamIntDefault("category_id", 0))
//一页显示10条
pageSize := 10
//文章列表
articles, total, _ := provider.GetArticleList(categoryId, "id desc", currentPage, pageSize)
//读取列表的分类
categories, _ := provider.GetCategories()
for i, v := range articles {
if v.CategoryId > 0 {
for _, c := range categories {
if c.Id == v.CategoryId {
articles[i].Category = c
}
}
}
}
//热门文章
populars, _, _ := provider.GetArticleList(categoryId, "views desc", 1, 10)
totalPage := math.Ceil(float64(total)/float64(pageSize))
prevPage := ""
nextPage := ""
urlPfx := "/?"
var category *model.Category
if categoryId > 0 {
urlPfx += fmt.Sprintf("category_id=%d&", categoryId)
category, _ = provider.GetCategoryById(categoryId)
}
if currentPage > 1 {
prevPage = fmt.Sprintf("%spage=%d", urlPfx, currentPage-1)
}
if currentPage < int(totalPage) {
nextPage = fmt.Sprintf("%spage=%d", urlPfx, currentPage+1)
}
if currentPage == 2 {
prevPage = strings.TrimRight(prevPage, "page=1")
}
ctx.ViewData("total", total)
ctx.ViewData("articles", articles)
ctx.ViewData("populars", populars)
ctx.ViewData("totalPage", totalPage)
ctx.ViewData("prevPage", prevPage)
ctx.ViewData("nextPage", nextPage)
ctx.ViewData("category", category)
ctx.View("index.html")
}
在首页文章列表控制器中,我们需要从url中获取两个参数,一个是当前页面的页码currentPage := ctx.URLParamIntDefault("page", 1)
,另一个是当前页面的分类idcategoryId := uint(ctx.URLParamIntDefault("category_id", 0))
。这里我们都是获取的int类型的数据,并且在没有获取到数据的时候,使用默认值来代替,因此我们使用了URLParamIntDefault
方法。
我们每页显示10条,可以让列表页面差不多维持在2屏到2屏半左右的高度。pageSize := 10
。
接着就是读取根据条件读取文章列表了articles, total, _ := provider.GetArticleList(categoryId, "id desc", currentPage, pageSize)
。我们在 provider/article.go 中,增加GetArticleList函数:
func GetArticleList(categoryId uint, order string, currentPage int, pageSize int) ([]*model.Article, int64, error) {
var articles []*model.Article
offset := (currentPage - 1) * pageSize
var total int64
builder := config.DB.Model(model.Article{})
if categoryId > 0 {
builder = builder.Where("`category_id` = ?", categoryId)
}
if order != "" {
builder = builder.Order(order)
}
if err := builder.Count(&total).Limit(pageSize).Offset(offset).Find(&articles).Error; err != nil {
return nil, 0, err
}
return articles, total, nil
}
获取文章列表函数接收4个参数:
categoryId
是分类id,如果指定分类id,则只显示当前分类的文章列表。order
是排序规则,传入order参数可以根据指定的字段规则进行排序,如id desc
则表示按id倒序来显示。currentPage
是当前读取的页数,这个参数一般由url参数中获取。pageSize
是一页显示数量,这里我们默认显示10条。
这里面我们通过当前页码和每页显示数量来计算出mysql的offsetoffset := (currentPage - 1) * pageSize
。
再通过判断categoryId是否大于零来确定是否传入了分类id,如果有分类id,则添加分类id的条件builder = builder.Where("
category_id = ?", categoryId)
。
如果传入了order排序规则,则添加order条件builder = builder.Order(order)
。
因为这是列表的展示,因此我们还需获取所有符合条件的文章数量,用来计算分页数量和分页展示信息var total int64
。
最后将文章列表、符合条件的文章数量、错误信息返回给控制器。
接着我们继续读取所有的分类,用来将分类赋值给文章列表中的文章:
categories, _ := provider.GetCategories()
for i, v := range articles {
if v.CategoryId > 0 {
for _, c := range categories {
if c.Id == v.CategoryId {
articles[i].Category = c
}
}
}
}
同样地,我们需要获取所有分类,也需要在 provider/category.go 中添加GetCategories
函数:
func GetCategories() ([]*model.Category, error) {
var categories []*model.Category
db := config.DB
err := db.Where("`status` = ?", 1).Find(&categories).Error
if err != nil {
return nil, err
}
return categories, nil
}
我们只读取status = 1
的分类,因为我们开始的时候,定义了status为1 表示正常的数据,status为0表示审核的数据,status为99表示已删除的数据。我们在处理数据的时候,不采取直接删除的方式,这么做是为了防止手误等各种意外操作,造成数据误删而没有恢复的机会。
首页列表中,我们在右边栏中,显示了热门文章。这里我们将浏览量最多的文章认为是热门文章。
populars, _, _ := provider.GetArticleList(categoryId, "views desc", 1, 10)
同样地,热门文章我们也使用GetArticleList
函数来获取数据,我们只需要将排序规则views desc
传入即可得到浏览量最多的文章。这里我们不需要读取分页,也不需要获取符合条件的数量,因此我们使用populars, _, _
来接收数据,只保留文章列表,存入populars
变量中,其他变量忽略,使用下划线_
表示。
接着我们通过计算,算出是否有上一页、下一页,以及根据条件拼接上一页、下一页的连接。
prevPage := ""
nextPage := ""
urlPfx := "/?"
var category *model.Category
if categoryId > 0 {
urlPfx += fmt.Sprintf("category_id=%d&", categoryId)
category, _ = provider.GetCategoryById(categoryId)
}
if currentPage > 1 {
prevPage = fmt.Sprintf("%spage=%d", urlPfx, currentPage-1)
}
if currentPage < int(totalPage) {
nextPage = fmt.Sprintf("%spage=%d", urlPfx, currentPage+1)
}
if currentPage == 2 {
prevPage = strings.TrimRight(prevPage, "page=1")
}
最后,将页面需要使用的变量,都注入到view中,供前端使用,并指定前端页面模板:
ctx.ViewData("total", total)
ctx.ViewData("articles", articles)
ctx.ViewData("populars", populars)
ctx.ViewData("totalPage", totalPage)
ctx.ViewData("prevPage", prevPage)
ctx.ViewData("nextPage", nextPage)
ctx.ViewData("category", category)
ctx.View("index.html")
配置首页文章列表页面路由
首页的路由,在一开始的时候,我们便已经配置过了,因此在这里我们不需要再次配置。它在route/route.go 中,我们给路由增加是否登录判断中间件:
app.Get("/", controller.Inspect, controller.IndexPage)
至此,我们的首页文章列表已经完成。我们的首页列表具有了分页功能,也能根据分类来筛选显示文章了。
验证结果
我们重启一下项目,我们先在浏览器中访问http://127.0.0.1:8001/
来看看效果。如果不出意外可以看到这样的画面:
完整的项目示例代码托管在GitHub上,需要查看完整的项目代码可以到github.com/fesiong/goblog 上查看,也可以直接fork一份来在上面做修改。