Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e63db4424 | |||
| 0335937312 | |||
| 6a0f8d7196 | |||
| 407a63f3cf |
@@ -338,6 +338,105 @@ def reopen_item(item_id):
|
||||
return jsonify({'success': True, 'data': item})
|
||||
|
||||
|
||||
# ============ Todo Events 待办事务API ============
|
||||
|
||||
@app.route('/api/items/<int:item_id>/todo-events', methods=['GET'])
|
||||
def list_todo_events(item_id):
|
||||
"""列出待办事务"""
|
||||
limit = int(request.args.get('limit', 10))
|
||||
offset = int(request.args.get('offset', 0))
|
||||
|
||||
events = db.list_todo_events(item_id, limit=limit, offset=offset)
|
||||
total = db.count_todo_events(item_id)
|
||||
|
||||
return jsonify({'success': True, 'data': events, 'total': total})
|
||||
|
||||
|
||||
@app.route('/api/items/<int:item_id>/todo-events', methods=['POST'])
|
||||
def create_todo_event(item_id):
|
||||
"""创建待办事务"""
|
||||
data = request.get_json()
|
||||
|
||||
if not data or not data.get('content'):
|
||||
return jsonify({'success': False, 'error': '缺少事务内容'}), 400
|
||||
|
||||
# 检查item是否存在且是text类型
|
||||
item = db.get_item(item_id)
|
||||
if not item:
|
||||
return jsonify({'success': False, 'error': '条目不存在'}), 404
|
||||
|
||||
if item['type'] != 'text':
|
||||
return jsonify({'success': False, 'error': '只有文本类型可添加待办事务'}), 400
|
||||
|
||||
remaining_days = data.get('remaining_days', 1)
|
||||
if remaining_days < 0:
|
||||
remaining_days = 1
|
||||
|
||||
event_id = db.create_todo_event(item_id, data['content'], remaining_days)
|
||||
event = db.get_todo_event(event_id)
|
||||
|
||||
return jsonify({'success': True, 'data': event}), 201
|
||||
|
||||
|
||||
@app.route('/api/todo-events/<int:event_id>', methods=['GET'])
|
||||
def get_todo_event(event_id):
|
||||
"""获取单个待办事务"""
|
||||
event = db.get_todo_event(event_id)
|
||||
if not event:
|
||||
return jsonify({'success': False, 'error': '事务不存在'}), 404
|
||||
|
||||
return jsonify({'success': True, 'data': event})
|
||||
|
||||
|
||||
@app.route('/api/todo-events/<int:event_id>', methods=['PUT'])
|
||||
def update_todo_event(event_id):
|
||||
"""更新待办事务"""
|
||||
data = request.get_json()
|
||||
|
||||
if not data:
|
||||
return jsonify({'success': False, 'error': '无数据'}), 400
|
||||
|
||||
event = db.get_todo_event(event_id)
|
||||
if not event:
|
||||
return jsonify({'success': False, 'error': '事务不存在'}), 404
|
||||
|
||||
content = data.get('content')
|
||||
remaining_days = data.get('remaining_days')
|
||||
is_completed = data.get('is_completed')
|
||||
|
||||
if is_completed is not None:
|
||||
is_completed = bool(is_completed)
|
||||
|
||||
db.update_todo_event(event_id, content=content, remaining_days=remaining_days, is_completed=is_completed)
|
||||
event = db.get_todo_event(event_id)
|
||||
|
||||
return jsonify({'success': True, 'data': event})
|
||||
|
||||
|
||||
@app.route('/api/todo-events/<int:event_id>', methods=['DELETE'])
|
||||
def delete_todo_event(event_id):
|
||||
"""删除待办事务"""
|
||||
event = db.get_todo_event(event_id)
|
||||
if not event:
|
||||
return jsonify({'success': False, 'error': '事务不存在'}), 404
|
||||
|
||||
db.delete_todo_event(event_id)
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
@app.route('/api/todo-events/<int:event_id>/complete', methods=['POST'])
|
||||
def complete_todo_event(event_id):
|
||||
"""完成待办事务"""
|
||||
event = db.get_todo_event(event_id)
|
||||
if not event:
|
||||
return jsonify({'success': False, 'error': '事务不存在'}), 404
|
||||
|
||||
db.update_todo_event(event_id, is_completed=True)
|
||||
event = db.get_todo_event(event_id)
|
||||
|
||||
return jsonify({'success': True, 'data': event})
|
||||
|
||||
|
||||
@app.route('/api/items/<int:item_id>/convert', methods=['POST'])
|
||||
def convert_item(item_id):
|
||||
"""将收藏转换为待办"""
|
||||
@@ -1084,6 +1183,14 @@ INDEX_TEMPLATE = '''
|
||||
.markdown-content pre { font-size: 0.85em; overflow-x: auto; }
|
||||
.markdown-content ul, .markdown-content ol { margin-bottom: 0.5em; }
|
||||
.markdown-content a { color: #0d6efd; }
|
||||
/* 快速插入区域样式 */
|
||||
.quick-insert-area:hover {
|
||||
background: #f0f8ff;
|
||||
border-color: #0d6efd;
|
||||
}
|
||||
.quick-insert-area:active {
|
||||
background: #e8f4ff;
|
||||
}
|
||||
.star-btn { font-size: 11px; }
|
||||
.status-pending { color: #ffc107; }
|
||||
.status-in_progress { color: #17a2b8; }
|
||||
@@ -1789,6 +1896,93 @@ INDEX_TEMPLATE = '''
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 快速插入模态框 -->
|
||||
<div class="modal fade" id="quickInsertModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-plus-circle"></i> 快速插入内容</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="quickInsertItemId">
|
||||
<input type="hidden" id="quickInsertField">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">插入位置</label>
|
||||
<select id="quickInsertPosition" class="form-select">
|
||||
<option value="start">开头</option>
|
||||
<option value="end" selected>末尾</option>
|
||||
<option value="selection_before">选中内容之前</option>
|
||||
<option value="selection_after">选中内容之后</option>
|
||||
<option value="selection_replace">替换选中内容</option>
|
||||
</select>
|
||||
<input type="hidden" id="quickInsertSelection">
|
||||
<div id="quickInsertSelectionInfo" class="form-text" style="display:none;">
|
||||
选中内容: <code id="quickInsertSelectionPreview"></code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">要插入的内容</label>
|
||||
<textarea id="quickInsertContent" class="form-control" rows="5" placeholder="输入要插入的内容..."></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">插入格式</label>
|
||||
<select id="quickInsertFormat" class="form-select">
|
||||
<option value="raw">原文本</option>
|
||||
<option value="newline">独立一行</option>
|
||||
<option value="bullet">列表项 (- item)</option>
|
||||
<option value="heading4">标题 ####</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-primary" onclick="executeQuickInsert()">
|
||||
<i class="bi bi-check"></i> 插入并保存
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 待办事务模态框 -->
|
||||
<div class="modal fade" id="todoEventModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-list-check"></i> <span id="todoEventModalTitle">添加待办事务</span></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="todoEventId">
|
||||
<input type="hidden" id="todoEventItemId">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">事务内容</label>
|
||||
<textarea id="todoEventContent" class="form-control" rows="3" placeholder="输入事务内容..."></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">剩余天数</label>
|
||||
<input type="number" id="todoEventDays" class="form-control" value="1" min="0">
|
||||
<small class="text-muted">0表示已过期</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">完成状态</label>
|
||||
<select id="todoEventCompleted" class="form-select">
|
||||
<option value="0">未完成</option>
|
||||
<option value="1">已完成</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveTodoEvent()">
|
||||
<i class="bi bi-check"></i> 保存
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 移动到文件夹模态框 -->
|
||||
<div class="modal fade" id="moveToFolderModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
@@ -2652,11 +2846,19 @@ async function deleteItem(id) {
|
||||
let currentDetailId = null;
|
||||
|
||||
// 显示详情
|
||||
async function showDetail(id) {
|
||||
async function showDetail(id, todoPage = 1, todoFilter = 'all') {
|
||||
currentDetailId = id;
|
||||
currentTodoPage = todoPage;
|
||||
currentTodoFilter = todoFilter;
|
||||
|
||||
// 增加阅读数
|
||||
await fetch(`${API_BASE}/items/${id}/view`, { method: 'POST' });
|
||||
// 记录当前弹窗滚动位置
|
||||
const detailContent = document.getElementById('detailContent');
|
||||
const scrollTop = detailContent.scrollTop;
|
||||
|
||||
// 增加阅读数(首次加载时才增加)
|
||||
if (todoPage === 1 && todoFilter === 'all') {
|
||||
await fetch(`${API_BASE}/items/${id}/view`, { method: 'POST' });
|
||||
}
|
||||
|
||||
const res = await fetch(`${API_BASE}/items/${id}`);
|
||||
const data = await res.json();
|
||||
@@ -2691,7 +2893,19 @@ async function showDetail(id) {
|
||||
}
|
||||
|
||||
if (item.content) {
|
||||
html += `<div class="mb-3"><strong>内容:</strong><br><div class="border rounded p-3 bg-light markdown-content">${renderMarkdown(item.content)}</div></div>`;
|
||||
html += `<div class="mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<strong>内容:</strong>
|
||||
<small class="text-muted"><i class="bi bi-hand-index"></i> 可先选中部分文本,再双击在此处插入</small>
|
||||
</div>
|
||||
<div class="border rounded p-3 bg-light markdown-content quick-insert-area"
|
||||
data-field="content"
|
||||
data-item-id="${item.id}"
|
||||
ondblclick="showQuickInsert(${item.id}, 'content')"
|
||||
style="cursor: pointer;">
|
||||
${renderMarkdown(item.content)}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (item.source) {
|
||||
@@ -2706,12 +2920,118 @@ async function showDetail(id) {
|
||||
}
|
||||
}
|
||||
|
||||
// 文本类型显示待办事务
|
||||
if (item.type === 'text') {
|
||||
const todoOffset = (todoPage - 1) * 10;
|
||||
|
||||
// 根据过滤状态获取数据
|
||||
let todoRes = await fetch(`${API_BASE}/items/${id}/todo-events?limit=10&offset=${todoOffset}`);
|
||||
let todoData = await todoRes.json();
|
||||
|
||||
// 客户端过滤(因为API返回全部数据)
|
||||
let filteredEvents = todoData.data;
|
||||
let totalTodo = todoData.total;
|
||||
|
||||
if (todoFilter === 'completed') {
|
||||
filteredEvents = filteredEvents.filter(e => e.is_completed);
|
||||
// 需要重新计算总数
|
||||
const allRes = await fetch(`${API_BASE}/items/${id}/todo-events?limit=1000&offset=0`);
|
||||
const allData = await allRes.json();
|
||||
totalTodo = allData.data.filter(e => e.is_completed).length;
|
||||
} else if (todoFilter === 'pending') {
|
||||
filteredEvents = filteredEvents.filter(e => !e.is_completed);
|
||||
const allRes = await fetch(`${API_BASE}/items/${id}/todo-events?limit=1000&offset=0`);
|
||||
const allData = await allRes.json();
|
||||
totalTodo = allData.data.filter(e => !e.is_completed).length;
|
||||
}
|
||||
|
||||
html += `<hr><div class="mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<strong><i class="bi bi-list-check"></i> 待办事务</strong>
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="showAddTodoEventModal(${item.id})">
|
||||
<i class="bi bi-plus"></i> 添加事务
|
||||
</button>
|
||||
</div>
|
||||
<!-- 过滤按钮 -->
|
||||
<div class="btn-group btn-group-sm mb-2" role="group">
|
||||
<button class="btn ${todoFilter === 'all' ? 'btn-primary' : 'btn-outline-secondary'}" onclick="showDetail(${id}, 1, 'all')">全部</button>
|
||||
<button class="btn ${todoFilter === 'pending' ? 'btn-warning' : 'btn-outline-warning'}" onclick="showDetail(${id}, 1, 'pending')">未完成</button>
|
||||
<button class="btn ${todoFilter === 'completed' ? 'btn-success' : 'btn-outline-success'}" onclick="showDetail(${id}, 1, 'completed')">已完成</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
if (filteredEvents.length > 0) {
|
||||
html += `<div class="border rounded p-2 bg-light" id="todoEventsList">`;
|
||||
filteredEvents.forEach(event => {
|
||||
const completedClass = event.is_completed ? 'text-muted' : '';
|
||||
const checkIcon = event.is_completed ? '✅' : '⏳';
|
||||
const remainingText = event.remaining_days > 0 ? `${event.remaining_days}天` : '已过期';
|
||||
const remainingClass = event.remaining_days <= 0 ? 'text-danger' : 'text-info';
|
||||
|
||||
html += `<div class="d-flex justify-content-between align-items-start py-2 border-bottom todo-event-item ${completedClass}">
|
||||
<div class="flex-grow-1">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span onclick="toggleTodoEventComplete(${event.id}, ${item.id}, ${event.is_completed})" style="cursor:pointer;" title="点击切换完成状态">${checkIcon}</span>
|
||||
<span class="${event.is_completed ? 'text-decoration-line-through' : ''}">${escapeHtml(event.content)}</span>
|
||||
<small class="${remainingClass}">(${remainingText})</small>
|
||||
</div>
|
||||
<div class="text-muted small mt-1">
|
||||
创建: ${formatShortDate(event.created_at)} | 更新: ${formatShortDate(event.updated_at)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="showEditTodoEventModal(${event.id}, ${item.id})" title="编辑">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-danger btn-sm" onclick="deleteTodoEvent(${event.id}, ${item.id})" title="删除">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
html += `</div>`;
|
||||
|
||||
// 分页
|
||||
const totalPagesTodo = Math.ceil(totalTodo / 10);
|
||||
if (totalPagesTodo > 1) {
|
||||
html += `<div class="d-flex justify-content-center mt-2">
|
||||
<nav><ul class="pagination pagination-sm">`;
|
||||
for (let p = 1; p <= Math.min(totalPagesTodo, 5); p++) {
|
||||
html += `<li class="page-item ${p === todoPage ? 'active' : ''}">
|
||||
<a class="page-link" href="#" onclick="showDetail(${id}, ${p}, '${todoFilter}'); return false;">${p}</a>
|
||||
</li>`;
|
||||
}
|
||||
if (totalPagesTodo > 5) {
|
||||
html += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
|
||||
html += `<li class="page-item"><a class="page-link" href="#" onclick="showDetail(${id}, ${totalPagesTodo}, '${todoFilter}'); return false;">${totalPagesTodo}</a></li>`;
|
||||
}
|
||||
html += `</ul></nav>
|
||||
<small class="text-muted ms-2">共 ${totalTodo} 条事务</small>
|
||||
</div>`;
|
||||
}
|
||||
} else {
|
||||
html += `<div class="text-center text-muted py-3">${todoFilter === 'all' ? '暂无待办事务' : (todoFilter === 'completed' ? '暂无已完成事务' : '暂无未完成事务')}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (item.tags.length) {
|
||||
html += `<div class="mb-3"><strong>标签:</strong> ${item.tags.map(t => `<span class="badge bg-secondary">${escapeHtml(t)}</span>`).join(' ')}</div>`;
|
||||
}
|
||||
|
||||
if (item.note) {
|
||||
html += `<div class="mb-3"><strong>详情/备注:</strong><br><div class="border rounded p-3 bg-light markdown-content">${renderMarkdown(item.note)}</div></div>`;
|
||||
html += `<div class="mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<strong>详情/备注:</strong>
|
||||
<small class="text-muted"><i class="bi bi-hand-index"></i> 可先选中部分文本,再双击在此处插入</small>
|
||||
</div>
|
||||
<div class="border rounded p-3 bg-light markdown-content quick-insert-area"
|
||||
data-field="note"
|
||||
data-item-id="${item.id}"
|
||||
ondblclick="showQuickInsert(${item.id}, 'note')"
|
||||
style="cursor: pointer;">
|
||||
${renderMarkdown(item.note)}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
html += `<div class="text-muted small"><strong>创建时间:</strong> ${formatDate(item.created_at)}<br><strong>更新时间:</strong> ${formatDate(item.updated_at)}</div>`;
|
||||
@@ -2732,7 +3052,147 @@ async function showDetail(id) {
|
||||
|
||||
document.getElementById('detailContent').innerHTML = html;
|
||||
|
||||
new bootstrap.Modal(document.getElementById('detailModal')).show();
|
||||
// 如果弹窗已经打开,保持滚动位置
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('detailModal'));
|
||||
if (modal && modal._isShown) {
|
||||
// 弹窗已打开,恢复滚动位置
|
||||
setTimeout(() => {
|
||||
document.getElementById('detailContent').scrollTop = scrollTop;
|
||||
}, 10);
|
||||
} else {
|
||||
// 弹窗未打开,打开弹窗
|
||||
new bootstrap.Modal(document.getElementById('detailModal')).show();
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 待办事务功能 ============
|
||||
|
||||
// 显示添加待办事务模态框
|
||||
function showAddTodoEventModal(itemId) {
|
||||
document.getElementById('todoEventId').value = '';
|
||||
document.getElementById('todoEventItemId').value = itemId;
|
||||
document.getElementById('todoEventContent').value = '';
|
||||
document.getElementById('todoEventDays').value = 1;
|
||||
document.getElementById('todoEventCompleted').value = 0;
|
||||
document.getElementById('todoEventModalTitle').textContent = '添加待办事务';
|
||||
|
||||
new bootstrap.Modal(document.getElementById('todoEventModal')).show();
|
||||
}
|
||||
|
||||
// 显示编辑待办事务模态框
|
||||
async function showEditTodoEventModal(eventId, itemId) {
|
||||
const res = await fetch(`${API_BASE}/todo-events/${eventId}`);
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.success) {
|
||||
alert('获取事务失败');
|
||||
return;
|
||||
}
|
||||
|
||||
const event = data.data;
|
||||
document.getElementById('todoEventId').value = eventId;
|
||||
document.getElementById('todoEventItemId').value = itemId;
|
||||
document.getElementById('todoEventContent').value = event.content;
|
||||
document.getElementById('todoEventDays').value = event.remaining_days;
|
||||
document.getElementById('todoEventCompleted').value = event.is_completed;
|
||||
document.getElementById('todoEventModalTitle').textContent = '编辑待办事务';
|
||||
|
||||
new bootstrap.Modal(document.getElementById('todoEventModal')).show();
|
||||
}
|
||||
|
||||
// 保存待办事务
|
||||
async function saveTodoEvent() {
|
||||
const eventId = document.getElementById('todoEventId').value;
|
||||
const itemId = document.getElementById('todoEventItemId').value;
|
||||
const content = document.getElementById('todoEventContent').value.trim();
|
||||
const days = parseInt(document.getElementById('todoEventDays').value) || 1;
|
||||
const completed = document.getElementById('todoEventCompleted').value === '1';
|
||||
|
||||
if (!content) {
|
||||
alert('请输入事务内容');
|
||||
return;
|
||||
}
|
||||
|
||||
// 离线检查
|
||||
if (!checkOnlineBeforeAction('保存待办事务')) return;
|
||||
|
||||
if (eventId) {
|
||||
// 更新
|
||||
const res = await fetch(`${API_BASE}/todo-events/${eventId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ content, remaining_days: days, is_completed: completed })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
bootstrap.Modal.getInstance(document.getElementById('todoEventModal')).hide();
|
||||
showDetail(itemId, currentTodoPage, currentTodoFilter); // 刷新详情,保持页面和过滤状态
|
||||
} else {
|
||||
alert('更新失败: ' + data.error);
|
||||
}
|
||||
} else {
|
||||
// 创建
|
||||
const res = await fetch(`${API_BASE}/items/${itemId}/todo-events`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ content, remaining_days: days })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
bootstrap.Modal.getInstance(document.getElementById('todoEventModal')).hide();
|
||||
showDetail(itemId, 1, currentTodoFilter); // 创建新事务后回到第一页
|
||||
} else {
|
||||
alert('创建失败: ' + data.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 切换待办事务完成状态(可切换为完成或未完成)
|
||||
async function toggleTodoEventComplete(eventId, itemId, currentCompleted) {
|
||||
// 离线检查
|
||||
if (!checkOnlineBeforeAction('切换完成状态')) return;
|
||||
|
||||
// 切换状态:已完成->未完成,未完成->已完成
|
||||
const newCompleted = !currentCompleted;
|
||||
|
||||
const res = await fetch(`${API_BASE}/todo-events/${eventId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ is_completed: newCompleted })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
// 保持当前页面和过滤状态
|
||||
showDetail(itemId, currentTodoPage, currentTodoFilter);
|
||||
} else {
|
||||
alert('操作失败: ' + data.error);
|
||||
}
|
||||
}
|
||||
|
||||
// 当前待办事务的页面和过滤状态
|
||||
let currentTodoPage = 1;
|
||||
let currentTodoFilter = 'all'; // all, completed, pending
|
||||
|
||||
// 删除待办事务
|
||||
async function deleteTodoEvent(eventId, itemId) {
|
||||
// 离线检查
|
||||
if (!checkOnlineBeforeAction('删除待办事务')) return;
|
||||
|
||||
if (!confirm('确认删除这条待办事务?')) return;
|
||||
|
||||
const res = await fetch(`${API_BASE}/todo-events/${eventId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
showDetail(itemId, currentTodoPage, currentTodoFilter); // 刷新详情,保持页面和过滤状态
|
||||
} else {
|
||||
alert('删除失败: ' + data.error);
|
||||
}
|
||||
}
|
||||
|
||||
// 从详情页打开转换
|
||||
@@ -3050,7 +3510,7 @@ function renderMarkdown(text) {
|
||||
let html = escapeHtml(text);
|
||||
|
||||
// 代码块 ```code```
|
||||
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre class="bg-dark text-light p-2 rounded"><code>$2</code></pre>');
|
||||
html = html.replace(/```(\\w*)\\n([\\s\\S]*?)```/g, '<pre class="bg-dark text-light p-2 rounded"><code>$2</code></pre>');
|
||||
|
||||
// 行内代码 `code`
|
||||
html = html.replace(/`([^`]+)`/g, '<code class="bg-light px-1 rounded">$1</code>');
|
||||
@@ -3072,16 +3532,16 @@ function renderMarkdown(text) {
|
||||
|
||||
// 无序列表 - item
|
||||
html = html.replace(/^- (.+)$/gm, '<li class="ms-3">$1</li>');
|
||||
html = html.replace(/(<li.*<\/li>\n?)+/g, '<ul class="mb-2">$&</ul>');
|
||||
html = html.replace(/(<li.*<\\/li>\\n?)+/g, '<ul class="mb-2">$&</ul>');
|
||||
|
||||
// 有序列表 1. item
|
||||
html = html.replace(/^\d+\. (.+)$/gm, '<li class="ms-3">$1</li>');
|
||||
html = html.replace(/^\\d+\\. (.+)$/gm, '<li class="ms-3">$1</li>');
|
||||
|
||||
// 段落分隔(两个换行)
|
||||
html = html.replace(/\n\n/g, '</p><p class="mb-2">');
|
||||
html = html.replace(/\\n\\n/g, '</p><p class="mb-2">');
|
||||
|
||||
// 单换行
|
||||
html = html.replace(/\n/g, '<br>');
|
||||
html = html.replace(/\\n/g, '<br>');
|
||||
|
||||
// 包装在段落中
|
||||
if (!html.startsWith('<')) {
|
||||
@@ -3546,6 +4006,137 @@ async function emptyTrash() {
|
||||
|
||||
// ============ Folder 文件夹管理 ============
|
||||
|
||||
// ============ 快速插入功能 ============
|
||||
|
||||
// 保存当前详情数据,用于快速插入
|
||||
let currentDetailItem = null;
|
||||
|
||||
// 显示快速插入模态框
|
||||
function showQuickInsert(itemId, field) {
|
||||
document.getElementById('quickInsertItemId').value = itemId;
|
||||
document.getElementById('quickInsertField').value = field;
|
||||
document.getElementById('quickInsertContent').value = '';
|
||||
document.getElementById('quickInsertPosition').value = 'end';
|
||||
document.getElementById('quickInsertFormat').value = 'newline';
|
||||
|
||||
// 获取当前选中的文本
|
||||
const selection = window.getSelection();
|
||||
const selectedText = selection.toString().trim();
|
||||
|
||||
if (selectedText) {
|
||||
document.getElementById('quickInsertSelection').value = selectedText;
|
||||
document.getElementById('quickInsertSelectionInfo').style.display = 'block';
|
||||
document.getElementById('quickInsertSelectionPreview').textContent = selectedText.substring(0, 50) + (selectedText.length > 50 ? '...' : '');
|
||||
document.getElementById('quickInsertPosition').value = 'selection_after'; // 默认在选中内容之后插入
|
||||
} else {
|
||||
document.getElementById('quickInsertSelection').value = '';
|
||||
document.getElementById('quickInsertSelectionInfo').style.display = 'none';
|
||||
}
|
||||
|
||||
new bootstrap.Modal(document.getElementById('quickInsertModal')).show();
|
||||
}
|
||||
|
||||
// 执行快速插入
|
||||
async function executeQuickInsert() {
|
||||
const itemId = document.getElementById('quickInsertItemId').value;
|
||||
const field = document.getElementById('quickInsertField').value;
|
||||
const insertContent = document.getElementById('quickInsertContent').value.trim();
|
||||
const position = document.getElementById('quickInsertPosition').value;
|
||||
const format = document.getElementById('quickInsertFormat').value;
|
||||
const selectedText = document.getElementById('quickInsertSelection').value;
|
||||
|
||||
if (!insertContent) {
|
||||
alert('请输入要插入的内容');
|
||||
return;
|
||||
}
|
||||
|
||||
// 离线检查
|
||||
if (!checkOnlineBeforeAction('快速插入')) return;
|
||||
|
||||
// 获取当前数据
|
||||
const res = await fetch(`${API_BASE}/items/${itemId}`);
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.success) {
|
||||
alert('获取数据失败');
|
||||
return;
|
||||
}
|
||||
|
||||
const item = data.data;
|
||||
let originalContent = item[field] || '';
|
||||
|
||||
// 根据格式处理插入内容
|
||||
let formattedContent = insertContent;
|
||||
switch (format) {
|
||||
case 'newline':
|
||||
formattedContent = '\\n' + insertContent;
|
||||
break;
|
||||
case 'bullet':
|
||||
formattedContent = '\\n- ' + insertContent;
|
||||
break;
|
||||
case 'heading4':
|
||||
formattedContent = '\\n#### ' + insertContent;
|
||||
break;
|
||||
default:
|
||||
formattedContent = insertContent;
|
||||
}
|
||||
|
||||
// 根据位置插入
|
||||
let newContent;
|
||||
|
||||
if (position === 'start') {
|
||||
newContent = formattedContent + originalContent;
|
||||
} else if (position === 'end') {
|
||||
newContent = originalContent + formattedContent;
|
||||
} else if (position.startsWith('selection_') && selectedText) {
|
||||
// 在选中内容位置插入
|
||||
const selectionIndex = originalContent.indexOf(selectedText);
|
||||
if (selectionIndex === -1) {
|
||||
alert('未在原内容中找到选中的文本,可能是因为Markdown渲染后的文本与原文本不完全一致。\\n请尝试手动编辑。');
|
||||
return;
|
||||
}
|
||||
|
||||
const before = originalContent.substring(0, selectionIndex);
|
||||
const selected = originalContent.substring(selectionIndex, selectionIndex + selectedText.length);
|
||||
const after = originalContent.substring(selectionIndex + selectedText.length);
|
||||
|
||||
if (position === 'selection_before') {
|
||||
newContent = before + formattedContent + selected + after;
|
||||
} else if (position === 'selection_after') {
|
||||
newContent = before + selected + formattedContent + after;
|
||||
} else if (position === 'selection_replace') {
|
||||
newContent = before + formattedContent + after;
|
||||
}
|
||||
} else {
|
||||
// 没有选中内容但选择了选中位置选项,默认末尾
|
||||
newContent = originalContent + formattedContent;
|
||||
}
|
||||
|
||||
// 保存更新
|
||||
const updateData = {};
|
||||
updateData[field] = newContent;
|
||||
|
||||
const updateRes = await fetch(`${API_BASE}/items/${itemId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(updateData)
|
||||
});
|
||||
|
||||
const updateResult = await updateRes.json();
|
||||
|
||||
if (updateResult.success) {
|
||||
bootstrap.Modal.getInstance(document.getElementById('quickInsertModal')).hide();
|
||||
|
||||
// 刷新详情显示
|
||||
showDetail(itemId);
|
||||
|
||||
// 刷新列表
|
||||
refreshData();
|
||||
} else {
|
||||
alert('保存失败: ' + updateResult.error);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFolders() {
|
||||
const res = await fetch(`${API_BASE}/folders`);
|
||||
const data = await res.json();
|
||||
|
||||
103
xian_favor/db.py
103
xian_favor/db.py
@@ -187,6 +187,22 @@ class Database:
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_folders_type ON folders(type)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_folders_parent ON folders(parent_id)")
|
||||
|
||||
# 待办事务表(关联到文本类别的items)
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS todo_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
item_id INTEGER NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
remaining_days INTEGER DEFAULT 1,
|
||||
is_completed INTEGER DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE
|
||||
)
|
||||
""")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_todo_events_item ON todo_events(item_id)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_todo_events_created ON todo_events(created_at)")
|
||||
|
||||
conn.commit()
|
||||
|
||||
# ============ Item 操作 ============
|
||||
@@ -1166,6 +1182,93 @@ class Database:
|
||||
# 删除
|
||||
self.delete_backup(backup['name'])
|
||||
|
||||
# ============ Todo Events 待办事务操作 ============
|
||||
|
||||
def create_todo_event(self, item_id: int, content: str, remaining_days: int = 1) -> int:
|
||||
"""创建待办事务"""
|
||||
self._ensure_init()
|
||||
now = datetime.now().isoformat()
|
||||
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
INSERT INTO todo_events (item_id, content, remaining_days, is_completed, created_at, updated_at)
|
||||
VALUES (?, ?, ?, 0, ?, ?)
|
||||
""", (item_id, content, remaining_days, now, now))
|
||||
conn.commit()
|
||||
return cursor.lastrowid
|
||||
|
||||
def list_todo_events(self, item_id: int, limit: int = 10, offset: int = 0) -> List[Dict[str, Any]]:
|
||||
"""列出待办事务(按时间倒序)"""
|
||||
self._ensure_init()
|
||||
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT * FROM todo_events
|
||||
WHERE item_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
""", (item_id, limit, offset))
|
||||
rows = cursor.fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
def count_todo_events(self, item_id: int) -> int:
|
||||
"""计算待办事务总数"""
|
||||
self._ensure_init()
|
||||
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT COUNT(*) as count FROM todo_events WHERE item_id = ?", (item_id,))
|
||||
return cursor.fetchone()['count']
|
||||
|
||||
def get_todo_event(self, event_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""获取单个待办事务"""
|
||||
self._ensure_init()
|
||||
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM todo_events WHERE id = ?", (event_id,))
|
||||
row = cursor.fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
def update_todo_event(self, event_id: int, content: str = None, remaining_days: int = None, is_completed: bool = None) -> bool:
|
||||
"""更新待办事务"""
|
||||
self._ensure_init()
|
||||
now = datetime.now().isoformat()
|
||||
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 获取当前数据
|
||||
cursor.execute("SELECT * FROM todo_events WHERE id = ?", (event_id,))
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
return False
|
||||
|
||||
# 更新字段
|
||||
new_content = content if content is not None else row['content']
|
||||
new_days = remaining_days if remaining_days is not None else row['remaining_days']
|
||||
new_completed = 1 if is_completed is True else (0 if is_completed is False else row['is_completed'])
|
||||
|
||||
cursor.execute("""
|
||||
UPDATE todo_events
|
||||
SET content = ?, remaining_days = ?, is_completed = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
""", (new_content, new_days, new_completed, now, event_id))
|
||||
conn.commit()
|
||||
return True
|
||||
|
||||
def delete_todo_event(self, event_id: int) -> bool:
|
||||
"""删除待办事务"""
|
||||
self._ensure_init()
|
||||
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM todo_events WHERE id = ?", (event_id,))
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
|
||||
|
||||
# 全局数据库实例
|
||||
db = Database()
|
||||
Reference in New Issue
Block a user