@@ -112,6 +112,93 @@ def complete_item(item_id):
return jsonify ( { ' success ' : True , ' data ' : item } )
@app.route ( ' /api/items/<int:item_id>/reopen ' , methods = [ ' POST ' ] )
def reopen_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
# 获取请求中的目标状态,默认为 pending
data = request . get_json ( ) or { }
new_status = data . get ( ' status ' , ' pending ' )
if new_status not in [ ' pending ' , ' in_progress ' ] :
return jsonify ( { ' success ' : False , ' error ' : ' 无效状态 ' } ) , 400
db . update_item ( item_id , status = new_status )
item = db . get_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 ( ) :
""" 列出标签 """
@@ -167,6 +254,13 @@ def get_stats():
return jsonify ( { ' success ' : True , ' data ' : stats } )
@app.route ( ' /api/reminders ' , methods = [ ' GET ' ] )
def get_reminders ( ) :
""" 获取提醒信息 """
reminders = db . get_reminders ( )
return jsonify ( { ' success ' : True , ' data ' : reminders } )
@app.route ( ' /api/ai-process ' , methods = [ ' POST ' ] )
def ai_process ( ) :
""" AI处理文本 """
@@ -490,6 +584,16 @@ INDEX_TEMPLATE = '''
<!-- 主内容 -->
<div class= " col-md-10 content " >
<!-- 提醒栏 -->
<div id= " reminderBar " class= " alert alert-warning alert-dismissible fade show mb-3 " style= " display:none; " role= " alert " >
<i class= " bi bi-bell-fill " ></i>
<span id= " reminderText " >有待办事项需要关注</span>
<button type= " button " class= " btn btn-sm btn-warning ms-2 " onclick= " showRemindersModal() " >
<i class= " bi bi-eye " ></i> 查看详情
</button>
<button type= " button " class= " btn-close " data-bs-dismiss= " alert " onclick= " dismissReminderBar() " ></button>
</div>
<!-- 顶部操作栏 -->
<div class= " d-flex justify-content-between align-items-center mb-4 " >
<div class= " d-flex gap-2 " >
@@ -505,12 +609,21 @@ INDEX_TEMPLATE = '''
<button class= " btn btn-outline-info me-2 " onclick= " showAIAddModal() " title= " AI自动添加 " >
<i class= " bi bi-robot " ></i> AI添加
</button>
<button class= " btn btn-outline-secondary me-1 " onclick= " showAddModal( ' text ' ) " title= " 添加文本 " >
<i class= " bi bi-file-text " ></i>
</button>
<button class= " btn btn-outline-success me-1 " onclick= " showAddModal( ' link ' ) " title= " 添加链接 " >
<i class= " bi bi-link-45deg " ></i>
</button>
<button class= " btn btn-outline-warning me-1 " onclick= " showAddModal( ' todo ' ) " title= " 添加待办 " >
<i class= " bi bi-check2-square " ></i>
</button>
<button class= " btn btn-outline-primary me-1 " onclick= " showAddModal( ' column ' ) " title= " 添加专栏 " >
<i class= " bi bi-newspaper " ></i>
</button>
<button class= " btn btn-outline-success me-2 " onclick= " exportData() " title= " 导出JSON " >
<i class= " bi bi-download " ></i> 导出
</button>
<button class= " btn btn-primary " data-bs-toggle= " modal " data-bs-target= " #addModal " >
<i class= " bi bi-plus-lg " ></i> 添加
</button>
</div>
<!-- 统计卡片 -->
@@ -563,20 +676,15 @@ INDEX_TEMPLATE = '''
<div class= " modal-dialog modal-lg " >
<div class= " modal-content " >
<div class= " modal-header " >
<h5 class= " modal-title " >添加条目</h5>
<h5 class= " modal-title " >
<span id= " addModalIcon " ></span>
<span id= " addModalTitle " >添加条目</span>
</h5>
<button type= " button " class= " btn-close " data-bs-dismiss= " modal " ></button>
</div>
<div class= " modal-body " >
<form id= " addForm " >
<div class = " mb-3 " >
<label class= " form-label " >类型</label>
<select id= " addType " class= " form-select " >
<option value= " text " >📝 文本</option>
<option value= " link " >🔗 链接</option>
<option value= " column " >📰 专栏</option>
<option value= " todo " >✅ 待办</option>
</select>
</div>
<input type= " hidden " id= " addType " value = " text " >
<div class= " mb-3 " >
<label class= " form-label " >标题</label>
<input type= " text " id= " addTitle " class= " form-control " placeholder= " 可选 " >
@@ -587,11 +695,11 @@ INDEX_TEMPLATE = '''
</div>
<div class= " mb-3 " id= " urlGroup " style= " display:none; " >
<label class= " form-label " >URL</label>
<input type= " url " id= " addUrl " class= " form-control " >
<input type= " url " id= " addUrl " class= " form-control " placeholder= " https://... " >
</div>
<div class= " mb-3 " id= " sourceGroup " style= " display:none; " >
<label class= " form-label " >来源</label>
<input type= " text " id= " addSource " class= " form-control " >
<input type= " text " id= " addSource " class= " form-control " placeholder= " 专栏来源 " >
</div>
<div class= " mb-3 " id= " todoFields " style= " display:none; " >
<div class= " row " >
@@ -614,8 +722,8 @@ INDEX_TEMPLATE = '''
</div>
</div>
<div class= " mt-3 " >
<label class= " form-label " >截止日期 </label>
<input type= " date " id= " addDueDate " class= " form-control " >
<label class= " form-label " >截止时间 </label>
<input type= " datetime-local " id= " addDueDate " class= " form-control " >
</div>
</div>
<div class= " mb-3 " >
@@ -652,6 +760,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>
@@ -718,8 +829,8 @@ INDEX_TEMPLATE = '''
</div>
</div>
<div class= " mt-3 " >
<label class= " form-label " >截止日期 </label>
<input type= " date " id= " editDueDate " class= " form-control " >
<label class= " form-label " >截止时间 </label>
<input type= " datetime-local " id= " editDueDate " class= " form-control " >
</div>
</div>
<div class= " mb-3 " >
@@ -881,6 +992,100 @@ INDEX_TEMPLATE = '''
</div>
</div>
<!-- 提醒详情模态框 -->
<div class= " modal fade " id= " remindersModal " tabindex= " -1 " >
<div class= " modal-dialog modal-lg " >
<div class= " modal-content " >
<div class= " modal-header bg-warning text-dark " >
<h5 class= " modal-title " ><i class= " bi bi-bell-fill " ></i> 待办提醒</h5>
<button type= " button " class= " btn-close " data-bs-dismiss= " modal " ></button>
</div>
<div class= " modal-body " >
<div id= " remindersContent " >
<!-- 动态填充 -->
</div>
</div>
<div class= " modal-footer " >
<button type= " button " class= " btn btn-secondary " data-bs-dismiss= " modal " >关闭</button>
</div>
</div>
</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 " >
<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 " checked>
<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= " datetime-local " 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; white-space: pre-wrap; word-break: break-all; " >
<!-- 动态填充 -->
</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 ' ;
@@ -891,6 +1096,10 @@ document.addEventListener('DOMContentLoaded', async () => {
await loadStats(); // 先加载统计,确保总数可用
loadItems();
loadTags();
loadReminders(); // 加载提醒
// 定时刷新提醒( 每5分钟)
setInterval(loadReminders, 5 * 60 * 1000);
// 标签输入自动提示
document.getElementById( ' addTags ' ).addEventListener( ' input ' , showTagSuggestions);
@@ -899,15 +1108,6 @@ document.addEventListener('DOMContentLoaded', async () => {
// 标签搜索实时过滤
document.getElementById( ' tagSearch ' )?.addEventListener( ' input ' , debounce(loadTagManagerList, 300));
// 类型切换时显示/隐藏字段
document.getElementById( ' addType ' ).addEventListener( ' change ' , (e) => {
const type = e.target.value;
document.getElementById( ' contentGroup ' ).style.display = type === ' text ' ? ' block ' : ' none ' ;
document.getElementById( ' urlGroup ' ).style.display = [ ' link ' , ' column ' ].includes(type) ? ' block ' : ' none ' ;
document.getElementById( ' sourceGroup ' ).style.display = type === ' column ' ? ' block ' : ' none ' ;
document.getElementById( ' todoFields ' ).style.display = type === ' todo ' ? ' block ' : ' none ' ;
});
// 编辑时类型切换
document.getElementById( ' editType ' ).addEventListener( ' change ' , (e) => {
updateEditFieldsByType(e.target.value);
@@ -976,19 +1176,22 @@ function renderItems(items) {
<div class= " card-body " >
<div class= " d-flex justify-content-between align-items-start " >
<div style= " flex: 1; min-width: 0; " >
<h6 class= " card-title text-truncate mb-1 " >
<h6 class= " card-title text-truncate mb-1 $ { item.type === ' todo ' && item.status === ' completed ' ? ' text-muted ' : ' ' } " >
$ { getTypeIcon(item.type)} $ { item.title || truncate(item.content || item.url, 30)}
$ { item.type === ' todo ' && item.status === ' completed ' ? ' ✓ ' : ' ' }
</h6>
<p class= " card-text text-muted small mb-0 text-truncate " style= " font-size:12px; " >
$ { item.url ? truncate(item.url, 50) : item.content ? truncate(item.content, 50) : item.note ? truncate(item.note, 50) : ' ' }
$ { item.type === ' todo ' ? `$ { getStatusLabelShort(item.status)} $ { getPriorityLabelShort(item.priority)} $ { item.due_date ? ' 📅 ' + item.due_date : ' ' }` : ' ' }
$ { item.type === ' todo ' ? `$ { getStatusLabelShort(item.status)} $ { getPriorityLabelShort(item.priority)} $ { item.due_date ? ' 📅 ' + formatDueDate( item.due_date) : ' ' }` : ' ' }
</p>
</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>` : ' ' }
$ { item.type === ' todo ' && item.status === ' completed ' ? `<button class= " btn btn-sm btn-outline-warning py-0 px-1 " onclick= " reopenItem($ {item.id} ) " title= " 重新打开 " ><i class= " bi bi-arrow-counterclockwise " style= " font-size:11px; " ></i></button>` : ' ' }
<button class= " btn btn-sm btn-outline-danger py-0 px-1 " onclick= " deleteItem($ {item.id} ) " title= " 删除 " ><i class= " bi bi-trash " style= " font-size:11px; " ></i></button>
</div>
</div>
@@ -1062,6 +1265,37 @@ async function refreshData() {
loadItems(currentPage);
}
// ============ 添加功能 ============
// 快捷添加按钮
function showAddModal(type) {
// 设置类型
document.getElementById( ' addType ' ).value = type;
// 设置弹窗标题和图标
const typeInfo = {
text: { icon: ' 📝 ' , title: ' 添加文本 ' },
link: { icon: ' 🔗 ' , title: ' 添加链接 ' },
column: { icon: ' 📰 ' , title: ' 添加专栏 ' },
todo: { icon: ' ✅ ' , title: ' 添加待办 ' }
};
document.getElementById( ' addModalIcon ' ).textContent = typeInfo[type].icon;
document.getElementById( ' addModalTitle ' ).textContent = typeInfo[type].title;
// 显示/隐藏对应字段
document.getElementById( ' contentGroup ' ).style.display = type === ' text ' ? ' block ' : ' none ' ;
document.getElementById( ' urlGroup ' ).style.display = [ ' link ' , ' column ' ].includes(type) ? ' block ' : ' none ' ;
document.getElementById( ' sourceGroup ' ).style.display = type === ' column ' ? ' block ' : ' none ' ;
document.getElementById( ' todoFields ' ).style.display = type === ' todo ' ? ' block ' : ' none ' ;
// 清空表单
document.getElementById( ' addForm ' ).reset();
// 打开弹窗
new bootstrap.Modal(document.getElementById( ' addModal ' )).show();
}
// 添加条目
async function addItem() {
const type = document.getElementById( ' addType ' ).value;
@@ -1097,6 +1331,94 @@ async function completeItem(id) {
refreshData();
}
// 重新打开待办
async function reopenItem(id) {
const res = await fetch(`$ {API_BASE} /items/$ {id} /reopen`, {
method: ' POST ' ,
headers: { ' Content-Type ' : ' application/json ' },
body: JSON.stringify( { status: ' pending ' })
});
if (res.ok) {
refreshData();
loadReminders(); // 刷新提醒
}
}
// ============ 转换为待办 ============
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( ' modeCopy ' ).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;
@@ -1119,6 +1441,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) {
@@ -1137,7 +1463,7 @@ async function showDetail(id) {
html += `<div class= " mb-3 " ><strong>状态:</strong> $ { getStatusLabel(item.status)}</div>`;
html += `<div class= " mb-3 " ><strong>优先级:</strong> $ { getPriorityLabel(item.priority)}</div>`;
if (item.due_date) {
html += `<div class= " mb-3 " ><strong>截止日期 :</strong> $ { item.due_date} </div>`;
html += `<div class= " mb-3 " ><strong>截止时间 :</strong> $ { formatDueDateFull( item.due_date) }</div>`;
}
}
@@ -1170,6 +1496,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();
@@ -1203,7 +1535,21 @@ async function openEditModal(id) {
if (type === ' todo ' ) {
document.getElementById( ' editStatus ' ).value = item.status;
document.getElementById( ' editPriority ' ).value = item.priority;
document.getElementById( ' editDueDate ' ).value = item.due_date || ' ' ;
// 处理 datetime-local 格式
if (item.due_date) {
// 尝试解析日期时间格式
let dueDate = item.due_date;
if (dueDate.includes( ' T ' )) {
// ISO 格式,截取到分钟
dueDate = dueDate.substring(0, 16);
} else if (dueDate.length === 10) {
// 只有日期,添加默认时间 00:00
dueDate = dueDate + ' T00:00 ' ;
}
document.getElementById( ' editDueDate ' ).value = dueDate;
} else {
document.getElementById( ' editDueDate ' ).value = ' ' ;
}
}
new bootstrap.Modal(document.getElementById( ' editModal ' )).show();
@@ -1286,6 +1632,64 @@ function formatDate(dateStr) {
return new Date(dateStr).toLocaleString( ' zh-CN ' );
}
// 格式化截止时间(友好显示)
function formatDueDate(dueDate) {
if (!dueDate) return ' ' ;
let date;
if (dueDate.includes( ' T ' )) {
date = new Date(dueDate);
} else if (dueDate.length === 10) {
// 只有日期
date = new Date(dueDate + ' T00:00:00 ' );
} else {
date = new Date(dueDate);
}
// 格式化为友好格式
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const targetDay = new Date(date.getFullYear(), date.getMonth(), date.getDate());
const daysDiff = Math.floor((targetDay - today) / (1000 * 60 * 60 * 24));
let datePart;
if (daysDiff === 0) {
datePart = ' 今天 ' ;
} else if (daysDiff === 1) {
datePart = ' 明天 ' ;
} else if (daysDiff === -1) {
datePart = ' 昨天 ' ;
} else {
datePart = date.toLocaleDateString( ' zh-CN ' , { month: ' short ' , day: ' numeric ' });
}
const timePart = date.toLocaleTimeString( ' zh-CN ' , { hour: ' 2-digit ' , minute: ' 2-digit ' });
return `$ {datePart} $ {timePart} `;
}
// 格式化截止时间(完整显示)
function formatDueDateFull(dueDate) {
if (!dueDate) return ' ' ;
let date;
if (dueDate.includes( ' T ' )) {
date = new Date(dueDate);
} else if (dueDate.length === 10) {
date = new Date(dueDate + ' T00:00:00 ' );
} else {
date = new Date(dueDate);
}
return date.toLocaleString( ' zh-CN ' , {
year: ' numeric ' ,
month: ' long ' ,
day: ' numeric ' ,
hour: ' 2-digit ' ,
minute: ' 2-digit '
});
}
function escapeHtml(str) {
if (!str) return ' ' ;
return str.replace(/&/g, ' & ' ).replace(/</g, ' < ' ).replace(/>/g, ' > ' );
@@ -1785,6 +2189,165 @@ function debounce(fn, delay) {
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// ============ 提醒功能 ============
let reminderData = null;
let reminderDismissed = false;
async function loadReminders() {
const res = await fetch(`$ {API_BASE} /reminders`);
const data = await res.json();
if (data.success) {
reminderData = data.data;
updateReminderBar();
}
}
function updateReminderBar() {
if (!reminderData || reminderDismissed) return;
const total = reminderData.total;
if (total === 0) {
document.getElementById( ' reminderBar ' ).style.display = ' none ' ;
return;
}
// 显示提醒栏
document.getElementById( ' reminderBar ' ).style.display = ' block ' ;
// 构建提醒文本
let text = ' ' ;
if (reminderData.overdue.length > 0) {
text += `<span class= " text-danger fw-bold " >$ {reminderData.overdue.length} 个已过期</span> `;
}
if (reminderData.due_today.length > 0) {
text += `<span class= " text-warning fw-bold " >$ {reminderData.due_today.length} 个今天到期</span> `;
}
if (reminderData.due_soon.length > 0) {
text += `<span>$ {reminderData.due_soon.length} 个即将到期</span>`;
}
document.getElementById( ' reminderText ' ).innerHTML = text;
// 更新提醒角标样式
const bar = document.getElementById( ' reminderBar ' );
if (reminderData.overdue.length > 0) {
bar.className = ' alert alert-danger alert-dismissible fade show mb-3 ' ;
} else if (reminderData.due_today.length > 0) {
bar.className = ' alert alert-warning alert-dismissible fade show mb-3 ' ;
} else {
bar.className = ' alert alert-info alert-dismissible fade show mb-3 ' ;
}
}
function dismissReminderBar() {
reminderDismissed = true;
document.getElementById( ' reminderBar ' ).style.display = ' none ' ;
}
function showRemindersModal() {
if (!reminderData) return;
let html = ' ' ;
// 已过期
if (reminderData.overdue.length > 0) {
html += `<div class= " mb-3 " ><h6 class= " text-danger " ><i class= " bi bi-exclamation-circle " ></i> 已过期 ($ {reminderData.overdue.length} )</h6>`;
html += `<div class= " list-group " >`;
reminderData.overdue.forEach(item => {
html += `<div class= " list-group-item list-group-item-danger d-flex justify-content-between align-items-center " >
<div>
<strong>$ { item.title || ' (无标题) ' }</strong>
<br><small class= " text-muted " >截止: $ { formatDueDateFull(item.due_date)} | 已过期 $ {item.days_overdue} 天</small>
</div>
<div>
<button class= " btn btn-sm btn-success " onclick= " completeReminder($ {item.id} ) " title= " 完成 " >
<i class= " bi bi-check " ></i>
</button>
<button class= " btn btn-sm btn-outline-primary " onclick= " openEditModal($ {item.id} ); bootstrap.Modal.getInstance(document.getElementById( ' remindersModal ' )).hide(); " title= " 编辑 " >
<i class= " bi bi-pencil " ></i>
</button>
</div>
</div>`;
});
html += `</div></div>`;
}
// 今天到期
if (reminderData.due_today.length > 0) {
html += `<div class= " mb-3 " ><h6 class= " text-warning " ><i class= " bi bi-clock-fill " ></i> 今天到期 ($ {reminderData.due_today.length} )</h6>`;
html += `<div class= " list-group " >`;
reminderData.due_today.forEach(item => {
html += `<div class= " list-group-item list-group-item-warning d-flex justify-content-between align-items-center " >
<div>
<strong>$ { item.title || ' (无标题) ' }</strong>
<br><small class= " text-muted " >截止: $ { formatDueDateFull(item.due_date)}</small>
</div>
<div>
<button class= " btn btn-sm btn-success " onclick= " completeReminder($ {item.id} ) " title= " 完成 " >
<i class= " bi bi-check " ></i>
</button>
<button class= " btn btn-sm btn-outline-primary " onclick= " openEditModal($ {item.id} ); bootstrap.Modal.getInstance(document.getElementById( ' remindersModal ' )).hide(); " title= " 编辑 " >
<i class= " bi bi-pencil " ></i>
</button>
</div>
</div>`;
});
html += `</div></div>`;
}
// 即将到期
if (reminderData.due_soon.length > 0) {
html += `<div class= " mb-3 " ><h6 class= " text-info " ><i class= " bi bi-hourglass-split " ></i> 即将到期 ($ {reminderData.due_soon.length} )</h6>`;
html += `<div class= " list-group " >`;
reminderData.due_soon.forEach(item => {
html += `<div class= " list-group-item list-group-item-info d-flex justify-content-between align-items-center " >
<div>
<strong>$ { item.title || ' (无标题) ' }</strong>
<br><small class= " text-muted " >截止: $ { formatDueDateFull(item.due_date)}</small>
</div>
<div>
<button class= " btn btn-sm btn-success " onclick= " completeReminder($ {item.id} ) " title= " 完成 " >
<i class= " bi bi-check " ></i>
</button>
<button class= " btn btn-sm btn-outline-primary " onclick= " openEditModal($ {item.id} ); bootstrap.Modal.getInstance(document.getElementById( ' remindersModal ' )).hide(); " title= " 编辑 " >
<i class= " bi bi-pencil " ></i>
</button>
</div>
</div>`;
});
html += `</div></div>`;
}
if (!html) {
html = ' <div class= " text-center text-muted py-3 " >暂无待办提醒</div> ' ;
}
document.getElementById( ' remindersContent ' ).innerHTML = html;
new bootstrap.Modal(document.getElementById( ' remindersModal ' )).show();
}
async function completeReminder(id) {
await fetch(`$ {API_BASE} /items/$ {id} /done`, { method: ' POST ' });
// 刷新数据
await loadReminders();
refreshData();
// 如果弹窗打开,更新显示
const modalEl = document.getElementById( ' remindersModal ' );
const modal = bootstrap.Modal.getInstance(modalEl);
if (modal) {
// 检查是否还有提醒
if (reminderData && reminderData.total > 0) {
showRemindersModal();
} else {
modal.hide();
}
}
}
</script>
</body>
</html>