feat: 实现笔记编辑器的自动保存功能与UI优化

refactor: 重构用户登录注册逻辑与数据验证

fix: 修复图片上传安全漏洞与路径处理问题

perf: 优化笔记列表分页加载与滚动性能

style: 改进侧边栏菜单的视觉设计与交互体验

chore: 更新环境变量与数据库连接配置

docs: 添加用户信息视图对象的Swagger文档

test: 增加用户注册登录的输入验证测试

ci: 配置JWT密钥环境变量与安全设置

build: 调整前端构建配置与模块加载方式
This commit is contained in:
ikmkj
2026-03-02 02:01:01 +08:00
parent c9c21df0f0
commit 392cc52fd2
23 changed files with 811 additions and 282 deletions

View File

@@ -56,12 +56,19 @@
@upload-markdown="handleMarkdownUpload"
@toggle-collapse="isCollapsed = !isCollapsed"
/>
<div class="note-list-wrapper">
<div class="note-list-wrapper" ref="noteListWrapper" @scroll="handleScroll">
<NoteList
:files="groupMarkdownFiles"
:files="displayedFiles"
:is-user-logged-in="userStore.isLoggedIn"
@preview="previewFile"
/>
<div v-if="isLoadingMore" class="loading-more">
<el-icon class="is-loading"><Loading /></el-icon>
<span>加载中...</span>
</div>
<div v-else-if="hasMoreFiles && !showEditor && !selectedFile" class="load-more-trigger">
<el-button @click="loadMoreFiles" type="primary" plain>加载更多</el-button>
</div>
</div>
</div>
</el-main>
@@ -157,7 +164,7 @@ import MoveNoteDialog from './home/dialogs/MoveNoteDialog.vue';
import SystemSettingsDialog from './home/dialogs/SystemSettingsDialog.vue';
import UpdatePasswordDialog from './home/dialogs/UpdatePasswordDialog.vue';
import PrivacyDialog from './home/dialogs/PrivacyDialog.vue';
import { Plus } from "@element-plus/icons-vue";
import { Plus, Loading } from "@element-plus/icons-vue";
// Basic Setup
const userStore = useUserStore();
@@ -167,6 +174,11 @@ const router = useRouter();
const searchKeyword = ref('');
const categoryTree = ref([]);
const groupMarkdownFiles = ref([]);
const displayedFiles = ref([]);
const currentPage = ref(0);
const pageSize = ref(16);
const isLoadingMore = ref(false);
const noteListWrapper = ref(null);
const showEditor = ref(false);
const selectedFile = ref(null);
const editData = ref(null);
@@ -188,6 +200,10 @@ const showPrivacyDialog = ref(false);
const itemToRename = ref(null);
const fileToImport = ref(null);
const hasMoreFiles = computed(() => {
return displayedFiles.value.length < groupMarkdownFiles.value.length;
});
const resetEdit = () => {
editData.value = null;
};
@@ -234,11 +250,38 @@ const resetToHomeView = async () => {
showEditor.value = false;
searchKeyword.value = '';
activeMenu.value = 'all';
currentPage.value = 0;
try {
groupMarkdownFiles.value = await getRecentFiles() || [];
groupMarkdownFiles.value = await getRecentFiles(100) || [];
updateDisplayedFiles();
} catch (error) {
ElMessage.error('获取最近文件失败: ' + error.message);
groupMarkdownFiles.value = [];
displayedFiles.value = [];
}
};
const updateDisplayedFiles = () => {
const start = 0;
const end = (currentPage.value + 1) * pageSize.value;
displayedFiles.value = groupMarkdownFiles.value.slice(start, end);
};
const loadMoreFiles = () => {
if (isLoadingMore.value || !hasMoreFiles.value) return;
isLoadingMore.value = true;
setTimeout(() => {
currentPage.value++;
updateDisplayedFiles();
isLoadingMore.value = false;
}, 300);
};
const handleScroll = (e) => {
const { scrollTop, scrollHeight, clientHeight } = e.target;
if (scrollHeight - scrollTop - clientHeight < 100 && hasMoreFiles.value && !isLoadingMore.value) {
loadMoreFiles();
}
};
@@ -248,12 +291,15 @@ const handleSelectFile = async (data) => {
try {
const files = await markdownList(data.id);
groupMarkdownFiles.value = files || [];
currentPage.value = 0;
updateDisplayedFiles();
selectedFile.value = null;
showEditor.value = false;
activeMenu.value = `group-${data.id}`;
} catch (error) {
ElMessage.error('获取笔记列表失败: ' + error.message);
groupMarkdownFiles.value = [];
displayedFiles.value = [];
}
};
@@ -272,30 +318,45 @@ const handleCreateNote = (payload) => {
const handleEditorBack = (data) => {
showEditor.value = false;
previewFile(data);
if (data && data.id) {
const fileWithGrouping = {
...data,
groupingName: data.groupingName || getCategoryName(data.groupingId)
};
selectedFile.value = fileWithGrouping;
} else {
selectedFile.value = null;
resetToHomeView();
}
};
const getCategoryName = (groupId) => {
if (!groupId) return '';
const findName = (items) => {
for (const item of items) {
if (item.id === groupId) return item.grouping;
if (item.children) {
const name = findName(item.children);
if (name) return name;
}
}
return null;
};
return findName(categoryTree.value) || '';
};
const handleSaveSuccess = (updatedFile) => {
selectedFile.value = updatedFile;
// 修复:保存成功后不退出编辑页面
// showEditor.value = false;
const index = groupMarkdownFiles.value.findIndex(f => f.id === updatedFile.id);
if (index !== -1) {
groupMarkdownFiles.value[index] = updatedFile;
} else {
// 如果是新创建的笔记之前ID为null添加到列表开头
groupMarkdownFiles.value.unshift(updatedFile);
}
updateDisplayedFiles();
fetchGroupings();
// 延迟清空 editData确保所有响应式更新完成后再清理状态
// Delay clearing editData to ensure all reactive updates are complete before cleaning up the state.
setTimeout(() => {
// 修复保存成功后不清空editData保持编辑状态
// resetEdit();
}, 100); // A short delay is usually sufficient
};
const previewFile = async (file) => {
@@ -390,6 +451,8 @@ const handleSearch = async () => {
}
try {
groupMarkdownFiles.value = await searchMarkdown(searchKeyword.value) || [];
currentPage.value = 0;
updateDisplayedFiles();
selectedFile.value = null;
showEditor.value = false;
activeMenu.value = 'search';
@@ -569,4 +632,19 @@ watch([selectedFile, showEditor], ([newFile, newShowEditor]) => {
height: 56px;
box-shadow: 0 4px 12px rgba(0,0,0,.15);
}
.loading-more {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
gap: 8px;
color: var(--text-color-secondary);
}
.load-more-trigger {
display: flex;
justify-content: center;
padding: 20px;
}
</style>