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; }
|
.type-todo { border-left: 4px solid #ffc107; }
|
||||||
.is-starred { border-left: 4px solid #ffc107; background: #fffbe6; }
|
.is-starred { border-left: 4px solid #ffc107; background: #fffbe6; }
|
||||||
.is-starred:hover { background: #fff9e0; }
|
.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内容样式 */
|
||||||
.markdown-content { line-height: 1.6; }
|
.markdown-content { line-height: 1.6; }
|
||||||
.markdown-content h3 { font-size: 1.2em; margin-bottom: 0.5em; }
|
.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';
|
const modeClass = displayMode === 'single' ? 'compact' : 'double-line';
|
||||||
|
|
||||||
container.innerHTML = items.map(item => `
|
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})">
|
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-body">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="card-main">
|
<div class="card-main">
|
||||||
<h6 class="card-title text-truncate mb-1 ${item.type === 'todo' && item.status === 'completed' ? 'text-muted' : ''}">
|
<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.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' ? ' ✓' : ''}
|
${item.type === 'todo' && item.status === 'completed' ? ' ✓' : ''}
|
||||||
<span style="font-size:10px; opacity:0.5; margin-left:8px;">
|
<span style="font-size:10px; opacity:0.5; margin-left:8px;">
|
||||||
@@ -2646,7 +2661,7 @@ function renderItems(items) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲染分页
|
// 渲染分页
|
||||||
@@ -2726,9 +2741,37 @@ async function loadStats() {
|
|||||||
document.getElementById('statPending').textContent = data.data.todo_status?.pending || 0;
|
document.getElementById('statPending').textContent = data.data.todo_status?.pending || 0;
|
||||||
document.getElementById('statProgress').textContent = data.data.todo_status?.in_progress || 0;
|
document.getElementById('statProgress').textContent = data.data.todo_status?.in_progress || 0;
|
||||||
document.getElementById('statCompleted').textContent = data.data.todo_status?.completed || 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() {
|
async function refreshData() {
|
||||||
await loadStats();
|
await loadStats();
|
||||||
|
|||||||
@@ -919,15 +919,23 @@ class Database:
|
|||||||
stats = {}
|
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']
|
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()}
|
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()}
|
stats['todo_status'] = {row['status']: row['count'] for row in cursor.fetchall()}
|
||||||
|
|
||||||
# 标签数
|
# 标签数
|
||||||
|
|||||||
Reference in New Issue
Block a user