feat(笔记预览): 实现大文件分块加载功能

添加分块加载API接口及前端实现,支持大文件(>500KB)的分页加载,提升大文件预览体验
后端实现分块逻辑并添加权限检查,前端添加加载提示和滚动加载功能
This commit is contained in:
ikmkj
2026-03-04 16:43:25 +08:00
parent 90626e73d9
commit 5ea9c776e7
7 changed files with 364 additions and 9 deletions

View File

@@ -3,6 +3,7 @@ package com.test.bijihoudaun.controller;
import cn.hutool.core.util.ObjectUtil;
import com.test.bijihoudaun.common.response.R;
import com.test.bijihoudaun.entity.MarkdownFile;
import com.test.bijihoudaun.entity.MarkdownFileChunk;
import com.test.bijihoudaun.entity.MarkdownFileVO;
import com.test.bijihoudaun.service.MarkdownFileService;
import com.test.bijihoudaun.util.SecurityUtil;
@@ -117,4 +118,26 @@ public class MarkdownController {
return R.success(files);
}
@Operation(summary = "分块加载Markdown文件内容", description = "用于大文件(> 500KB的分页加载")
@Parameters({
@Parameter(name = "id", description = "文件ID", required = true),
@Parameter(name = "chunkIndex", description = "块索引从0开始", required = false),
@Parameter(name = "chunkSize", description = "块大小字符数默认10000", required = false)
})
@GetMapping("/{id}/chunk")
public R<MarkdownFileChunk> getMarkdownChunk(
@PathVariable Long id,
@RequestParam(defaultValue = "0") int chunkIndex,
@RequestParam(defaultValue = "10000") int chunkSize) {
// 获取当前认证状态
boolean isAuthenticated = SecurityUtil.isUserAuthenticated();
MarkdownFileChunk chunk = markdownFileService.getMarkdownChunk(id, chunkIndex, chunkSize, isAuthenticated);
if (ObjectUtil.isNotNull(chunk)) {
return R.success(chunk);
}
return R.fail("文件未找到");
}
}

View File

@@ -0,0 +1,46 @@
package com.test.bijihoudaun.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* Markdown文件分块加载响应实体
* 用于大文件分页加载
*/
@Data
@Schema(name = "Markdown文件分块")
public class MarkdownFileChunk implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "当前块的内容")
private String chunk;
@Schema(description = "当前块索引从0开始")
private int chunkIndex;
@Schema(description = "总块数")
private int totalChunks;
@Schema(description = "是否还有更多块")
private boolean hasMore;
@Schema(description = "笔记ID")
@JsonFormat(shape = JsonFormat.Shape.STRING)
private Long fileId;
@Schema(description = "笔记标题")
private String title;
@Schema(description = "是否私密 0-公开 1-私密")
private Integer isPrivate;
@Schema(description = "总字符数")
private long totalLength;
@Schema(description = "当前块字符数")
private int chunkLength;
}

View File

@@ -2,6 +2,7 @@ package com.test.bijihoudaun.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.test.bijihoudaun.entity.MarkdownFile;
import com.test.bijihoudaun.entity.MarkdownFileChunk;
import com.test.bijihoudaun.entity.MarkdownFileVO;
import java.util.List;
@@ -66,4 +67,15 @@ public interface MarkdownFileService extends IService<MarkdownFile> {
* @return 文件列表
*/
List<MarkdownFileVO> getRecentFiles(int limit);
/**
* 分块加载Markdown文件内容
* 用于大文件(> 500KB的分页加载
* @param id 文件ID
* @param chunkIndex 块索引从0开始
* @param chunkSize 块大小字符数默认10000
* @param isAuthenticated 是否已认证
* @return 文件块对象
*/
MarkdownFileChunk getMarkdownChunk(Long id, int chunkIndex, int chunkSize, boolean isAuthenticated);
}

View File

@@ -8,6 +8,7 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.test.bijihoudaun.entity.ImageName;
import com.test.bijihoudaun.entity.MarkdownFile;
import com.test.bijihoudaun.entity.MarkdownFileChunk;
import com.test.bijihoudaun.entity.MarkdownFileVO;
import com.test.bijihoudaun.mapper.ImageNameMapper;
import com.test.bijihoudaun.mapper.MarkdownFileMapper;
@@ -156,4 +157,92 @@ public class MarkdownFileServiceImpl
return markdownFileMapper.selectRecentWithGrouping(limit);
}
@Override
public MarkdownFileChunk getMarkdownChunk(Long id, int chunkIndex, int chunkSize, boolean isAuthenticated) {
// 获取文件基本信息(不包含内容)
MarkdownFileVO fileVO = markdownFileMapper.selectByIdWithGrouping(id);
if (fileVO == null) {
return null;
}
// 检查权限:私密笔记需要登录
if (fileVO.getIsPrivate() != null && fileVO.getIsPrivate() == 1 && !isAuthenticated) {
MarkdownFileChunk chunk = new MarkdownFileChunk();
chunk.setFileId(id);
chunk.setTitle(fileVO.getTitle());
chunk.setIsPrivate(fileVO.getIsPrivate());
chunk.setChunk("该笔记为私密笔记,请登录后查看");
chunk.setChunkIndex(0);
chunk.setTotalChunks(1);
chunk.setHasMore(false);
chunk.setTotalLength(0);
chunk.setChunkLength(0);
return chunk;
}
// 获取完整内容
MarkdownFile file = this.getById(id);
if (file == null || file.getContent() == null) {
return null;
}
String fullContent = file.getContent();
int totalLength = fullContent.length();
// 如果文件小于 500KB约50万字符直接返回全部内容
if (totalLength <= 500000) {
MarkdownFileChunk chunk = new MarkdownFileChunk();
chunk.setFileId(id);
chunk.setTitle(file.getTitle());
chunk.setIsPrivate(file.getIsPrivate());
chunk.setChunk(fullContent);
chunk.setChunkIndex(0);
chunk.setTotalChunks(1);
chunk.setHasMore(false);
chunk.setTotalLength(totalLength);
chunk.setChunkLength(totalLength);
return chunk;
}
// 大文件分页加载
// 计算总块数
int totalChunks = (int) Math.ceil((double) totalLength / chunkSize);
// 确保 chunkIndex 在有效范围内
if (chunkIndex < 0) {
chunkIndex = 0;
}
if (chunkIndex >= totalChunks) {
chunkIndex = totalChunks - 1;
}
// 计算当前块的起止位置
int start = chunkIndex * chunkSize;
int end = Math.min(start + chunkSize, totalLength);
// 截取内容
String chunkContent = fullContent.substring(start, end);
// 如果不是第一块,尝试从完整的换行处开始
if (chunkIndex > 0 && start < totalLength) {
// 找到第一个换行符,从下一行开始显示
int firstNewLine = chunkContent.indexOf('\n');
if (firstNewLine > 0) {
chunkContent = chunkContent.substring(firstNewLine + 1);
}
}
MarkdownFileChunk chunk = new MarkdownFileChunk();
chunk.setFileId(id);
chunk.setTitle(file.getTitle());
chunk.setIsPrivate(file.getIsPrivate());
chunk.setChunk(chunkContent);
chunk.setChunkIndex(chunkIndex);
chunk.setTotalChunks(totalChunks);
chunk.setHasMore(chunkIndex < totalChunks - 1);
chunk.setTotalLength(totalLength);
chunk.setChunkLength(chunkContent.length());
return chunk;
}
}