一、需求背景
在详情页中实现"上一条/下一条"的跨页循环导航功能:
功能需求:
**下一条:**当前页最后一条时,自动加载下一页第一条;最后一页最后一条时,弹框确认后跳转到第一页第一条
**上一条:**当前页第一条时,可跳转到上一页最后一条;第一页第一条时,弹框确认后跳转到最后一页最后一条
二、技术方案
前端调用现有分页接口,无需后端改动
使用 sessionStorage 保存查询条件和列表状态
通过 queryPageListBw 获取指定页数据,取首/尾记录
循环导航:首尾可循环跳转,提升浏览体验
三、实现步骤
3.1 列表页:保存查询条件
在跳转详情页时,将查询条件保存到 sessionStorage:
javascript
// collectionst/index.vue 或 collectionst1/index.vue
const handleViewDetail = (item) => {
const listData = {
files: dataList.value.map(file => ({
collectRecordId: file.collectRecordId,
collectFileId: file.collectFileId
})),
currentIndex: dataList.value.findIndex(file => file.collectFileId === item.collectFileId),
pageNum: queryParams.value.pageNum,
pageSize: queryParams.value.pageSize,
total: total.value,
// 关键:保存查询条件,用于跨页调用接口
queryParams: {
collectType: queryParams.value.collectType, // 采集类型(1-舌头 2-面部 3-脉诊 4-手掌)
collectPart: queryParams.value.collectPart, // 采集部位(1面 2舌底 3舌面 4手掌 5左脉 6右脉)
patientName: queryParams.value.patientName,
// ... 其他筛选条件
}
};
sessionStorage.setItem('collectionst_list_data', JSON.stringify(listData));
};
注意: collectType 和 collectPart 是接口必需参数,必须保存:
collectType:区分采集类型(舌头/面部/脉诊/手掌)
collectPart:区分采集部位(面/舌底/舌面/手掌/左脉/右脉)
3.2 详情页:导入接口
javascript
import { queryPageListBw } from "@/api/system/collection";
3.3 核心函数:获取跨页数据
javascript
// 获取下一页的第一条数据
const fetchNextPageFirstItem = async (listData) => {
const nextPageNum = listData.pageNum + 1;
const queryParams = listData.queryParams || {};
const bodyData = {
pageNum: nextPageNum,
pageSize: listData.pageSize,
collectType: queryParams.collectType || '1',
collectPart: queryParams.collectPart || '3',
patientName: queryParams.patientName || '',
patientPhone: queryParams.patientPhone || '',
collectNumber: queryParams.collectNumber || '',
beginTime: queryParams.beginTime || '',
endTime: queryParams.endTime || '',
codeList: [
{ interpretationTypeCode: 'collect_part_shedi', interpretationCode: queryParams.collectPartShedi },
{ interpretationTypeCode: 'collect_part_shetai', interpretationCode: queryParams.collectPartShetai },
{ interpretationTypeCode: 'collect_part_shexing', interpretationCode: queryParams.collectPartShexing },
{ interpretationTypeCode: 'collect_part_shezhi', interpretationCode: queryParams.collectPartShezhi }
].filter((item) => item.interpretationCode)
};
const res = await queryPageListBw(bodyData);
return res.data?.rows?.[0] || null;
};
// 获取最后一页的最后一条数据
const fetchLastPageLastItem = async (listData) => {
const totalPages = Math.ceil(listData.total / listData.pageSize);
const queryParams = listData.queryParams || {};
const bodyData = {
pageNum: totalPages,
pageSize: listData.pageSize,
collectType: queryParams.collectType || '1',
collectPart: queryParams.collectPart || '3',
// ... 其他参数同上
};
const res = await queryPageListBw(bodyData);
const rows = res.data?.rows || [];
return rows[rows.length - 1] || null;
};
3.4 下一条:自动跨页 + 循环确认
javascript
const cancel = async () => {
const listData = JSON.parse(sessionStorage.getItem('collectionst_list_data'));
const currentIndex = listData.files.findIndex(...);
// 如果是当前页最后一条
if (currentIndex === listData.files.length - 1) {
const totalPages = Math.ceil(listData.total / listData.pageSize);
if (listData.pageNum < totalPages) {
// 有下一页,自动获取下一页第一条
const nextItem = await fetchNextPageFirstItem(listData);
if (nextItem) {
// 更新 sessionStorage 并跳转
listData.pageNum += 1;
listData.currentIndex = 0;
// ... 更新文件列表
await router.replace({
query: {
collectFileId: nextItem.collectFileId,
collectRecordId: nextItem.collectRecordId
}
});
await fetchReportData();
}
} else {
// 是最后一页的最后一条,弹框确认是否查看第一条
goNextLoopConfirmVisible.value = true;
}
}
};
3.5 下一条循环确认处理
javascript
// 下一条循环确认弹窗状态
const goNextLoopConfirmVisible = ref(false);
// 处理下一条循环确认
const handleGoNextLoopConfirm = async () => {
goNextLoopConfirmVisible.value = false;
try {
const listData = JSON.parse(sessionStorage.getItem('collectionst_list_data'));
const queryParams = listData.queryParams || {};
// 获取第一页数据
const bodyData = {
pageNum: 1, // 第一页
pageSize: listData.pageSize,
collectType: queryParams.collectType || '1',
collectPart: queryParams.collectPart || '3',
// ... 其他参数
};
const res = await queryPageListBw(bodyData);
if (res.data?.rows?.[0]) {
const firstItem = res.data.rows[0];
// 更新 sessionStorage
listData.currentIndex = 0;
listData.pageNum = 1;
listData.files = res.data.rows.map(file => ({
collectRecordId: file.collectRecordId,
collectFileId: file.collectFileId
}));
sessionStorage.setItem('collectionst_list_data', JSON.stringify(listData));
// 跳转到第一条
await router.replace({
query: {
collectFileId: firstItem.collectFileId,
collectRecordId: firstItem.collectRecordId
}
});
await fetchReportData();
}
} catch (error) {
console.error("获取第一条数据失败:", error);
ElMessage.error("获取第一条数据失败");
}
};
3.6 上一条:分情况处理
javascript
const goBack = async () => {
const listData = JSON.parse(sessionStorage.getItem('collectionst_list_data'));
const currentIndex = listData.files.findIndex(...);
if (currentIndex === 0) {
if (listData.pageNum === 1) {
// 第一页第一条:弹框确认是否查看末尾数据
goBackLoopConfirmVisible.value = true;
} else {
// 其他页第一条:自动获取上一页最后一条
const prevPageNum = listData.pageNum - 1;
const res = await queryPageListBw({
pageNum: prevPageNum,
...listData.queryParams
});
const lastItem = res.data.rows[res.data.rows.length - 1];
// 更新并跳转...
}
}
};
3.7 弹窗模板
javascript
<!-- 上一条循环确认弹窗 -->
<div v-if="goBackLoopConfirmVisible" class="custom-confirm-overlay" @click="handleGoBackLoopCancel">
<div class="custom-confirm-dialog" @click.stop>
<div class="custom-confirm-content">
<img src="@/assets/images/th.png" alt="警告" />
<div class="custom-confirm-text">
<div class="custom-confirm-message">
当前是第一条数据,是否查看<br>末尾数据?
</div>
</div>
</div>
<div class="custom-confirm-buttons">
<button @click="handleGoBackLoopCancel">取消</button>
<button @click="handleGoBackLoopConfirm">确认</button>
</div>
</div>
</div>
<!-- 下一条循环确认弹窗 -->
<div v-if="goNextLoopConfirmVisible" class="custom-confirm-overlay" @click="handleGoNextLoopCancel">
<div class="custom-confirm-dialog" @click.stop>
<div class="custom-confirm-content">
<img src="@/assets/images/th.png" alt="警告" />
<div class="custom-confirm-text">
<div class="custom-confirm-message">
当前数据已经是最后一条,是否要<br>查看最新一条记录?
</div>
</div>
</div>
<div class="custom-confirm-buttons">
<button @click="handleGoNextLoopCancel">取消</button>
<button @click="handleGoNextLoopConfirm">确认</button>
</div>
</div>
</div>
四、完整流程图
下一条流程
用户点击"下一条"
↓
当前是当前页最后一条?
↓ 是
有下一页?
↓ 是
自动调用接口获取下一页第一条
↓
更新 sessionStorage 并跳转
↓ 否(最后一页最后一条)
弹框:"当前数据已经是最后一条,是否要查看最新一条记录?"
↓ 用户确认
调用接口获取第一页第一条
↓
更新 sessionStorage 并跳转到第一条
上一条流程
用户点击"上一条"
↓
当前是第一条?
↓ 是
是第一页?
↓ 是
弹框:"当前是第一条数据,是否查看末尾数据?"
↓ 用户确认
计算总页数:Math.ceil(total / pageSize)
↓
调用接口获取最后一页最后一条
↓
更新 sessionStorage 并跳转
↓ 否(其他页第一条)
自动调用接口获取上一页最后一条
↓
更新 sessionStorage 并跳转
五、关键技术点
1.查询条件持久化:通过 sessionStorage 保存筛选条件,跨页时保持一致
2.总页数计算:Math.ceil(total / pageSize)
3.数据同步:跨页后同步更新 sessionStorage 中的页码、文件列表、索引
4.循环导航:首尾可循环跳转,提升浏览体验
5.用户确认:首尾循环时弹框确认,避免误操作
六、注意事项
6.1 必须保存的字段
collectType:采集类型(1-舌头 2-面部 3-脉诊 4-手掌)
collectPart:采集部位(1面 2舌底 3舌面 4手掌 5左脉 6右脉)
这两个字段是接口的必需参数,用于确定查询的数据类型和部位。
6.2 数据关系
舌底、舌面 → collectType: '1'(舌头)
左脉、右脉 → collectType: '3'(脉诊)
面部 → collectType: '2'
手掌 → collectType: '4'
七、总结
无需后端改动,复用现有分页接口
通过保存查询条件实现筛选状态保持
自动跨页提升操作效率
循环导航提升浏览体验
代码结构清晰,易于维护
八、适用场景
详情页需要跨页导航
需要保持筛选条件
希望实现循环浏览
希望减少后端改动
总结:
一、核心思路
不需要后端新接口,直接复用列表页的分页接口 queryPageListBw。
二、实现方式
保存查询条件
从列表页进入详情页时,将查询条件(筛选参数、页码、每页大小、总记录数)保存到 sessionStorage
跨页时用这些条件调用接口
跨页调用接口
下一条到最后一页最后一条时:调用接口获取 pageNum + 1 的第一条
上一条到第一页第一条时:调用接口获取最后一页的最后一条
循环时:调用接口获取第一页的第一条
更新本地状态
获取到数据后,更新 sessionStorage 中的页码、文件列表、当前索引
然后跳转到新记录
三、为什么用 Math.ceil(total / pageSize) 计算总页数?
原因:
total:总记录数(如 100 条)
pageSize:每页大小(如 10 条)
总页数 = 总记录数 ÷ 每页大小,向上取整
举例:
100 条记录,每页 10 条 → 100 ÷ 10 = 10 页
101 条记录,每页 10 条 → 101 ÷ 10 = 10.1 → Math.ceil(10.1) = 11 页
为什么向上取整?
因为最后一条数据需要单独一页,即使只有 1 条也要算 1 页
Math.ceil() 向上取整,确保最后一条数据能被正确计算到最后一页
不需要后端新接口,复用现有接口
通过保存查询条件,跨页时调用接口获取指定页数据
用 Math.ceil(total / pageSize) 计算总页数,确保最后一条数据被正确计算
实现成本低,只需前端改动