Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e84111ffe |
29
app.py
29
app.py
@@ -33,7 +33,9 @@ LLM_CONFIG = {
|
||||
def load_notes():
|
||||
"""加载所有笔记"""
|
||||
if NOTES_FILE.exists():
|
||||
return json.loads(NOTES_FILE.read_text(encoding='utf-8'))
|
||||
notes = json.loads(NOTES_FILE.read_text(encoding='utf-8'))
|
||||
# 按置顶和更新时间排序
|
||||
return sorted(notes, key=lambda x: (not x.get('pinned', False), x.get('updated_at', '')), reverse=True)
|
||||
return []
|
||||
|
||||
def save_notes(notes):
|
||||
@@ -118,6 +120,7 @@ def api_notes():
|
||||
'updated_at': n['updated_at'],
|
||||
'created_at': n['created_at'],
|
||||
'preview': n['content'][:50] if n['content'] else '',
|
||||
'pinned': n.get('pinned', False),
|
||||
} for n in notes])
|
||||
|
||||
@app.route('/api/notes/<note_id>')
|
||||
@@ -193,9 +196,11 @@ def api_regenerate_title(note_id):
|
||||
note['title'] = title
|
||||
note['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
# 重新排序
|
||||
notes = sorted(notes, key=lambda x: (not x.get('pinned', False), x.get('updated_at', '')), reverse=True)
|
||||
save_notes(notes)
|
||||
|
||||
return jsonify({'success': True, 'title': title})
|
||||
return jsonify({'success': True, 'title': title, 'note': note})
|
||||
|
||||
@app.route('/api/notes/<note_id>', methods=['DELETE'])
|
||||
def api_delete_note(note_id):
|
||||
@@ -206,6 +211,23 @@ def api_delete_note(note_id):
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
@app.route('/api/notes/<note_id>/pin', methods=['POST'])
|
||||
def api_pin_note(note_id):
|
||||
"""置顶/取消置顶笔记"""
|
||||
notes = load_notes()
|
||||
note = next((n for n in notes if n['id'] == note_id), None)
|
||||
|
||||
if not note:
|
||||
return jsonify({'error': 'Note not found'}), 404
|
||||
|
||||
note['pinned'] = not note.get('pinned', False)
|
||||
|
||||
# 重新排序
|
||||
notes = sorted(notes, key=lambda x: (not x.get('pinned', False), x.get('updated_at', '')), reverse=True)
|
||||
save_notes(notes)
|
||||
|
||||
return jsonify({'success': True, 'pinned': note['pinned'], 'note': note})
|
||||
|
||||
@app.route('/api/search')
|
||||
def api_search():
|
||||
"""搜索笔记"""
|
||||
@@ -217,13 +239,12 @@ def api_search():
|
||||
notes = load_notes()
|
||||
results = [n for n in notes if keyword in n.get('title', '').lower() or keyword in n.get('content', '').lower()]
|
||||
|
||||
results = sorted(results, key=lambda x: x.get('updated_at', ''), reverse=True)
|
||||
|
||||
return jsonify([{
|
||||
'id': n['id'],
|
||||
'title': n['title'],
|
||||
'updated_at': n['updated_at'],
|
||||
'preview': n['content'][:100] if n['content'] else '',
|
||||
'pinned': n.get('pinned', False),
|
||||
} for n in results])
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -9,10 +9,15 @@
|
||||
<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">
|
||||
@@ -27,6 +32,15 @@
|
||||
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>
|
||||
@@ -46,10 +60,18 @@
|
||||
<!-- 顶部工具栏 -->
|
||||
<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"></h2>
|
||||
<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>
|
||||
@@ -79,12 +101,15 @@
|
||||
|
||||
<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);
|
||||
@@ -103,11 +128,26 @@
|
||||
}
|
||||
|
||||
container.innerHTML = notes.map(n => `
|
||||
<div class="note-item p-4 cursor-pointer border-b ${currentNoteId === n.id ? 'active' : ''}"
|
||||
<div class="note-item relative p-4 cursor-pointer border-b ${currentNoteId === n.id ? 'active' : ''} ${n.pinned ? 'pinned' : ''}"
|
||||
onclick="selectNote('${n.id}')">
|
||||
<h3 class="font-medium text-gray-800 truncate">${n.title || '新记录'}</h3>
|
||||
<p class="text-sm text-gray-500 truncate mt-1">${n.preview || '空白记录'}</p>
|
||||
<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('');
|
||||
}
|
||||
@@ -118,6 +158,7 @@
|
||||
const note = await res.json();
|
||||
|
||||
currentNoteId = note.id;
|
||||
currentNotePinned = false;
|
||||
loadNotes();
|
||||
showEditor(note);
|
||||
}
|
||||
@@ -129,8 +170,9 @@
|
||||
const res = await fetch(`/api/notes/${id}`);
|
||||
const note = await res.json();
|
||||
|
||||
currentNotePinned = note.pinned || false;
|
||||
showEditor(note);
|
||||
loadNotes(); // 更新高亮状态
|
||||
loadNotes();
|
||||
}
|
||||
|
||||
// 显示编辑器
|
||||
@@ -139,7 +181,16 @@
|
||||
document.getElementById('editorContainer').classList.remove('hidden');
|
||||
document.getElementById('emptyState').classList.add('hidden');
|
||||
|
||||
document.getElementById('currentTitle').textContent = note.title || '新记录';
|
||||
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 || '';
|
||||
}
|
||||
@@ -148,7 +199,6 @@
|
||||
function saveContent() {
|
||||
if (!currentNoteId) return;
|
||||
|
||||
// 延迟保存,避免频繁请求
|
||||
if (saveTimer) clearTimeout(saveTimer);
|
||||
|
||||
saveTimer = setTimeout(async () => {
|
||||
@@ -162,11 +212,11 @@
|
||||
|
||||
const note = await res.json();
|
||||
|
||||
// 更新显示
|
||||
document.getElementById('currentTitle').textContent = note.title;
|
||||
// 更新标题显示
|
||||
document.getElementById('titleText').textContent = note.title;
|
||||
document.getElementById('currentTime').textContent = `创建于 ${note.created_at} · 更新于 ${note.updated_at}`;
|
||||
|
||||
// 更新列表
|
||||
// 立即刷新列表
|
||||
loadNotes();
|
||||
}, 500);
|
||||
}
|
||||
@@ -189,7 +239,8 @@
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
document.getElementById('currentTitle').textContent = data.title;
|
||||
document.getElementById('titleText').textContent = data.title;
|
||||
// 立即刷新列表
|
||||
loadNotes();
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -200,7 +251,65 @@
|
||||
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;
|
||||
|
||||
@@ -217,8 +326,28 @@
|
||||
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>
|
||||
Reference in New Issue
Block a user