Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 68e3d30f3f | |||
| a30514e6da | |||
| cbe014e10d | |||
| a21a813d83 |
34
app.py
34
app.py
@@ -167,22 +167,46 @@ def api_update_note(note_id):
|
|||||||
|
|
||||||
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
# 判断是否需要生成新标题
|
# 新记录第一次输入内容时,自动生成标题
|
||||||
old_content = note.get('content', '')
|
old_content = note.get('content', '')
|
||||||
need_new_title = (not note['title'] or note['title'] == '新记录' or
|
need_generate_title = (note['title'] == '新记录' and len(content) >= 20)
|
||||||
len(old_content) < 50 and len(content) >= 50)
|
|
||||||
|
|
||||||
note['content'] = content
|
note['content'] = content
|
||||||
note['updated_at'] = now
|
note['updated_at'] = now
|
||||||
|
|
||||||
# 如果内容变化较大,异步生成新标题
|
# 如果是新记录第一次输入内容,异步生成标题
|
||||||
if need_new_title and len(content) >= 20:
|
if need_generate_title:
|
||||||
threading.Thread(target=generate_title_async, args=(note_id, content)).start()
|
threading.Thread(target=generate_title_async, args=(note_id, content)).start()
|
||||||
|
|
||||||
save_notes(notes)
|
save_notes(notes)
|
||||||
|
|
||||||
return jsonify(note)
|
return jsonify(note)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/notes/<note_id>/rename', methods=['POST'])
|
||||||
|
def api_rename_note(note_id):
|
||||||
|
"""重命名笔记标题"""
|
||||||
|
data = request.get_json()
|
||||||
|
new_title = data.get('title', '').strip()
|
||||||
|
|
||||||
|
if not new_title:
|
||||||
|
return jsonify({'error': '标题不能为空'}), 400
|
||||||
|
|
||||||
|
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['title'] = new_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': new_title, 'note': note})
|
||||||
|
|
||||||
@app.route('/api/notes/<note_id>/title', methods=['POST'])
|
@app.route('/api/notes/<note_id>/title', methods=['POST'])
|
||||||
def api_regenerate_title(note_id):
|
def api_regenerate_title(note_id):
|
||||||
"""手动重新生成标题"""
|
"""手动重新生成标题"""
|
||||||
|
|||||||
@@ -15,9 +15,13 @@
|
|||||||
.fade-in { animation: fadeIn 0.3s ease; }
|
.fade-in { animation: fadeIn 0.3s ease; }
|
||||||
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||||||
.gradient-bg { background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); }
|
.gradient-bg { background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); }
|
||||||
.hidden-menu { display: none; }
|
.action-btn {
|
||||||
.note-item:hover .hidden-menu { display: flex; }
|
opacity: 0;
|
||||||
.context-menu { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); }
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
.note-item:hover .action-btn {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-100 h-screen overflow-hidden">
|
<body class="bg-gray-100 h-screen overflow-hidden">
|
||||||
@@ -69,6 +73,9 @@
|
|||||||
<p id="currentTime" class="text-sm text-gray-500"></p>
|
<p id="currentTime" class="text-sm text-gray-500"></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<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">
|
<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>
|
<i class="ri-pushpin-line mr-1"></i> <span id="pinBtnText">置顶</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -130,23 +137,37 @@
|
|||||||
container.innerHTML = notes.map(n => `
|
container.innerHTML = notes.map(n => `
|
||||||
<div class="note-item relative p-4 cursor-pointer border-b ${currentNoteId === n.id ? 'active' : ''} ${n.pinned ? 'pinned' : ''}"
|
<div class="note-item relative p-4 cursor-pointer border-b ${currentNoteId === n.id ? 'active' : ''} ${n.pinned ? 'pinned' : ''}"
|
||||||
onclick="selectNote('${n.id}')">
|
onclick="selectNote('${n.id}')">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2 pr-16">
|
||||||
${n.pinned ? '<i class="ri-pushpin-fill text-yellow-500 text-sm"></i>' : ''}
|
${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>
|
<h3 class="font-medium text-gray-800 truncate flex-1">${n.title || '新记录'}</h3>
|
||||||
</div>
|
</div>
|
||||||
${showPreview && n.preview ? `<p class="text-sm text-gray-500 truncate mt-1">${n.preview}</p>` : ''}
|
${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>
|
<p class="text-xs text-gray-400 mt-1">${n.updated_at}</p>
|
||||||
|
|
||||||
<!-- 隐藏菜单 -->
|
<!-- 操作按钮组 -->
|
||||||
<div class="hidden-menu context-menu gap-1">
|
<div class="action-btn absolute right-2 top-1/2 -translate-y-1/2">
|
||||||
<button onclick="event.stopPropagation(); togglePinItem('${n.id}')"
|
<button onclick="event.stopPropagation(); toggleMenu('${n.id}')"
|
||||||
class="p-1.5 rounded hover:bg-gray-200 text-gray-500" title="${n.pinned ? '取消置顶' : '置顶'}">
|
class="p-1.5 rounded hover:bg-gray-200 text-gray-500" title="更多操作">
|
||||||
<i class="ri-pushpin-${n.pinned ? 'fill text-yellow-500' : 'line'}"></i>
|
<i class="ri-more-fill"></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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`).join('');
|
||||||
@@ -204,6 +225,19 @@
|
|||||||
saveTimer = setTimeout(async () => {
|
saveTimer = setTimeout(async () => {
|
||||||
const content = document.getElementById('editor').value;
|
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}`, {
|
const res = await fetch(`/api/notes/${currentNoteId}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -309,6 +343,79 @@
|
|||||||
loadNotes();
|
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}`);
|
||||||
|
if (menu) {
|
||||||
|
// 先隐藏其他所有菜单
|
||||||
|
document.querySelectorAll('[id^="menu-"]').forEach(m => {
|
||||||
|
if (m.id !== `menu-${id}`) m.classList.add('hidden');
|
||||||
|
});
|
||||||
|
menu.classList.toggle('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideMenu(id) {
|
||||||
|
const menu = document.getElementById(`menu-${id}`);
|
||||||
|
if (menu) menu.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击其他地方关闭所有菜单
|
||||||
|
document.addEventListener('click', () => {
|
||||||
|
document.querySelectorAll('[id^="menu-"]').forEach(m => m.classList.add('hidden'));
|
||||||
|
});
|
||||||
|
|
||||||
// 删除当前笔记
|
// 删除当前笔记
|
||||||
async function deleteCurrentNote() {
|
async function deleteCurrentNote() {
|
||||||
if (!currentNoteId) return;
|
if (!currentNoteId) return;
|
||||||
@@ -326,6 +433,27 @@
|
|||||||
loadNotes();
|
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() {
|
function startTitlePolling() {
|
||||||
if (titleUpdateTimer) clearInterval(titleUpdateTimer);
|
if (titleUpdateTimer) clearInterval(titleUpdateTimer);
|
||||||
|
|||||||
Reference in New Issue
Block a user