Mobile wallpaper 1Mobile wallpaper 2Mobile wallpaper 3Mobile wallpaper 4Mobile wallpaper 5Mobile wallpaper 6
3883 字
19 分钟
6-文章篇-分页查询-Springboot Vue 教程

现在开始正式在前端展示各个用户的文章。

分页查询#

什么是“分页查询”?#

想象一下你正在看一本非常厚非常厚的书,比如一本字典。

  • 没有分页查询: 就像你一次性要把整本字典从头到尾都读完,或者一次性把整本字典都搬到桌子上。这显然非常慢,非常累,而且你的桌子(电脑内存)也可能放不下。
  • 有了分页查询: 就像你只看字典的“第 1 页到第 10 页”,或者“第 101 页到第 110 页”。你每次只看一小部分内容,看完这部分,觉得不够再翻到下一页。

在计算机和网络的世界里,这个“书”就是数据库里的大量数据(比如几万、几十万、几百万篇文章),而“看书”就是你的程序去数据库里拿这些数据。

它的核心思想就是:#

  1. 按需加载: 我不需要一次性把所有数据都拿出来,我只想要当前用户在屏幕上能看到的那一小部分数据。
  2. 分批展示: 我把所有数据分成一页一页的,用户想看哪一页,我就去拿哪一页的数据。

为什么需要分页查询?#

想象一下你的网站上有 100 万篇文章:

  1. 慢! 如果你一次性把这 100 万篇文章全部从数据库拿出来,再全部传到你的网页上,再全部显示出来,你的网站会慢到爆炸,用户等得花都谢了。
  2. 卡! 就算传到了网页,你的电脑内存也可能瞬间爆满,网页直接卡死或崩溃。
  3. 浪费! 用户通常只关心当前屏幕上显示的那几篇文章,后面几十万篇他可能根本没看,白白传输了那么多数据,浪费了网络流量和服务器资源。

有了分页查询,这些问题就迎刃而解了:

  • 用户访问网站,网站只去数据库拿**“第 1 页”**的 10 篇文章。
  • 用户点击“下一页”,网站再去数据库拿**“第 2 页”**的 10 篇文章。
  • 这样既快又省,用户体验也很好。

“分页查询”的实现原理#

当你的网页(前端)告诉服务器(后端)说:“我想要第 3 页的文章,每页显示 10 篇”,那么后端会:

  1. 计算跳过多少条: 要拿第 3 页,每页 10 条,那前面第 1 页和第 2 页加起来就是 20 条,所以要跳过 20 条数据。
    • 公式:(页码 - 1) * 每页大小
  2. 从数据库查询: 向数据库发送一个特殊的命令,告诉它:“跳过前面 20 条数据,然后给我取接下来的 10 条数据”。
    • 这个命令在 SQL 语言中通常是 LIMIT 10 OFFSET 20 (或者 LIMIT 20, 10)。
  3. 计算总数: 同时,后端还会问数据库:“满足我条件的文章总共有多少篇?” 这样前端就知道总共有多少页可以翻。
    • 这个命令通常是 SELECT COUNT(*) FROM your_table WHERE ...
  4. 返回数据: 把当前页的 10 篇文章和总文章数一起返回给前端。

安装依赖#

**MyBatis Plus3.5.9及以上版本的分页功能独立了出来,所以我们需要额外安装一个依赖mybatis-plus-jsqlparser** 如果你的版本是3.5.9以下,则无需安装。

我们来到Maven依赖网站

https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-jsqlparser

选择3.5.12版本,请根据自己的**mybatis-plus** 版本自行选择对应的版本安装,我用的是3.5.12版本。

<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
<version>3.5.12</version>
</dependency>

将依赖写入pom.xml后记得更新一下依赖,前文说过便不再赘述。

在config包下创建一个MybatisPlusConfig,代码如下:

package com.example.demo.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// **核心:添加分页拦截器,指定数据库类型**
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 假设你使用的是 MySQL
return interceptor;
}
}

前端部分#

