feat: 收藏转待办功能 v2.0.0
- 新增 /api/items/<id>/convert API - 支持两种转换方式: - 直接转换(convert):原收藏变为待办 - 复制创建(copy):保留原收藏,新建待办 - 文本/链接/专栏类型可转换 - 内容自动合并到待办备注 - 前端列表和详情页添加转换按钮 - 弹窗配置:标题、状态、优先级、截止日期
This commit is contained in:
@@ -134,6 +134,71 @@ def reopen_item(item_id):
|
||||
return jsonify({'success': True, 'data': item})
|
||||
|
||||
|
||||
@app.route('/api/items/<int:item_id>/convert', methods=['POST'])
|
||||
def convert_item(item_id):
|
||||
"""将收藏转换为待办"""
|
||||
item = db.get_item(item_id)
|
||||
if not item:
|
||||
return jsonify({'success': False, 'error': '条目不存在'}), 404
|
||||
|
||||
# 不能转换已经是待办的
|
||||
if item['type'] == 'todo':
|
||||
return jsonify({'success': False, 'error': '已经是待办事项'}), 400
|
||||
|
||||
data = request.get_json() or {}
|
||||
mode = data.get('mode', 'convert') # convert 或 copy
|
||||
|
||||
if mode not in ['convert', 'copy']:
|
||||
return jsonify({'success': False, 'error': '无效转换模式'}), 400
|
||||
|
||||
# 构建待办数据
|
||||
todo_data = {
|
||||
'type': 'todo',
|
||||
'title': data.get('title') or item['title'] or f'{item["type"]}收藏',
|
||||
'status': data.get('status', 'pending'),
|
||||
'priority': data.get('priority', 'medium'),
|
||||
'due_date': data.get('due_date'),
|
||||
'tags': item['tags'], # 继承原标签
|
||||
}
|
||||
|
||||
# 根据原类型处理内容
|
||||
if item['type'] == 'text':
|
||||
# 文本:content 放到 note
|
||||
todo_data['note'] = item['content'] or ''
|
||||
if item['note']:
|
||||
todo_data['note'] += '\n\n' + item['note']
|
||||
elif item['type'] == 'link':
|
||||
# 链接:url 放到 note,保留链接可点击
|
||||
todo_data['note'] = f'链接: {item["url"]}\n\n'
|
||||
if item['content']:
|
||||
todo_data['note'] += item['content'] + '\n\n'
|
||||
if item['note']:
|
||||
todo_data['note'] += item['note']
|
||||
# url 字段也保留(方便后续操作)
|
||||
todo_data['content'] = item['url']
|
||||
elif item['type'] == 'column':
|
||||
# 专栏:url + source 放到 note
|
||||
todo_data['note'] = f'专栏: {item["url"]}\n'
|
||||
if item['source']:
|
||||
todo_data['note'] += f'来源: {item["source"]}\n\n'
|
||||
if item['content']:
|
||||
todo_data['note'] += item['content'] + '\n\n'
|
||||
if item['note']:
|
||||
todo_data['note'] += item['note']
|
||||
todo_data['content'] = item['url']
|
||||
|
||||
if mode == 'convert':
|
||||
# 直接转换:更新原条目
|
||||
db.update_item(item_id, **todo_data)
|
||||
result = db.get_item(item_id)
|
||||
return jsonify({'success': True, 'data': result, 'mode': 'convert'})
|
||||
else:
|
||||
# 复制创建:新建待办,原条目保留
|
||||
new_id = db.create_item(**todo_data)
|
||||
result = db.get_item(new_id)
|
||||
return jsonify({'success': True, 'data': result, 'mode': 'copy', 'original_id': item_id})
|
||||
|
||||
|
||||
@app.route('/api/tags', methods=['GET'])
|
||||
def list_tags():
|
||||
"""列出标签"""
|
||||
@@ -691,6 +756,9 @@ INDEX_TEMPLATE = '''
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" id="detailConvertBtn" onclick="showConvertModalFromDetail()" style="display:none;">
|
||||
<i class="bi bi-arrow-repeat"></i> 转为待办
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary" onclick="openEditModalFromDetail()">
|
||||
<i class="bi bi-pencil"></i> 编辑
|
||||
</button>
|
||||
@@ -940,6 +1008,80 @@ INDEX_TEMPLATE = '''
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 转换为待办模态框 -->
|
||||
<div class="modal fade" id="convertModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-arrow-repeat"></i> 转为待办</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="convertItemId">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">转换方式</label>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="convertMode" id="modeConvert" value="convert" checked>
|
||||
<label class="form-check-label" for="modeConvert">
|
||||
<strong>直接转换</strong> - 原收藏变为待办,数据合并
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="convertMode" id="modeCopy" value="copy">
|
||||
<label class="form-check-label" for="modeCopy">
|
||||
<strong>复制创建</strong> - 保留原收藏,新建待办任务
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">待办标题</label>
|
||||
<input type="text" id="convertTitle" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<label class="form-label">状态</label>
|
||||
<select id="convertStatus" class="form-select">
|
||||
<option value="pending">⏳ 待处理</option>
|
||||
<option value="in_progress">🔄 进行中</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">优先级</label>
|
||||
<select id="convertPriority" class="form-select">
|
||||
<option value="low">🟢 低</option>
|
||||
<option value="medium" selected>🟡 中</option>
|
||||
<option value="high">🟠 高</option>
|
||||
<option value="urgent">🔴 紧急</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">截止日期</label>
|
||||
<input type="date" id="convertDueDate" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted">内容预览(转换后会合并到待办备注)</label>
|
||||
<div id="convertPreview" class="border rounded p-2 bg-light" style="max-height: 150px; overflow-y: auto;">
|
||||
<!-- 动态填充 -->
|
||||
</div>
|
||||
</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="executeConvert()">
|
||||
<i class="bi bi-check"></i> 确认转换
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
const API_BASE = '/api';
|
||||
@@ -1050,6 +1192,7 @@ function renderItems(items) {
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-1 flex-wrap ms-2" onclick="event.stopPropagation();">
|
||||
${item.tags.slice(0, 2).map(t => `<span class="badge bg-secondary" style="font-size:10px;">${t}</span>`).join('')}
|
||||
${item.type !== 'todo' ? `<button class="btn btn-sm btn-outline-secondary py-0 px-1" onclick="showConvertModal(${item.id})" title="转为待办"><i class="bi bi-arrow-repeat" style="font-size:11px;"></i></button>` : ''}
|
||||
<button class="btn btn-sm btn-outline-info py-0 px-1" onclick="showSendEmailModal(${item.id})" title="发送邮件"><i class="bi bi-envelope" style="font-size:11px;"></i></button>
|
||||
<button class="btn btn-sm btn-outline-primary py-0 px-1" onclick="openEditModal(${item.id})" title="编辑"><i class="bi bi-pencil" style="font-size:11px;"></i></button>
|
||||
${item.type === 'todo' && item.status !== 'completed' ? `<button class="btn btn-sm btn-outline-success py-0 px-1" onclick="completeItem(${item.id})" title="完成"><i class="bi bi-check-lg" style="font-size:11px;"></i></button>` : ''}
|
||||
@@ -1176,6 +1319,80 @@ async function reopenItem(id) {
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 转换为待办 ============
|
||||
|
||||
let convertItemData = null;
|
||||
|
||||
async function showConvertModal(itemId) {
|
||||
const res = await fetch(`${API_BASE}/items/${itemId}`);
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.success) return;
|
||||
|
||||
convertItemData = data.data;
|
||||
document.getElementById('convertItemId').value = itemId;
|
||||
|
||||
// 设置默认标题
|
||||
document.getElementById('convertTitle').value = convertItemData.title || '';
|
||||
|
||||
// 重置选项
|
||||
document.getElementById('modeConvert').checked = true;
|
||||
document.getElementById('convertStatus').value = 'pending';
|
||||
document.getElementById('convertPriority').value = 'medium';
|
||||
document.getElementById('convertDueDate').value = '';
|
||||
|
||||
// 显示内容预览
|
||||
let preview = '';
|
||||
if (convertItemData.type === 'text') {
|
||||
preview = convertItemData.content || '';
|
||||
} else if (convertItemData.type === 'link') {
|
||||
preview = `链接: ${convertItemData.url}`;
|
||||
if (convertItemData.content) preview += `\n${convertItemData.content}`;
|
||||
} else if (convertItemData.type === 'column') {
|
||||
preview = `专栏: ${convertItemData.url}`;
|
||||
if (convertItemData.source) preview += `\n来源: ${convertItemData.source}`;
|
||||
}
|
||||
if (convertItemData.note) preview += `\n\n备注: ${convertItemData.note}`;
|
||||
|
||||
document.getElementById('convertPreview').textContent = preview || '(无内容)';
|
||||
|
||||
new bootstrap.Modal(document.getElementById('convertModal')).show();
|
||||
}
|
||||
|
||||
async function executeConvert() {
|
||||
const itemId = document.getElementById('convertItemId').value;
|
||||
const mode = document.querySelector('input[name="convertMode"]:checked').value;
|
||||
|
||||
const data = {
|
||||
mode: mode,
|
||||
title: document.getElementById('convertTitle').value,
|
||||
status: document.getElementById('convertStatus').value,
|
||||
priority: document.getElementById('convertPriority').value,
|
||||
due_date: document.getElementById('convertDueDate').value || null
|
||||
};
|
||||
|
||||
const res = await fetch(`${API_BASE}/items/${itemId}/convert`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
|
||||
if (result.success) {
|
||||
bootstrap.Modal.getInstance(document.getElementById('convertModal')).hide();
|
||||
|
||||
if (mode === 'copy') {
|
||||
alert(`已创建新待办,原收藏保留\n新待办ID: ${result.data.id}`);
|
||||
}
|
||||
|
||||
refreshData();
|
||||
loadReminders();
|
||||
} else {
|
||||
alert(result.error || '转换失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 删除条目
|
||||
async function deleteItem(id) {
|
||||
if (!confirm('确认删除?')) return;
|
||||
@@ -1198,6 +1415,10 @@ async function showDetail(id) {
|
||||
document.getElementById('detailTypeIcon').textContent = getTypeIcon(item.type);
|
||||
document.getElementById('detailTitle').textContent = item.title || '(无标题)';
|
||||
|
||||
// 非待办类型显示转换按钮
|
||||
const convertBtn = document.getElementById('detailConvertBtn');
|
||||
convertBtn.style.display = item.type !== 'todo' ? 'inline-block' : 'none';
|
||||
|
||||
let html = `<div class="mb-3"><strong>类型:</strong> ${getTypeLabel(item.type)}</div>`;
|
||||
|
||||
if (item.url) {
|
||||
@@ -1249,6 +1470,12 @@ async function showDetail(id) {
|
||||
new bootstrap.Modal(document.getElementById('detailModal')).show();
|
||||
}
|
||||
|
||||
// 从详情页打开转换
|
||||
function showConvertModalFromDetail() {
|
||||
bootstrap.Modal.getInstance(document.getElementById('detailModal')).hide();
|
||||
setTimeout(() => showConvertModal(currentDetailId), 300);
|
||||
}
|
||||
|
||||
// 从详情页打开编辑
|
||||
function openEditModalFromDetail() {
|
||||
bootstrap.Modal.getInstance(document.getElementById('detailModal')).hide();
|
||||
|
||||
Reference in New Issue
Block a user