web-第10次课后作业

📅 2026/7/5 8:32:39
web-第10次课后作业
分页功能详解本文档整理项目中分页功能涉及的全部内容从数据库到前端共四层。一、整体数据流前端 UIApp.vue → 前端 API 层api/user.js → HTTP 请求 → 后端 ControllerUserController.java → 后端 MapperUserMapper.java → SQL Server 执行分页 SQL ← 返回 { list, total, page, pageSize } ← Axios 拿到响应 ← 更新 users[] 和 totalUI 自动重渲染二、数据库层 —— SQL Server 分页语法2.1 分页查询 SQL文件src/main/java/com/example/pj4/mapper/UserMapper.java:14-20Select(SELECT id, name, age, gender, phone FROM [user] WHERE name LIKE CONCAT(%, #{keyword}, %) ORDER BY id OFFSET #{offset} ROWS FETCH NEXT #{pageSize} ROWS ONLY)ListUserlistByPage(Param(keyword)Stringkeyword,Param(offset)intoffset,Param(pageSize)intpageSize);SQL Server 分页语法SELECT...FROM[user]WHERE...ORDERBYidOFFSET0ROWSFETCHNEXT10ROWSONLY-- 跳过 0 行取 10 行第 1 页OFFSET10ROWSFETCHNEXT10ROWSONLY-- 跳过 10 行取 10 行第 2 页OFFSET20ROWSFETCHNEXT10ROWSONLY-- 跳过 20 行取 10 行第 3 页页码OFFSETFETCH NEXT含义1010跳过 0 行取 10 行21010跳过 10 行取 10 行32010跳过 20 行取 10 行注意SQL Server 要求OFFSET/FETCH NEXT必须配合ORDER BY使用否则报错。本项目按id排序确保分页结果稳定。2.2 总数统计 SQL文件src/main/java/com/example/pj4/mapper/UserMapper.java:22-24Select(SELECT COUNT(*) FROM [user] WHERE name LIKE CONCAT(%, #{keyword}, %))intcount(Param(keyword)Stringkeyword);关键点COUNT(*)使用与分页查询相同的 WHERE 条件确保统计的总记录数与实际数据一致前端分页组件渲染才正确。2.3 模糊搜索WHEREnameLIKECONCAT(%,#{keyword}, %)传空字符串时 →LIKE %%匹配全部记录传王时 →LIKE %王%匹配姓名包含王的记录使用#{}参数化查询防 SQL 注入三、后端 Controller 层文件src/main/java/com/example/pj4/controller/UserController.java:26-36GetMappingpublicResponseEntityMapString,Objectlist(RequestParam(requiredfalse,defaultValue)Stringkeyword,RequestParam(defaultValue1)intpage,RequestParam(defaultValue10)intpageSize){intoffset(page-1)*pageSize;ListUserusersuserMapper.listByPage(keyword,offset,pageSize);inttotaluserMapper.count(keyword);returnResponseEntity.ok(Map.of(message,success,data,Map.of(list,users,total,total,page,page,pageSize,pageSize)));}3.1 接口参数参数类型默认值说明keywordstring搜索关键词空字符串查全部pageint1当前页码从 1 开始pageSizeint10每页条数3.2 offset 计算公式offset (page - 1) × pageSizepagepageSizeoffset含义1100第 1 页跳过 0 行21010第 2 页跳过 10 行32040第 3 页每页 20 条跳过 40 行3.3 响应格式{message:success,data:{list:[{id:1,name:白眉鹰王,age:55,gender:1,phone:18800000000},{id:2,name:金毛狮王,age:45,gender:1,phone:18800000001}],total:6,page:1,pageSize:10}}为什么 data 里包含 total— 前端分页组件需要同时知道当前页数据和总记录数才能渲染页码按钮和判断是否有下一页。四、前端 API 层文件frontend/src/api/user.js:8-9exportfunctiongetUsers({keyword,page1,pageSize10}{}){returnapi.get(/users,{params:{keyword,page,pageSize}})}参数解构 默认值调用方可以getUsers()查全部第 1 页、getUsers({ keyword: 王 })搜索、getUsers({ page: 2, pageSize: 20 })翻页。Axios 会把 params 对象序列化为查询字符串GET /api/users?keyword王page2pageSize10五、前端 UI 层App.vue5.1 状态变量文件frontend/src/App.vue:7-18constcurrentPageref(1)// 当前页码constpageSizeref(10)// 每页条数consttotalref(0)// 总记录数constkeywordref()// 搜索关键词constusersref([])// 当前页用户数据constloadingref(false)// 加载状态5.2 加载数据文件frontend/src/App.vue:20-36constloadUsersasync(){loading.valuetruetry{constresawaitgetUsers({keyword:keyword.value.trim(),page:currentPage.value,pageSize:pageSize.value})constbodyres.data.data users.valuebody.list// 当前页数据total.valuebody.total// 总记录数}catch{ElMessage.error(加载用户列表失败)}finally{loading.valuefalse}}5.3 搜索行为文件frontend/src/App.vue:38-41constsearch(){currentPage.value1// 搜索时重置到第 1 页loadUsers()}5.4 翻页行为// 点击页码constonPageChange(page){currentPage.valuepageloadUsers()}// 切换每页条数constonPageSizeChange(size){pageSize.valuesize currentPage.value1// 条数变化时重置到第 1 页loadUsers()}5.5 删除后的分页边界处理文件frontend/src/App.vue:82-83// 当前页最后一条被删除时自动回到上一页if(users.value.length1currentPage.value1){currentPage.value--}例子第 3 页只有 1 条数据删除后数据库只剩 2 页。如果不减 page刷新后第 3 页为空。5.6 分页组件模板文件frontend/src/App.vue:252-315分页栏由四个部分组成┌─────────────────────────────────────────────────────────────────────┐ │ 第 1 / 3 页 │ ‹ 1 2 3 › │ 每页 5 10 20 50 条 │ 跳至 [__] 页 │ └─────────────────────────────────────────────────────────────────────┘ ①页数统计 ②翻页按钮 ③每页条数选择器 ④页码跳转① 页数统计spanclasspage-stats第 {{ currentPage }} / {{ Math.ceil(total / pageSize) || 1 }} 页/span总页数 Math.ceil(total / pageSize)例如 38 条每页 10 条 4 页。② 翻页按钮!-- 上一页第 1 页时 disabled --button:disabledcurrentPage 1clickonPageChange(currentPage - 1)‹/button!-- 页码按钮最多显示 5 个 --buttonv-forp in Math.min(5, Math.ceil(total / pageSize))...{{ p }}/button!-- 下一页最后一页时 disabled --button:disabledcurrentPage Math.ceil(total / pageSize)clickonPageChange(currentPage 1)›/button当前版本的页码按钮简化为只显示前 5 页实际的 UI 实现为简洁考虑。③ 每页条数选择器buttonv-fors in [5, 10, 20, 50]clickonPageSizeChange(s){{ s }}/button可选 5、10、20、50 条每页点击后pageSize更新且currentPage重置为 1。④ 页码跳转inputkeyup.enter(e) { const v parseInt(e.target.value) const max Math.ceil(total / pageSize) || 1 if (v 1 v max) onPageChange(v) e.target.value }/输入页码后按 Enter 跳转超出范围则忽略。六、分页功能完整链路图用户操作 前端 后端 数据库 ─────── ────── ────── ────── 点击搜索 │ ├─ keyword search() ├─ page 1 currentPage.value 1 │ loadUsers() │ │ │ ├─ getUsers({ keyword:, ──→ GET /api/users?keywordpage1pageSize10 │ │ page:1, pageSize:10 }) │ │ │ list(keyword, page1, │ │ pageSize10) │ │ │ │ │ ├─ offset (1-1)*10 0 │ │ │ │ │ ├─ userMapper.listByPage → SELECT ... WHERE │ │ │ (, 0, 10) name LIKE %% │ │ │ ORDER BY id │ │ │ OFFSET 0 ROWS │ │ │ FETCH NEXT 10 ROWS ONLY │ │ │ → 返回前 10 行 │ │ │ │ │ └─ userMapper.count() → SELECT COUNT(*) │ │ WHERE name LIKE %% │ │ → 返回 total6 │ │ │ │ ←─── 200 OK │ users.value res.data.data.list { message:success, │ total.value res.data.data.total data: { list:[...], total:6, page:1, pageSize:10 } } │ loading.value false │ └─ UI 自动重渲染表格显示 6 条数据分页栏显示第 1 / 1 页、每页 10 条 点击下一页 │ ├─ page 3 onPageChange(3) │ currentPage.value 3 │ loadUsers() │ │ │ ├─ getUsers({ keyword:, ──→ GET /api/users?keywordpage3pageSize10 │ │ page:3, pageSize:10 }) │ │ │ offset (3-1)*10 20 │ │ │ │ │ └─ OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY │ │ → 跳过前 20 行返回第 21~30 行 │ │ │ users [第21~30条] │ total 38 │ └─ UI 显示第 3 页数据分页栏显示第 3 / 4 页 切换每页 20 条 │ ├─ size 20 onPageSizeChange(20) │ pageSize.value 20 │ currentPage.value 1 ← 关键重置到第 1 页 │ loadUsers() │ │ │ └─ getUsers({ page:1, pageSize:20 }) │ └─ UI 重新以每页 20 条显示第 1 页数据 删除当前页最后一条 │ ├─ ElMessageBox.confirm handleDelete(row) │ deleteUser(row.id) │ │ │ if (users.length1 │ ← API 调用成功后 │ currentPage 1) │ │ currentPage-- ← 当前页只剩 1 条且不是第 1 页回退一页 │ └─ loadUsers() → 刷新数据七、分页参数对照表7.1 前端到后端前端变量请求参数后端接收说明keyword?keywordString keyword搜索关键词currentPage?pageint page页码从 1 开始pageSize?pageSizeint pageSize每页条数7.2 后端到数据库后端计算Mapper 参数SQL 片段说明(page-1)*pageSizeint offsetOFFSET #{offset} ROWS跳过的行数pageSizeint pageSizeFETCH NEXT #{pageSize} ROWS ONLY取多少行keywordString keywordWHERE name LIKE CONCAT(%, #{keyword}, %)模糊搜索7.3 默认值链路参数前端默认后端默认SQL 效果keywordLIKE %%查全部page11offset0pageSize1010FETCH NEXT 10八、关键设计决策8.1 搜索在 SQL 层而非 Java 内存为什么— 如果提取全部数据后在 Java 中用.filter()过滤分页就会乱❌ 错误做法SQL 返回全部 50 条 → Java 过滤后剩 20 条 → 手动截取前 10 条 问题第 2 页还是同样的 20 条中截取分页没意义 ✅ 正确做法SQL 直接带 WHERE 条件 OFFSET/FETCH SQL 返回的就是当前页的匹配数据COUNT 返回的也是匹配总数8.2 搜索/切换每页条数时重置页码操作重置 currentPage原因搜索→ 1新关键词的匹配结果可能只有 1 页当前页可能超出范围切换每页条数→ 1每页条数变化后总页数变了当前页可能超出范围翻页不重置正常翻页行为8.3 删除最后一条的边界处理if(users.value.length1currentPage.value1){currentPage.value--}场景第 3 页有 1 条数据删除后只剩 2 页总数据。如果不减去 1loadUsers()刷新后第 3 页为空。九、SQL Server vs MySQL 分页语法对比数据库分页语法示例SQL ServerOFFSET n ROWS FETCH NEXT m ROWS ONLYOFFSET 0 ROWS FETCH NEXT 10 ROWS ONLYMySQLLIMIT offset, sizeLIMIT 0, 10PostgreSQLLIMIT size OFFSET nLIMIT 10 OFFSET 0OracleROWNUM子查询WHERE ROWNUM 10SQL Server 的OFFSET / FETCH NEXT语法从 SQL Server 2012 开始支持要求必须有ORDER BY子句。