一个主页通常包含顶部导航,主要内容区,页脚。 大部分网站都是这样的,你可以打开各类网站观察一下他们的布局,在此我不赘述。 一个顶部导航可能会在很多地方都能用到,所以它最好是个组件,以便我们复用。 在src下的components下创建一个TheHeader.vue

<template>
<el-menu default-active="" class="el-menu-demo" mode="horizontal" :ellipsis="false" @select=""
style="align-items: center;">
<h1 class="menu-item">博客系统</h1>
<el-menu-item class="menu-item" index="0" key="">首页</el-menu-item>
<el-menu-item class="menu-item" index="1" key="">创建文章</el-menu-item>
<el-menu-item class="menu-item" index="2" key="">管理文章</el-menu-item>
<el-link v-if="!isLogin" class="menu-item" href="login" type="primary" underline="always">请登录</el-link>
<div v-else class="menu-item" style="display: flex;">
<p class="menu-item">{{ username }},欢迎您</p>
<el-button type="primary" class="menu-item" round @click="logout">注销</el-button>
</div>
</el-menu>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import type { User } from '@/interface/User';
const username = ref('')
const isLogin = ref(false)
const logout = () => {
localStorage.removeItem('curUser')
localStorage.removeItem('token')
updataInfo()
}
const updataInfo = () => {
const curUser: User | null = JSON.parse(localStorage.getItem('curUser') || 'null');
if (curUser && curUser.username) {
username.value = curUser.username;
isLogin.value = true;
} else {
username.value = "";
isLogin.value = false;
}
}
onMounted(() => {
updataInfo()
})
</script>
<style scoped>
.menu-item {
margin: 0 6px;
align-items: center;
}
.el-menu--horizontal>.el-menu-item:nth-child(4) {
margin-right: auto;
}
</style>

在视图下创建一个Home.vue

并导入TheHeader组件

<template>
<TheHeader />
<div>
</div>
</template>
<script setup lang="ts">
import TheHeader from '@/components/TheHeader.vue'
</script>
<style scoped>
</style>

按照惯例在router里加上Home,这样我们就能正确路由到Home了

{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue')
}

数据库部分#

我们需要一个article表,来存放文章内容 点击左上角的创建sql,写入sql代码,最后点击上方的闪电按钮执行。 这里的user_id和用户表的id关联了起来,当用户表的用户被删除时,与之关联的文章也会被一并删除。

