feat: 添加未读提醒功能
- 数据列表中未读数据(views=0)显示特殊样式(黄色背景+红色边框+红点标记) - 左侧类别显示未读数量(红色badge) - API返回unread和unread_by_type统计数据
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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()}
|
||||
|
||||
# 标签数
|
||||
|
||||
Reference in New Issue
Block a user