Files
yunzerwebsiteallinone/backend/src/views/tools/notebook/index.vue
T
2026-07-16 00:35:50 +08:00

380 lines
8.8 KiB
Vue

<template>
<div class="notebook-container">
<div class="notebook-sidebar">
<div class="sidebar-header">
<h3><i class="fa-solid fa-book"></i> 我的笔记</h3>
<el-button type="primary" size="small" @click="handleCreate">
<el-icon><Plus /></el-icon>
新建笔记
</el-button>
</div>
<div class="search-box">
<el-input
v-model="searchKeyword"
placeholder="搜索笔记..."
:prefix-icon="Search"
clearable
@input="handleSearch"
/>
</div>
<div class="notes-list">
<div
v-for="note in filteredNotes"
:key="note.id"
:class="['note-item', { active: currentNoteId === note.id }]"
@click="handleSelectNote(note)"
>
<div class="note-item-header">
<span class="note-title">{{ note.title || '无标题' }}</span>
<el-dropdown trigger="click" @command="(cmd) => handleNoteAction(cmd, note)">
<el-icon class="note-more"><MoreFilled /></el-icon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="delete">
<el-icon><Delete /></el-icon> 删除
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<div class="note-preview">{{ getPreviewText(note.content) }}</div>
<div class="note-time">{{ formatTime(note.update_time || note.create_time) }}</div>
</div>
<el-empty v-if="filteredNotes.length === 0" description="暂无笔记" />
</div>
</div>
<div class="notebook-editor">
<NoteEditor
v-if="currentNoteId"
:key="currentNoteId"
:note-id="currentNoteId"
@save-success="handleSaveSuccess"
@create-success="handleCreateSuccess"
/>
<div v-else class="empty-editor">
<el-empty description="请选择或创建一个笔记" />
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Plus, Search, Delete, MoreFilled } from '@element-plus/icons-vue';
import NoteEditor from './components/edit.vue';
import { getNotebookList, deleteNotebook } from '@/api/notebook';
const searchKeyword = ref('');
const currentNoteId = ref(null);
const notes = ref([]);
const loading = ref(false);
const total = ref(0);
// 过滤后的笔记列表
const filteredNotes = computed(() => {
return notes.value;
});
// 获取预览文本
const getPreviewText = (html) => {
if (!html) return '暂无内容';
const text = html.replace(/<[^>]+>/g, '').trim();
return text.substring(0, 60) + (text.length > 60 ? '...' : '');
};
// 格式化时间
const formatTime = (dateStr) => {
if (!dateStr) return '';
const date = new Date(dateStr);
const now = new Date();
const diff = now - date;
const minute = 60 * 1000;
const hour = 60 * minute;
const day = 24 * hour;
if (diff < minute) {
return '刚刚';
} else if (diff < hour) {
return `${Math.floor(diff / minute)} 分钟前`;
} else if (diff < day) {
return `${Math.floor(diff / hour)} 小时前`;
} else if (diff < 7 * day) {
return `${Math.floor(diff / day)} 天前`;
} else {
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
});
}
};
// 加载笔记列表
const loadNotes = async () => {
loading.value = true;
try {
const response = await getNotebookList({
page: 1,
pageSize: 100,
keyword: searchKeyword.value,
});
if (response.code === 200) {
notes.value = response.data.list || [];
total.value = response.data.total || 0;
// 如果有笔记且没有选中任何笔记,默认选中第一个
if (notes.value.length > 0 && !currentNoteId.value) {
currentNoteId.value = notes.value[0].id;
}
// 如果当前选中的笔记已被删除,选中第一个
if (currentNoteId.value && !notes.value.find(n => n.id === currentNoteId.value)) {
currentNoteId.value = notes.value[0]?.id || null;
}
}
} catch (error) {
console.error('加载笔记失败:', error);
ElMessage.error('加载笔记失败');
} finally {
loading.value = false;
}
};
// 创建新笔记
const handleCreate = () => {
currentNoteId.value = 'new';
};
// 选择笔记
const handleSelectNote = (note) => {
currentNoteId.value = note.id;
};
// 搜索笔记
const handleSearch = () => {
loadNotes();
};
// 笔记操作
const handleNoteAction = async (command, note) => {
if (command === 'delete') {
try {
await ElMessageBox.confirm('确定要删除这条笔记吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await deleteNotebook(note.id);
ElMessage.success('删除成功');
// 重新加载列表
await loadNotes();
// 如果删除的是当前笔记,切换到第一个
if (currentNoteId.value === note.id) {
currentNoteId.value = notes.value[0]?.id || null;
}
} catch (error) {
if (error !== 'cancel') {
console.error('删除失败:', error);
ElMessage.error('删除失败');
}
}
}
};
// 保存成功后刷新列表
const handleSaveSuccess = async () => {
await loadNotes();
};
// 创建成功后的回调
const handleCreateSuccess = async (noteId) => {
currentNoteId.value = noteId;
await loadNotes();
};
onMounted(() => {
loadNotes();
});
</script>
<style scoped lang="less">
.notebook-container {
display: flex;
height: calc(100vh - 180px);
gap: 16px;
background: var(--el-bg-color);
border-radius: 8px;
overflow: hidden;
}
.notebook-sidebar {
width: 300px;
display: flex;
flex-direction: column;
background: var(--el-bg-color-overlay);
border-radius: 8px;
border: 1px solid var(--el-border-color-lighter);
overflow: hidden;
.sidebar-header {
padding: 16px;
border-bottom: 1px solid var(--el-border-color-lighter);
display: flex;
align-items: center;
justify-content: space-between;
background: var(--el-fill-color-light);
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
i {
margin-right: 8px;
color: var(--el-color-primary);
}
}
}
.search-box {
padding: 12px 16px;
border-bottom: 1px solid var(--el-border-color-lighter);
}
.notes-list {
flex: 1;
overflow-y: auto;
padding: 8px;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: var(--el-border-color);
border-radius: 3px;
&:hover {
background: var(--el-border-color-darker);
}
}
}
}
.note-item {
padding: 12px;
margin-bottom: 8px;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
border: 1px solid transparent;
background: var(--el-bg-color);
&:hover {
background: var(--el-fill-color-light);
border-color: var(--el-border-color);
}
&.active {
background: var(--el-color-primary-light-9);
border-color: var(--el-color-primary);
.note-title {
color: var(--el-color-primary);
font-weight: 600;
}
}
.note-item-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
.note-title {
flex: 1;
font-size: 14px;
font-weight: 500;
color: var(--el-text-color-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.note-more {
margin-left: 8px;
color: var(--el-text-color-secondary);
cursor: pointer;
padding: 4px;
border-radius: 4px;
&:hover {
background: var(--el-fill-color);
color: var(--el-text-color-primary);
}
}
}
.note-preview {
font-size: 13px;
color: var(--el-text-color-secondary);
line-height: 1.5;
margin-bottom: 8px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.note-time {
font-size: 12px;
color: var(--el-text-color-placeholder);
}
}
.notebook-editor {
flex: 1;
background: var(--el-bg-color-overlay);
border-radius: 8px;
border: 1px solid var(--el-border-color-lighter);
overflow: hidden;
.empty-editor {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
}
// 响应式设计
@media (max-width: 768px) {
.notebook-container {
flex-direction: column;
height: auto;
}
.notebook-sidebar {
width: 100%;
height: 300px;
}
.notebook-editor {
height: 500px;
}
}
</style>