Files
snippet-notes/templates/index.html
hubian 0e84111ffe feat: 增加置顶功能和列表显示开关
- 标题生成后左侧列表实时刷新(2秒轮询)
- 增加显示内容预览开关
- 增加置顶/取消置顶功能
- 列表项hover显示隐藏菜单(置顶、删除)
2026-04-08 18:34:13 +08:00

353 lines
15 KiB
HTML

<!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%); }
.hidden-menu { display: none; }
.note-item:hover .hidden-menu { display: flex; }
.context-menu { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); }
</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="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">
${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="hidden-menu context-menu gap-1">
<button onclick="event.stopPropagation(); togglePinItem('${n.id}')"
class="p-1.5 rounded hover:bg-gray-200 text-gray-500" title="${n.pinned ? '取消置顶' : '置顶'}">
<i class="ri-pushpin-${n.pinned ? 'fill text-yellow-500' : 'line'}"></i>
</button>
<button onclick="event.stopPropagation(); deleteItem('${n.id}')"
class="p-1.5 rounded hover:bg-red-100 text-red-500" title="删除">
<i class="ri-delete-bin-line"></i>
</button>
</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;
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 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();
}
// 定时检查标题更新(用于异步生成标题后刷新)
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>