feat: 添加未读提醒功能

- 数据列表中未读数据(views=0)显示特殊样式(黄色背景+红色边框+红点标记)
- 左侧类别显示未读数量(红色badge)
- API返回unread和unread_by_type统计数据
This commit is contained in:
2026-04-22 23:44:22 +08:00
parent 9eb872391e
commit 4b2a94002b
2 changed files with 57 additions and 6 deletions

View File

@@ -1208,6 +1208,17 @@ INDEX_TEMPLATE = '''
.type-todo { border-left: 4px solid #ffc107; }
.is-starred { border-left: 4px solid #ffc107; background: #fffbe6; }
.is-starred:hover { background: #fff9e0; }
/* 未读数据样式 */
.unread-item { background: #fff3cd; border-left: 4px solid #dc3545; }
.unread-item:hover { background: #ffeaa7; }
.unread-dot {
display: inline-block;
width: 8px;
height: 8px;
background: #dc3545;
border-radius: 50%;
margin-right: 4px;
}
/* Markdown内容样式 */
.markdown-content { line-height: 1.6; }
.markdown-content h3 { font-size: 1.2em; margin-bottom: 0.5em; }
@@ -2610,12 +2621,16 @@ function renderItems(items) {
const modeClass = displayMode === 'single' ? 'compact' : 'double-line';
container.innerHTML = items.map(item => `
<div class="card type-${item.type} item-card ${modeClass} ${item.is_starred ? 'is-starred' : ''}" style="cursor: pointer;" onclick="showDetail(${item.id})">
container.innerHTML = items.map(item => {
const unreadClass = (!item.views || item.views === 0) ? 'unread-item' : '';
const unreadBadge = (!item.views || item.views === 0) ? '<span class="unread-dot" title="未读"></span>' : '';
return `<div class="card type-${item.type} item-card ${modeClass} ${item.is_starred ? 'is-starred' : ''} ${unreadClass}" style="cursor: pointer;" onclick="showDetail(${item.id})">
<div class="card-body">
<div class="card-content">
<div class="card-main">
<h6 class="card-title text-truncate mb-1 ${item.type === 'todo' && item.status === 'completed' ? 'text-muted' : ''}">
${unreadBadge}
${item.is_starred ? '<i class="bi bi-star-fill" style="color:#ffc107;"></i>' : ''} ${getTypeIcon(item.type)} ${item.title || truncate(item.content || item.url, 30)}
${item.type === 'todo' && item.status === 'completed' ? '' : ''}
<span style="font-size:10px; opacity:0.5; margin-left:8px;">
@@ -2646,7 +2661,7 @@ function renderItems(items) {
</div>
</div>
</div>
`).join('');
`}).join('');
}
// 渲染分页
@@ -2726,9 +2741,37 @@ async function loadStats() {
document.getElementById('statPending').textContent = data.data.todo_status?.pending || 0;
document.getElementById('statProgress').textContent = data.data.todo_status?.in_progress || 0;
document.getElementById('statCompleted').textContent = data.data.todo_status?.completed || 0;
// 更新左侧类别未读数量显示
updateUnreadCounts(data.data.unread_by_type || {});
}
}
// 更新未读数量显示
function updateUnreadCounts(unreadByType) {
const types = ['text', 'link', 'column', 'todo'];
types.forEach(type => {
const section = document.getElementById(`section-${type}`);
if (section) {
const count = unreadByType[type] || 0;
// 找到或创建未读数量badge
let badge = section.querySelector('.unread-badge');
if (count > 0) {
if (!badge) {
badge = document.createElement('span');
badge.className = 'unread-badge';
badge.style.cssText = 'background:#dc3545;color:#fff;font-size:10px;padding:2px 6px;border-radius:10px;margin-left:8px;';
section.querySelector('.section-header .header-left').appendChild(badge);
}
badge.textContent = count;
badge.style.display = 'inline';
} else if (badge) {
badge.style.display = 'none';
}
}
});
}
// 刷新数据(统计+列表)
async function refreshData() {
await loadStats();

View File

@@ -919,15 +919,23 @@ class Database:
stats = {}
# 总数
cursor.execute("SELECT COUNT(*) as count FROM items")
cursor.execute("SELECT COUNT(*) as count FROM items WHERE is_deleted = 0")
stats['total'] = cursor.fetchone()['count']
# 按类型统计
cursor.execute("SELECT type, COUNT(*) as count FROM items GROUP BY type")
cursor.execute("SELECT type, COUNT(*) as count FROM items WHERE is_deleted = 0 GROUP BY type")
stats['by_type'] = {row['type']: row['count'] for row in cursor.fetchall()}
# 未读数量统计views = 0
cursor.execute("SELECT COUNT(*) as count FROM items WHERE is_deleted = 0 AND (views IS NULL OR views = 0)")
stats['unread'] = cursor.fetchone()['count']
# 按类型统计未读数量
cursor.execute("SELECT type, COUNT(*) as count FROM items WHERE is_deleted = 0 AND (views IS NULL OR views = 0) GROUP BY type")
stats['unread_by_type'] = {row['type']: row['count'] for row in cursor.fetchall()}
# 待办状态统计
cursor.execute("SELECT status, COUNT(*) as count FROM items WHERE type = 'todo' GROUP BY status")
cursor.execute("SELECT status, COUNT(*) as count FROM items WHERE type = 'todo' AND is_deleted = 0 GROUP BY status")
stats['todo_status'] = {row['status']: row['count'] for row in cursor.fetchall()}
# 标签数