use blog;
CREATE TABLE `article` (
`id` int NOT NULL AUTO_INCREMENT,
`title` varchar(100) NOT NULL COMMENT '文章标题',
`content` text NOT NULL COMMENT '文章内容',
`user_id` int NOT NULL COMMENT '作者ID,关联user表',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:1-发布,0-草稿',
`view_count` int NOT NULL DEFAULT '0' COMMENT '浏览次数',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
CONSTRAINT `fk_article_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='文章表';

我们再造一些模拟的假数据,你可以将上面的SQL代码复制下来,去问ai,让ai给你造一点数据

这里AI给我生成了20条假数据,文章作者分别对应user_id 1 到 5,请注意你有没有5个用户在用户表里,否则会出错

image.png

use blog;
INSERT INTO article (title, content, user_id, create_time, update_time, status, view_count) VALUES
('Spring Boot入门指南', 'Spring Boot是一个用于简化Spring应用初始搭建和开发过程的框架...', 1, '2023-01-15 09:30:00', '2023-01-15 09:30:00', 1, 245),
('MySQL优化技巧', '本文将介绍MySQL数据库性能优化的10个实用技巧...', 2, '2023-02-20 14:15:00', '2023-02-22 11:20:00', 1, 189),
('RESTful API设计原则', '良好的API设计是构建可维护系统的关键...', 3, '2023-03-05 10:00:00', '2023-03-05 10:00:00', 1, 312),
('Vue3新特性解析', 'Vue3带来了许多令人兴奋的新特性...', 1, '2023-03-18 16:45:00', '2023-03-20 08:30:00', 1, 178),
('微服务架构实践', '微服务架构已成为现代应用开发的主流选择...', 4, '2023-04-10 13:20:00', '2023-04-12 09:15:00', 1, 267),
('Docker容器化入门', 'Docker改变了我们部署和运行应用的方式...', 2, '2023-04-25 11:10:00', '2023-04-25 11:10:00', 1, 154),
('未完成的草稿', '这是一篇尚未完成的文章...', 5, '2023-05-08 15:30:00', '2023-05-08 15:30:00', 0, 12),
('JavaScript ES6新特性', 'ES6为JavaScript带来了许多强大的新特性...', 3, '2023-05-20 10:45:00', '2023-05-22 14:20:00', 1, 198),
('Redis缓存策略', '合理使用Redis可以显著提升系统性能...', 4, '2023-06-05 09:15:00', '2023-06-07 16:30:00', 1, 223),
('Git高级用法', '掌握这些Git高级技巧可以提升你的开发效率...', 5, '2023-06-18 14:00:00', '2023-06-18 14:00:00', 1, 176),
('Kubernetes基础教程', 'Kubernetes是容器编排的事实标准...', 1, '2023-07-10 11:30:00', '2023-07-12 10:45:00', 1, 289),
('设计模式实践', '设计模式是解决常见软件设计问题的可复用方案...', 2, '2023-07-25 16:20:00', '2023-07-25 16:20:00', 1, 134),
('TypeScript类型系统', 'TypeScript的类型系统提供了强大的开发体验...', 3, '2023-08-05 10:10:00', '2023-08-07 09:30:00', 1, 167),
('网络安全基础', '了解基本的网络安全知识对每个开发者都很重要...', 4, '2023-08-20 14:45:00', '2023-08-20 14:45:00', 1, 211),
('性能优化方法论', '系统性能优化需要科学的方法和工具...', 5, '2023-09-05 09:30:00', '2023-09-07 11:20:00', 1, 189),
('React Hooks详解', 'React Hooks彻底改变了我们编写React组件的方式...', 1, '2023-09-18 13:15:00', '2023-09-20 15:45:00', 1, 256),
('数据库事务隔离级别', '理解事务隔离级别对开发可靠应用至关重要...', 2, '2023-10-10 10:00:00', '2023-10-10 10:00:00', 1, 178),
('CI/CD实践指南', '持续集成和持续部署是现代DevOps的核心...', 3, '2023-10-25 15:30:00', '2023-10-27 09:15:00', 1, 234),
('Python异步编程', 'Python的异步编程模型可以显著提高IO密集型应用性能...', 4, '2023-11-05 11:20:00', '2023-11-07 14:30:00', 1, 167),
('数据结构与算法', '扎实的数据结构与算法基础是优秀程序员的必备技能...', 5, '2023-11-20 16:10:00', '2023-11-20 16:10:00', 1, 198);

后端部分#

Article 实体类#

创建一个Article实体类

package com.example.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class Article {
@TableId(type = IdType.AUTO)
private Integer id;
private String title;
private String content;
private Integer userId;
private LocalDateTime createTime; // 创建时间
private LocalDateTime updateTime; // 更新时间
private Integer status; // 状态:1-发布,0-草稿
private Integer viewCount; //
}

ArticleMapper 接口#

创建ArticleMapper接口

package com.example.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.entity.Article;
public interface ArticleMapper extends BaseMapper<Article> {
}

服务层#

ArticleService接口#

在service包下创建一个ArticleService接口,代码如下:

package com.example.demo.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.core.metadata.IPage; // 引入分页接口
import com.example.demo.entity.Article;
import com.example.demo.dto.ArticleDto; // 假设你有用于接收前端文章数据的DTO
import com.example.demo.dto.ArticleQueryParams; // 假设你有用于查询参数的DTO
import java.util.List;
/**
* 文章业务接口
*/
public interface ArticleService extends IService<Article> {
/**
* @param queryParams 包含分页信息和查询条件的DTO。
* @return 包含文章列表的分页结果。
*/
IPage<Article> getArticlesByPage(ArticleQueryParams queryParams);
}

目前我们只有一个需求,实现分页查询,那就先设计一个分页查询接口。

ArticleServiceImpl类#

接下来实现具体方法

在Impl包下创建ArticleServiceImpl(注意Impl开头是大写i,末尾是小写l),代码如下:

package com.example.demo.service.Impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.demo.entity.Article;
import com.example.demo.mapper.ArticleMapper;
import com.example.demo.service.ArticleService;
import com.example.demo.dto.ArticleQueryParams; // 导入你的 DTO
import org.springframework.stereotype.Service;
@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {
@Override
public IPage<Article> getArticlesByPage(ArticleQueryParams queryParams) {
// 1. 参数校验与默认值设置
final ArticleQueryParams finalQueryParams;
if (queryParams == null) {
finalQueryParams = new ArticleQueryParams();
} else {
finalQueryParams = queryParams;
}
// 2. 创建 Mybatis-Plus 的 Page 对象
Page<Article> page = new Page<>(finalQueryParams.getCurrent(), finalQueryParams.getSize());
// 3. 构建查询条件 (QueryWrapper)
QueryWrapper<Article> queryWrapper = new QueryWrapper<>();
// **核心简化:只查询已发布的文章 (status = 1)**
queryWrapper.eq("status", 1);
// 4. 设置排序规则:按创建时间倒序(最新的文章在前)
queryWrapper.orderByDesc("create_time");
// 5. 执行分页查询
return page(page, queryWrapper);
}
}

ArticleController类#

package com.example.demo.controller;
import cn.dev33.satoken.util.SaResult;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.example.demo.entity.Article;
import com.example.demo.service.ArticleService;
import com.example.demo.dto.ArticleQueryParams;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 文章相关的API接口
*/
@RestController
@RequestMapping("/article")
public class ArticleController {
@Autowired
private ArticleService articleService;
/**
* 获取所有已发布的公共文章列表 (分页)。
* 所有人都可以访问此接口,无需登录校验。
*
* @param queryParams 查询参数,只包含分页信息(页码和每页大小)。
* @return 统一响应体,包含文章列表的分页数据。
*/
@GetMapping("/list")
public SaResult getArticleList(ArticleQueryParams queryParams) {
IPage<Article> pageResult = articleService.getArticlesByPage(queryParams);
return SaResult.ok("获取文章列表成功").setData(pageResult);
}
}

与之对应的前端#

打开你的VSCode

Article接口#

在interface文件夹下创建一个Article.ts 接口,

export interface Article {
id: number
title: string
content: string
userId: number
author: string // 可选字段,因为可能是后查询的
createTime: string | Date
updateTime?: string | Date // 可选
status?: 0 | 1 // 0: 草稿, 1: 已发布
viewCount?: number
}
export interface IPage<T> {
records: T[];
total: number;
size: number;
current: number;
pages: number;
}
export interface ArticleQueryParams {
current?: number; // 当前页码
size?: number; // 每页大小
// **移除:不再包含 keyword, userId, status**
}

与后端的对应。

对接Api#

来到前端,增加get方法getArticleList

/**
* 获取文章列表(分页),只返回所有已发布的公共文章。
* @param params 查询参数 (页码, 每页大小)
* @returns Promise<IPage<Article>>
*/
export function getArticleList(params: ArticleQueryParams): Promise<IPage<Article>> {
return api.get<Result<IPage<Article>>, IPage<Article>>('/article/list', {
params
});
}

别忘记export方法

文章列表卡片#

<template>
<div class="article-container">
<h1>文章列表</h1>
<el-pagination
v-model:current-page="queryParams.current" v-model:page-size="queryParams.size" :page-sizes="[5, 10, 20]" :small="false"
:background="true"
layout="total, sizes, prev, pager, next, jumper"
:total="totalArticles" @size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
<div v-for="art in articles" :key="art.id">
<el-card class="art-card" @click="goToArticleDetail(art.id)"> <template #header>
<div class="card-header">
<span>{{ art.title }}</span>
</div>
</template>
<p class="text item">{{ truncateContent(art.content) }}</p>
<template #footer>{{ formatTime(art.createTime) }}</template>
</el-card>
<el-divider />
</div>
<el-empty v-if="articles.length === 0 && !loading" description="暂无文章"></el-empty>
<el-skeleton :rows="5" animated v-if="loading" />
<el-pagination
v-model:current-page="queryParams.current"
v-model:page-size="queryParams.size"
:page-sizes="[5, 10, 20]"
:small="false"
:background="true"
layout="total, sizes, prev, pager, next, jumper"
:total="totalArticles"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue'; // 移除 computed,因为不再需要前端切片
import { useRouter } from 'vue-router'; // 用于跳转详情页
import { ElMessage, ElSkeleton, ElEmpty } from 'element-plus'; // 导入 Element Plus 组件
import { getArticleList } from '@/api/Api'; // **修改点:导入 getArticleList,而不是 getAllArticle**
import type { Article, ArticleQueryParams, IPage } from '@/interface/Article'; // 导入接口
import dayjs from 'dayjs'; // 用于格式化时间
const router = useRouter(); // 获取路由实例
const articles = ref<Article[]>([]); // **现在只存储当前页的文章**
const totalArticles = ref(0); // **存储后端返回的总文章数**
const loading = ref(false); // 加载状态
// **修改点:queryParams 包含分页参数**
const queryParams = ref<ArticleQueryParams>({
current: 1, // 初始页码
size: 10, // 初始每页大小
// 后续若要添加搜索,可在 ArticleQueryParams 中添加 keyword 等字段
});
// --- 核心方法:从后端获取文章列表 ---
const fetchArticles = async () => {
loading.value = true; // 开始加载
try {
// 调用后端接口,传递当前页码和每页大小
const response: IPage<Article> = await getArticleList(queryParams.value);
// **修改点:直接将后端返回的当前页数据赋值给 articles**
articles.value = response.records;
// **修改点:将后端返回的总数赋值给 totalArticles**
totalArticles.value = response.total;
// 确保分页器和 queryParams 状态同步(通常后端返回的 current 和 size 会和请求的一致)
queryParams.value.current = response.current;
queryParams.value.size = response.size;
} catch (error: any) {
console.error('获取文章列表失败:', error);
ElMessage.error(error.message || '获取文章列表失败!');
articles.value = []; // 清空列表
totalArticles.value = 0; // 总数清零
} finally {
loading.value = false; // 结束加载
}
};
// 截断内容,用于文章摘要显示
const truncateContent = (content: string, maxLength = 40) => {
if (!content) return '';
return content.length > maxLength
? content.slice(0, maxLength) + '...'
: content;
};
// 时间格式化函数
const formatTime = (time: string | Date) => {
if (!time) return '未知时间';
return dayjs(time).format('YYYY-MM-DD HH:mm'); // 推荐使用 dayjs 进行更专业的日期格式化
};
// --- 分页事件处理函数 ---
// 当每页显示数量改变时
const handleSizeChange = (newSize: number) => {
if (newSize > 20) { // 限制最大每页 20 条
newSize = 20;
}
queryParams.value.size = newSize;
queryParams.value.current = 1; // **每页大小改变后,重置到第一页**
fetchArticles(); // 重新发起请求获取新数据
};
// 当当前页码改变时
const handleCurrentChange = (newPage: number) => {
queryParams.value.current = newPage;
fetchArticles(); // 重新发起请求获取新数据
};
// 跳转到文章详情页(假设你将来的详情页路由是 /article/:id)
const goToArticleDetail = (id: number) => {
router.push(`/article/${id}`);
};
// 组件挂载时,首次加载文章数据
onMounted(() => {
fetchArticles();
});
</script>
<style scoped>
.article-container {
padding: 20px;
max-width: 800px;
margin: 0 auto;
text-align: center;
}
h1 {
margin-bottom: 20px;
color: #333;
}
.art-card {
max-width: 800px;
margin: 24px auto;
}
</style>

记得把这个组件添加到Home.vue里,像TheHeader.vue一样

保存文件,热更新后可以看到你的主页有了一系列文章卡片展示

接下来实现点击文章卡片就能跳转到文章的详情页功能。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

6-文章篇-分页查询-Springboot Vue 教程
https://blog.yumui.top/posts/6-springboot-vue/
作者
Yu Felix
发布于
2025-05-11
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时