Files
snippet-notes/templates/index.html

505 lines
22 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>碎片记录</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
<style>
.note-item:hover { background: #f3f4f6; }
.note-item.active { background: #e0e7ff; border-left: 3px solid #6366f1; }
.note-item.pinned { background: #fef3c7; }
.note-item.pinned.active { background: #fde68a; border-left: 3px solid #f59e0b; }
.editor-area:focus { outline: none; }
.fade-in { animation: fadeIn 0.3s ease; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
.gradient-bg { background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); }
.action-btn {
opacity: 0;
transition: opacity 0.2s;
}
.note-item:hover .action-btn {
opacity: 1;
}
/* 菜单弹出时强制显示,不被其他元素遮挡 */
.action-btn.show-menu {
opacity: 1 !important;
z-index: 100 !important;
}
.popup-menu {
z-index: 1000 !important;
}
</style>
</head>
<body class="bg-gray-100 h-screen overflow-hidden">
<div class="flex h-full">
<!-- 左侧:笔记列表 -->
<aside class="w-72 bg-white border-r flex flex-col">
<!-- 顶部:搜索和新建 -->
<div class="p-4 border-b">
<div class="relative mb-3">
<i class="ri-search-line absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
<input type="text" id="searchInput" placeholder="搜索..."
class="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-lg focus:outline-none focus:border-purple-400"
oninput="searchNotes()">
</div>
<!-- 显示模式开关 -->
<div class="flex items-center gap-2 mb-3">
<label class="flex items-center gap-1 text-sm text-gray-600 cursor-pointer">
<input type="checkbox" id="showPreview" checked onchange="loadNotes()" class="w-4 h-4 rounded">
<span>显示内容预览</span>
</label>
</div>
<button onclick="createNote()" class="w-full py-2 gradient-bg text-white rounded-lg hover:opacity-90 transition">
<i class="ri-add-line mr-1"></i> 新建记录
</button>
</div>
<!-- 笔记列表 -->
<div id="noteList" class="flex-1 overflow-auto">
<div class="p-4 text-center text-gray-400">
<i class="ri-file-text-line text-4xl mb-2"></i>
<p>暂无记录</p>
</div>
</div>
</aside>
<!-- 右侧:编辑区域 -->
<main class="flex-1 flex flex-col bg-gray-50">
<!-- 顶部工具栏 -->
<div id="toolbar" class="hidden p-4 bg-white border-b flex justify-between items-center">
<div>
<h2 id="currentTitle" class="text-lg font-semibold text-gray-800 flex items-center gap-2">
<span id="titleText"></span>
<span id="pinBadge" class="hidden px-2 py-0.5 bg-yellow-100 text-yellow-600 rounded text-xs">
<i class="ri-pushpin-line"></i> 置顶
</span>
</h2>
<p id="currentTime" class="text-sm text-gray-500"></p>
</div>
<div class="flex gap-2">
<button onclick="exportCurrentNote()" class="px-3 py-1 text-sm text-gray-600 hover:bg-gray-50 rounded-lg">
<i class="ri-download-line mr-1"></i> 导出
</button>
<button onclick="togglePin()" id="pinBtn" class="px-3 py-1 text-sm text-gray-600 hover:bg-gray-50 rounded-lg">
<i class="ri-pushpin-line mr-1"></i> <span id="pinBtnText">置顶</span>
</button>
<button onclick="regenerateTitle()" class="px-3 py-1 text-sm text-purple-600 hover:bg-purple-50 rounded-lg">
<i class="ri-magic-line mr-1"></i> 重新生成标题
</button>
<button onclick="deleteCurrentNote()" class="px-3 py-1 text-sm text-red-500 hover:bg-red-50 rounded-lg">
<i class="ri-delete-bin-line mr-1"></i> 删除
</button>
</div>
</div>
<!-- 编辑区域 -->
<div id="editorContainer" class="hidden flex-1 p-6 fade-in">
<textarea id="editor"
class="w-full h-full p-4 bg-white rounded-xl border border-gray-200 resize-none editor-area text-gray-700 leading-relaxed"
placeholder="在这里记录你的想法..."
oninput="saveContent()"></textarea>
</div>
<!-- 空状态 -->
<div id="emptyState" class="flex-1 flex items-center justify-center">
<div class="text-center text-gray-400">
<i class="ri-quill-pen-line text-6xl mb-4"></i>
<p class="text-lg">选择一个记录,或新建一个</p>
</div>
</div>
</main>
</div>
<script>
let currentNoteId = null;
let currentNotePinned = false;
let saveTimer = null;
let notes = [];
let titleUpdateTimer = null;
// 加载笔记列表
async function loadNotes() {
const keyword = document.getElementById('searchInput').value.trim();
const showPreview = document.getElementById('showPreview').checked;
const url = keyword ? `/api/search?q=${encodeURIComponent(keyword)}` : '/api/notes';
const res = await fetch(url);
notes = await res.json();
const container = document.getElementById('noteList');
if (notes.length === 0) {
container.innerHTML = `
<div class="p-4 text-center text-gray-400">
<i class="ri-file-text-line text-4xl mb-2"></i>
<p>${keyword ? '未找到相关记录' : '暂无记录'}</p>
</div>
`;
return;
}
container.innerHTML = notes.map(n => `
<div class="note-item relative p-4 cursor-pointer border-b ${currentNoteId === n.id ? 'active' : ''} ${n.pinned ? 'pinned' : ''}"
onclick="selectNote('${n.id}')">
<div class="flex items-center gap-2 pr-16">
${n.pinned ? '<i class="ri-pushpin-fill text-yellow-500 text-sm"></i>' : ''}
<h3 class="font-medium text-gray-800 truncate flex-1">${n.title || '新记录'}</h3>
</div>
${showPreview && n.preview ? `<p class="text-sm text-gray-500 truncate mt-1">${n.preview}</p>` : ''}
<p class="text-xs text-gray-400 mt-1">${n.updated_at}</p>
<!-- 操作按钮组 -->
<div class="action-btn absolute right-2 top-1/2 -translate-y-1/2">
<button onclick="event.stopPropagation(); toggleMenu('${n.id}')"
class="p-1.5 rounded hover:bg-gray-200 text-gray-500" title="更多操作">
<i class="ri-more-fill"></i>
</button>
<div id="menu-${n.id}" class="hidden absolute right-0 top-full mt-1 bg-white rounded-lg shadow-lg border z-10 min-w-[100px]">
<button onclick="event.stopPropagation(); renameTitle('${n.id}', '${n.title || '新记录'}'); hideMenu('${n.id}')"
class="w-full px-3 py-2 text-left text-sm text-gray-600 hover:bg-gray-50 flex items-center gap-2">
<i class="ri-edit-line"></i> 重命名
</button>
<button onclick="event.stopPropagation(); exportItem('${n.id}'); hideMenu('${n.id}')"
class="w-full px-3 py-2 text-left text-sm text-gray-600 hover:bg-gray-50 flex items-center gap-2">
<i class="ri-download-line"></i> 导出
</button>
<button onclick="event.stopPropagation(); togglePinItem('${n.id}'); hideMenu('${n.id}')"
class="w-full px-3 py-2 text-left text-sm text-gray-600 hover:bg-gray-50 flex items-center gap-2">
<i class="ri-pushpin-line"></i> ${n.pinned ? '取消置顶' : '置顶'}
</button>
<button onclick="event.stopPropagation(); deleteItem('${n.id}'); hideMenu('${n.id}')"
class="w-full px-3 py-2 text-left text-sm text-red-500 hover:bg-red-50 flex items-center gap-2">
<i class="ri-delete-bin-line"></i> 删除
</button>
</div>
</div>
</div>
`).join('');
}
// 创建新笔记
async function createNote() {
const res = await fetch('/api/notes', { method: 'POST' });
const note = await res.json();
currentNoteId = note.id;
currentNotePinned = false;
loadNotes();
showEditor(note);
}
// 选择笔记
async function selectNote(id) {
currentNoteId = id;
const res = await fetch(`/api/notes/${id}`);
const note = await res.json();
currentNotePinned = note.pinned || false;
showEditor(note);
loadNotes();
}
// 显示编辑器
function showEditor(note) {
document.getElementById('toolbar').classList.remove('hidden');
document.getElementById('editorContainer').classList.remove('hidden');
document.getElementById('emptyState').classList.add('hidden');
document.getElementById('titleText').textContent = note.title || '新记录';
if (note.pinned) {
document.getElementById('pinBadge').classList.remove('hidden');
document.getElementById('pinBtnText').textContent = '取消置顶';
} else {
document.getElementById('pinBadge').classList.add('hidden');
document.getElementById('pinBtnText').textContent = '置顶';
}
document.getElementById('currentTime').textContent = `创建于 ${note.created_at} · 更新于 ${note.updated_at}`;
document.getElementById('editor').value = note.content || '';
}
// 保存内容(延迟保存)
function saveContent() {
if (!currentNoteId) return;
if (saveTimer) clearTimeout(saveTimer);
saveTimer = setTimeout(async () => {
const content = document.getElementById('editor').value;
// 检查内容是否为空(只有空白字符)
if (content.trim() === '') {
const oldContent = await fetch(`/api/notes/${currentNoteId}`).then(r => r.json()).then(n => n.content || '');
// 如果原来有内容,现在变空了,需要确认
if (oldContent.trim() !== '') {
if (!confirm('内容已清空,确定要保存为空吗?\n可能发生意外清空请确认')) {
// 用户取消,恢复旧内容
document.getElementById('editor').value = oldContent;
return;
}
}
}
const res = await fetch(`/api/notes/${currentNoteId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content })
});
const note = await res.json();
// 更新标题显示
document.getElementById('titleText').textContent = note.title;
document.getElementById('currentTime').textContent = `创建于 ${note.created_at} · 更新于 ${note.updated_at}`;
// 立即刷新列表
loadNotes();
}, 500);
}
// 搜索笔记
function searchNotes() {
loadNotes();
}
// 重新生成标题
async function regenerateTitle() {
if (!currentNoteId) return;
const btn = event.target.closest('button');
btn.disabled = true;
btn.innerHTML = '<i class="ri-loader-4-line animate-spin mr-1"></i> 生成中...';
try {
const res = await fetch(`/api/notes/${currentNoteId}/title`, { method: 'POST' });
const data = await res.json();
if (data.success) {
document.getElementById('titleText').textContent = data.title;
// 立即刷新列表
loadNotes();
}
} catch (e) {
console.error(e);
}
btn.disabled = false;
btn.innerHTML = '<i class="ri-magic-line mr-1"></i> 重新生成标题';
}
// 置顶当前笔记
async function togglePin() {
if (!currentNoteId) return;
const res = await fetch(`/api/notes/${currentNoteId}/pin`, { method: 'POST' });
const data = await res.json();
if (data.success) {
currentNotePinned = data.pinned;
if (data.pinned) {
document.getElementById('pinBadge').classList.remove('hidden');
document.getElementById('pinBtnText').textContent = '取消置顶';
} else {
document.getElementById('pinBadge').classList.add('hidden');
document.getElementById('pinBtnText').textContent = '置顶';
}
loadNotes();
}
}
// 置顶列表项
async function togglePinItem(id) {
const res = await fetch(`/api/notes/${id}/pin`, { method: 'POST' });
const data = await res.json();
if (data.success) {
if (currentNoteId === id) {
currentNotePinned = data.pinned;
if (data.pinned) {
document.getElementById('pinBadge').classList.remove('hidden');
document.getElementById('pinBtnText').textContent = '取消置顶';
} else {
document.getElementById('pinBadge').classList.add('hidden');
document.getElementById('pinBtnText').textContent = '置顶';
}
}
loadNotes();
}
}
// 删除列表项
async function deleteItem(id) {
if (!confirm('确定删除这条记录?')) return;
await fetch(`/api/notes/${id}`, { method: 'DELETE' });
if (currentNoteId === id) {
currentNoteId = null;
document.getElementById('toolbar').classList.add('hidden');
document.getElementById('editorContainer').classList.add('hidden');
document.getElementById('emptyState').classList.remove('hidden');
}
loadNotes();
}
// 导出笔记
async function exportItem(id) {
const res = await fetch(`/api/notes/${id}`);
const note = await res.json();
if (!note) return;
// 创建下载内容
const content = `# ${note.title}\n\n创建时间: ${note.created_at}\n更新时间: ${note.updated_at}\n\n---\n\n${note.content}`;
// 创建Blob并下载
const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${note.title || '记录'}.md`;
a.click();
URL.revokeObjectURL(url);
}
// 重命名标题
async function renameTitle(id, currentTitle) {
const newTitle = prompt('请输入新标题:', currentTitle);
if (newTitle === null) return; // 用户取消
const trimmedTitle = newTitle.trim();
if (!trimmedTitle) {
alert('标题不能为空!');
return;
}
const res = await fetch(`/api/notes/${id}/rename`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: trimmedTitle })
});
const data = await res.json();
if (data.success) {
// 如果是当前编辑的笔记,更新标题显示
if (currentNoteId === id) {
document.getElementById('titleText').textContent = data.title;
}
loadNotes();
} else {
alert(data.error || '重命名失败');
}
}
// 显示/隐藏菜单
function toggleMenu(id) {
const menu = document.getElementById(`menu-${id}`);
const actionBtn = menu?.closest('.action-btn');
if (menu) {
// 先隐藏其他所有菜单
document.querySelectorAll('[id^="menu-"]').forEach(m => {
m.classList.remove('popup-menu');
m.classList.add('hidden');
m.closest('.action-btn')?.classList.remove('show-menu');
});
const isHidden = menu.classList.contains('hidden');
menu.classList.toggle('hidden');
if (!menu.classList.contains('hidden')) {
menu.classList.add('popup-menu');
actionBtn?.classList.add('show-menu');
}
}
}
function hideMenu(id) {
const menu = document.getElementById(`menu-${id}`);
const actionBtn = menu?.closest('.action-btn');
if (menu) {
menu.classList.add('hidden');
menu.classList.remove('popup-menu');
actionBtn?.classList.remove('show-menu');
}
}
// 点击其他地方关闭所有菜单
document.addEventListener('click', () => {
document.querySelectorAll('[id^="menu-"]').forEach(m => m.classList.add('hidden'));
});
// 删除当前笔记
async function deleteCurrentNote() {
if (!currentNoteId) return;
if (!confirm('确定删除这条记录?')) return;
await fetch(`/api/notes/${currentNoteId}`, { method: 'DELETE' });
currentNoteId = null;
document.getElementById('toolbar').classList.add('hidden');
document.getElementById('editorContainer').classList.add('hidden');
document.getElementById('emptyState').classList.remove('hidden');
loadNotes();
}
// 导出当前笔记
async function exportCurrentNote() {
if (!currentNoteId) return;
const title = document.getElementById('titleText').textContent;
const content = document.getElementById('editor').value;
const timeInfo = document.getElementById('currentTime').textContent;
// 创建下载内容
const exportContent = `# ${title}\n\n${timeInfo}\n\n---\n\n${content}`;
// 创建Blob并下载
const blob = new Blob([exportContent], { type: 'text/markdown;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${title}.md`;
a.click();
URL.revokeObjectURL(url);
}
// 定时检查标题更新(用于异步生成标题后刷新)
function startTitlePolling() {
if (titleUpdateTimer) clearInterval(titleUpdateTimer);
titleUpdateTimer = setInterval(async () => {
if (currentNoteId) {
const res = await fetch(`/api/notes/${currentNoteId}`);
const note = await res.json();
const currentTitle = document.getElementById('titleText').textContent;
if (note.title !== currentTitle && note.title !== '新记录') {
document.getElementById('titleText').textContent = note.title;
loadNotes();
}
}
}, 2000);
}
// 初始化
loadNotes();
startTitlePolling();
</script>
</body>
</html>