feat(笔记预览): 实现大文件分块加载功能
添加分块加载API接口及前端实现,支持大文件(>500KB)的分页加载,提升大文件预览体验 后端实现分块逻辑并添加权限检查,前端添加加载提示和滚动加载功能
This commit is contained in:
@@ -3,6 +3,7 @@ package com.test.bijihoudaun.controller;
|
|||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import com.test.bijihoudaun.common.response.R;
|
import com.test.bijihoudaun.common.response.R;
|
||||||
import com.test.bijihoudaun.entity.MarkdownFile;
|
import com.test.bijihoudaun.entity.MarkdownFile;
|
||||||
|
import com.test.bijihoudaun.entity.MarkdownFileChunk;
|
||||||
import com.test.bijihoudaun.entity.MarkdownFileVO;
|
import com.test.bijihoudaun.entity.MarkdownFileVO;
|
||||||
import com.test.bijihoudaun.service.MarkdownFileService;
|
import com.test.bijihoudaun.service.MarkdownFileService;
|
||||||
import com.test.bijihoudaun.util.SecurityUtil;
|
import com.test.bijihoudaun.util.SecurityUtil;
|
||||||
@@ -117,4 +118,26 @@ public class MarkdownController {
|
|||||||
return R.success(files);
|
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("文件未找到");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package com.test.bijihoudaun.service;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
import com.test.bijihoudaun.entity.MarkdownFile;
|
import com.test.bijihoudaun.entity.MarkdownFile;
|
||||||
|
import com.test.bijihoudaun.entity.MarkdownFileChunk;
|
||||||
import com.test.bijihoudaun.entity.MarkdownFileVO;
|
import com.test.bijihoudaun.entity.MarkdownFileVO;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -66,4 +67,15 @@ public interface MarkdownFileService extends IService<MarkdownFile> {
|
|||||||
* @return 文件列表
|
* @return 文件列表
|
||||||
*/
|
*/
|
||||||
List<MarkdownFileVO> getRecentFiles(int limit);
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
|||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.test.bijihoudaun.entity.ImageName;
|
import com.test.bijihoudaun.entity.ImageName;
|
||||||
import com.test.bijihoudaun.entity.MarkdownFile;
|
import com.test.bijihoudaun.entity.MarkdownFile;
|
||||||
|
import com.test.bijihoudaun.entity.MarkdownFileChunk;
|
||||||
import com.test.bijihoudaun.entity.MarkdownFileVO;
|
import com.test.bijihoudaun.entity.MarkdownFileVO;
|
||||||
import com.test.bijihoudaun.mapper.ImageNameMapper;
|
import com.test.bijihoudaun.mapper.ImageNameMapper;
|
||||||
import com.test.bijihoudaun.mapper.MarkdownFileMapper;
|
import com.test.bijihoudaun.mapper.MarkdownFileMapper;
|
||||||
@@ -156,4 +157,92 @@ public class MarkdownFileServiceImpl
|
|||||||
return markdownFileMapper.selectRecentWithGrouping(limit);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ export const markdownAll = () => axiosApi.get(`/api/markdown`);
|
|||||||
// 预览markdown文件
|
// 预览markdown文件
|
||||||
export const Preview = (id) => axiosApi.get(`/api/markdown/${id}`);
|
export const Preview = (id) => axiosApi.get(`/api/markdown/${id}`);
|
||||||
|
|
||||||
|
// 分块加载markdown文件内容(用于大文件)
|
||||||
|
export const PreviewChunk = (id, chunkIndex = 0, chunkSize = 10000) =>
|
||||||
|
axiosApi.get(`/api/markdown/${id}/chunk?chunkIndex=${chunkIndex}&chunkSize=${chunkSize}`);
|
||||||
|
|
||||||
// 创建分类分组
|
// 创建分类分组
|
||||||
export const addGroupings = (group) => {
|
export const addGroupings = (group) => {
|
||||||
return axiosApi.post('/api/groupings', group);
|
return axiosApi.post('/api/groupings', group);
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
:file="selectedFile"
|
:file="selectedFile"
|
||||||
:is-mobile="isMobile"
|
:is-mobile="isMobile"
|
||||||
:is-user-logged-in="userStore.isLoggedIn"
|
:is-user-logged-in="userStore.isLoggedIn"
|
||||||
|
:has-more-chunks="hasMoreChunks"
|
||||||
@back="selectedFile = null"
|
@back="selectedFile = null"
|
||||||
@edit="editNote(selectedFile)"
|
@edit="editNote(selectedFile)"
|
||||||
@delete="deleteNote(selectedFile)"
|
@delete="deleteNote(selectedFile)"
|
||||||
@@ -66,8 +67,11 @@
|
|||||||
<el-icon class="is-loading"><Loading /></el-icon>
|
<el-icon class="is-loading"><Loading /></el-icon>
|
||||||
<span>加载中...</span>
|
<span>加载中...</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="hasMoreFiles && !showEditor && !selectedFile" class="load-more-trigger">
|
<div v-else-if="hasMoreFiles && !showEditor && !selectedFile" class="load-more-hint">
|
||||||
<el-button @click="loadMoreFiles" type="primary" plain>加载更多</el-button>
|
<span>继续滚动加载更多...</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="!hasMoreFiles && displayedFiles.length > 0" class="no-more-hint">
|
||||||
|
<span>没有更多笔记了</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -140,6 +144,7 @@ import {
|
|||||||
groupingAll,
|
groupingAll,
|
||||||
markdownList,
|
markdownList,
|
||||||
Preview,
|
Preview,
|
||||||
|
PreviewChunk,
|
||||||
updateMarkdown,
|
updateMarkdown,
|
||||||
searchMarkdown,
|
searchMarkdown,
|
||||||
deleteMarkdown,
|
deleteMarkdown,
|
||||||
@@ -180,6 +185,7 @@ const displayedFiles = ref([]);
|
|||||||
const currentPage = ref(0);
|
const currentPage = ref(0);
|
||||||
const pageSize = ref(16);
|
const pageSize = ref(16);
|
||||||
const isLoadingMore = ref(false);
|
const isLoadingMore = ref(false);
|
||||||
|
const loadCount = ref(0); // 记录加载次数,用于动态增加加载数量
|
||||||
const noteListWrapper = ref(null);
|
const noteListWrapper = ref(null);
|
||||||
const showEditor = ref(false);
|
const showEditor = ref(false);
|
||||||
const selectedFile = ref(null);
|
const selectedFile = ref(null);
|
||||||
@@ -203,6 +209,12 @@ const vditorReady = ref(false);
|
|||||||
// 提供给子组件使用
|
// 提供给子组件使用
|
||||||
provide('vditorReady', vditorReady);
|
provide('vditorReady', vditorReady);
|
||||||
|
|
||||||
|
// 大文件分块加载状态
|
||||||
|
const currentChunkIndex = ref(0);
|
||||||
|
const hasMoreChunks = ref(false);
|
||||||
|
const isLoadingChunk = ref(false);
|
||||||
|
const totalChunks = ref(0);
|
||||||
|
|
||||||
// Data for dialogs
|
// Data for dialogs
|
||||||
const itemToRename = ref(null);
|
const itemToRename = ref(null);
|
||||||
const fileToImport = ref(null);
|
const fileToImport = ref(null);
|
||||||
@@ -258,6 +270,7 @@ const resetToHomeView = async () => {
|
|||||||
searchKeyword.value = '';
|
searchKeyword.value = '';
|
||||||
activeMenu.value = 'all';
|
activeMenu.value = 'all';
|
||||||
currentPage.value = 0;
|
currentPage.value = 0;
|
||||||
|
loadCount.value = 0; // 重置加载计数
|
||||||
try {
|
try {
|
||||||
groupMarkdownFiles.value = await getRecentFiles(100) || [];
|
groupMarkdownFiles.value = await getRecentFiles(100) || [];
|
||||||
updateDisplayedFiles();
|
updateDisplayedFiles();
|
||||||
@@ -269,9 +282,21 @@ const resetToHomeView = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 计算动态加载数量:基础16个,每次增加8个,最多48个
|
||||||
|
const getDynamicPageSize = () => {
|
||||||
|
const baseSize = 16;
|
||||||
|
const increment = 8;
|
||||||
|
const maxSize = 48;
|
||||||
|
return Math.min(baseSize + loadCount.value * increment, maxSize);
|
||||||
|
};
|
||||||
|
|
||||||
const updateDisplayedFiles = () => {
|
const updateDisplayedFiles = () => {
|
||||||
const start = 0;
|
const start = 0;
|
||||||
const end = (currentPage.value + 1) * pageSize.value;
|
const dynamicPageSize = getDynamicPageSize();
|
||||||
|
// 计算实际显示数量:首次16个,之后动态增加
|
||||||
|
const end = displayedFiles.value.length === 0
|
||||||
|
? dynamicPageSize
|
||||||
|
: displayedFiles.value.length + dynamicPageSize;
|
||||||
displayedFiles.value = groupMarkdownFiles.value.slice(start, end);
|
displayedFiles.value = groupMarkdownFiles.value.slice(start, end);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -280,6 +305,7 @@ const loadMoreFiles = () => {
|
|||||||
isLoadingMore.value = true;
|
isLoadingMore.value = true;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
loadCount.value++;
|
||||||
currentPage.value++;
|
currentPage.value++;
|
||||||
updateDisplayedFiles();
|
updateDisplayedFiles();
|
||||||
isLoadingMore.value = false;
|
isLoadingMore.value = false;
|
||||||
@@ -288,11 +314,21 @@ const loadMoreFiles = () => {
|
|||||||
|
|
||||||
const handleScroll = (e) => {
|
const handleScroll = (e) => {
|
||||||
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
||||||
if (scrollHeight - scrollTop - clientHeight < 100 && hasMoreFiles.value && !isLoadingMore.value) {
|
// 距离底部 200px 时自动加载,给用户更流畅的体验
|
||||||
|
if (scrollHeight - scrollTop - clientHeight < 200 && hasMoreFiles.value && !isLoadingMore.value) {
|
||||||
loadMoreFiles();
|
loadMoreFiles();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 预览区域滚动处理(用于大文件分块加载)
|
||||||
|
const handlePreviewScroll = (e) => {
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
||||||
|
// 距离底部 300px 时加载下一页内容
|
||||||
|
if (scrollHeight - scrollTop - clientHeight < 300 && hasMoreChunks.value && !isLoadingChunk.value) {
|
||||||
|
loadMoreContent();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Event Handlers from Components
|
// Event Handlers from Components
|
||||||
const handleSelectFile = async (data) => {
|
const handleSelectFile = async (data) => {
|
||||||
resetEdit();
|
resetEdit();
|
||||||
@@ -300,6 +336,7 @@ const handleSelectFile = async (data) => {
|
|||||||
const files = await markdownList(data.id);
|
const files = await markdownList(data.id);
|
||||||
groupMarkdownFiles.value = files || [];
|
groupMarkdownFiles.value = files || [];
|
||||||
currentPage.value = 0;
|
currentPage.value = 0;
|
||||||
|
loadCount.value = 0; // 重置加载计数
|
||||||
updateDisplayedFiles();
|
updateDisplayedFiles();
|
||||||
selectedFile.value = null;
|
selectedFile.value = null;
|
||||||
showEditor.value = false;
|
showEditor.value = false;
|
||||||
@@ -377,26 +414,71 @@ const previewFile = async (file) => {
|
|||||||
}
|
}
|
||||||
// 重置渲染缓存,确保每次打开笔记都重新渲染
|
// 重置渲染缓存,确保每次打开笔记都重新渲染
|
||||||
lastRenderedKey = null;
|
lastRenderedKey = null;
|
||||||
|
// 重置分块加载状态
|
||||||
|
currentChunkIndex.value = 0;
|
||||||
|
hasMoreChunks.value = false;
|
||||||
|
isLoadingChunk.value = false;
|
||||||
|
totalChunks.value = 0;
|
||||||
|
|
||||||
// 先立即显示预览页(加载状态),让用户感知到响应
|
// 先立即显示预览页(加载状态),让用户感知到响应
|
||||||
selectedFile.value = { ...file, content: '', isLoading: true, isRendering: false };
|
selectedFile.value = { ...file, content: '', isLoading: true, isRendering: false };
|
||||||
showEditor.value = false;
|
showEditor.value = false;
|
||||||
|
|
||||||
// 异步加载内容
|
// 异步加载内容(使用分块加载)
|
||||||
|
await loadNoteChunk(file.id, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 分块加载笔记内容
|
||||||
|
const loadNoteChunk = async (fileId, chunkIndex) => {
|
||||||
|
if (isLoadingChunk.value) return;
|
||||||
|
|
||||||
|
isLoadingChunk.value = true;
|
||||||
try {
|
try {
|
||||||
const content = await Preview(file.id) || '';
|
const chunkData = await PreviewChunk(fileId, chunkIndex, 10000);
|
||||||
// 内容加载完成后更新
|
|
||||||
if (selectedFile.value && selectedFile.value.id === file.id) {
|
if (selectedFile.value && selectedFile.value.id === fileId) {
|
||||||
|
// 更新分块加载状态
|
||||||
|
currentChunkIndex.value = chunkData.chunkIndex;
|
||||||
|
hasMoreChunks.value = chunkData.hasMore;
|
||||||
|
totalChunks.value = chunkData.totalChunks;
|
||||||
|
|
||||||
|
// 如果是第一块,直接设置内容;否则追加内容
|
||||||
|
let newContent;
|
||||||
|
if (chunkIndex === 0) {
|
||||||
|
newContent = chunkData.chunk;
|
||||||
|
} else {
|
||||||
|
newContent = selectedFile.value.content + chunkData.chunk;
|
||||||
|
}
|
||||||
|
|
||||||
// 如果 Vditor 渲染引擎未就绪,显示渲染中状态
|
// 如果 Vditor 渲染引擎未就绪,显示渲染中状态
|
||||||
const isRendering = !vditorReady.value;
|
const isRendering = !vditorReady.value;
|
||||||
selectedFile.value = { ...file, content, isLoading: false, isRendering };
|
|
||||||
|
selectedFile.value = {
|
||||||
|
...selectedFile.value,
|
||||||
|
content: newContent,
|
||||||
|
isLoading: false,
|
||||||
|
isRendering,
|
||||||
|
title: chunkData.title || selectedFile.value.title,
|
||||||
|
isPrivate: chunkData.isPrivate !== undefined ? chunkData.isPrivate : selectedFile.value.isPrivate
|
||||||
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 错误已在 axios 拦截器中显示,这里不再重复显示
|
// 错误已在 axios 拦截器中显示,这里不再重复显示
|
||||||
console.error('获取笔记内容失败:', error);
|
console.error('获取笔记内容失败:', error);
|
||||||
selectedFile.value = null;
|
selectedFile.value = null;
|
||||||
|
} finally {
|
||||||
|
isLoadingChunk.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 加载更多内容(滚动到底部时调用)
|
||||||
|
const loadMoreContent = async () => {
|
||||||
|
if (!selectedFile.value || !hasMoreChunks.value || isLoadingChunk.value) return;
|
||||||
|
|
||||||
|
const nextChunkIndex = currentChunkIndex.value + 1;
|
||||||
|
await loadNoteChunk(selectedFile.value.id, nextChunkIndex);
|
||||||
|
};
|
||||||
|
|
||||||
const editNote = (file) => {
|
const editNote = (file) => {
|
||||||
editData.value = { ...file };
|
editData.value = { ...file };
|
||||||
showEditor.value = true;
|
showEditor.value = true;
|
||||||
@@ -474,6 +556,7 @@ const handleSearch = async () => {
|
|||||||
try {
|
try {
|
||||||
groupMarkdownFiles.value = await searchMarkdown(searchKeyword.value) || [];
|
groupMarkdownFiles.value = await searchMarkdown(searchKeyword.value) || [];
|
||||||
currentPage.value = 0;
|
currentPage.value = 0;
|
||||||
|
loadCount.value = 0; // 重置加载计数
|
||||||
updateDisplayedFiles();
|
updateDisplayedFiles();
|
||||||
selectedFile.value = null;
|
selectedFile.value = null;
|
||||||
showEditor.value = false;
|
showEditor.value = false;
|
||||||
@@ -669,6 +752,10 @@ const renderMarkdown = async (file) => {
|
|||||||
img.setAttribute('loading', 'lazy');
|
img.setAttribute('loading', 'lazy');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// 添加滚动监听,用于大文件分块加载
|
||||||
|
if (hasMoreChunks.value) {
|
||||||
|
previewElement.addEventListener('scroll', handlePreviewScroll);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
lastRenderedKey = renderKey;
|
lastRenderedKey = renderKey;
|
||||||
@@ -776,4 +863,68 @@ watch(showEditor, (isEditor) => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 没有更多笔记了 - 美化样式 */
|
||||||
|
.no-more-hint {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 30px 20px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-more-hint span {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: linear-gradient(135deg, #f5f7fa 0%, #e4e7ed 100%);
|
||||||
|
border-radius: 24px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-more-hint span::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
background-color: #c0c4cc;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-more-hint span::after {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
background-color: #c0c4cc;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗黑模式适配 */
|
||||||
|
.dark-theme .no-more-hint span {
|
||||||
|
background: linear-gradient(135deg, #2c2c3d 0%, #1e1e2f 100%);
|
||||||
|
color: #606266;
|
||||||
|
border-color: #2c2c3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .no-more-hint span::before,
|
||||||
|
.dark-theme .no-more-hint span::after {
|
||||||
|
background-color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 继续滚动加载更多提示 */
|
||||||
|
.load-more-hint {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -51,6 +51,11 @@
|
|||||||
<el-icon class="loading-icon is-loading"><Loading /></el-icon>
|
<el-icon class="loading-icon is-loading"><Loading /></el-icon>
|
||||||
<span class="loading-text">正在渲染...</span>
|
<span class="loading-text">正在渲染...</span>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 大文件分块加载提示 -->
|
||||||
|
<div v-if="file.hasMoreChunks" class="chunk-loading-hint">
|
||||||
|
<el-icon class="is-loading"><Loading /></el-icon>
|
||||||
|
<span>加载更多内容...</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -64,6 +69,10 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
isMobile: Boolean,
|
isMobile: Boolean,
|
||||||
isUserLoggedIn: Boolean,
|
isUserLoggedIn: Boolean,
|
||||||
|
hasMoreChunks: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
@@ -192,4 +201,25 @@ const handleExport = (format) => {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--text-color-secondary);
|
color: var(--text-color-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 分块加载提示 */
|
||||||
|
.chunk-loading-hint {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .chunk-loading-hint {
|
||||||
|
background-color: rgba(30, 30, 47, 0.9);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user