feat: 新增阅读数功能
- 数据库添加 views 字段,兼容旧数据库自动添加 - API 新增 /api/items/<id>/view 接口增加阅读数 - 列表显示阅读数(👁图标) - 详情页显示阅读数,点击详情时自动增加
This commit is contained in:
@@ -30,6 +30,8 @@ def list_items():
|
||||
tag=request.args.get('tag'),
|
||||
keyword=request.args.get('keyword'),
|
||||
starred=starred,
|
||||
sort_by=request.args.get('sort_by'),
|
||||
sort_order=request.args.get('sort_order'),
|
||||
limit=int(request.args.get('limit', 50)),
|
||||
offset=int(request.args.get('offset', 0))
|
||||
)
|
||||
@@ -163,6 +165,15 @@ def set_star_item(item_id, status):
|
||||
return jsonify({'success': False, 'error': '条目不存在'}), 404
|
||||
|
||||
|
||||
@app.route('/api/items/<int:item_id>/view', methods=['POST'])
|
||||
def increment_views(item_id):
|
||||
"""增加阅读数"""
|
||||
if db.increment_views(item_id):
|
||||
item = db.get_item(item_id)
|
||||
return jsonify({'success': True, 'data': item})
|
||||
return jsonify({'success': False, 'error': '条目不存在'}), 404
|
||||
|
||||
|
||||
@app.route('/api/items/<int:item_id>/done', methods=['POST'])
|
||||
def complete_item(item_id):
|
||||
"""完成待办"""
|
||||
@@ -714,6 +725,15 @@ INDEX_TEMPLATE = '''
|
||||
<option value="column">专栏</option>
|
||||
<option value="todo">待办</option>
|
||||
</select>
|
||||
<select id="sortBy" class="form-select" style="width: 130px;" onchange="changeSort()">
|
||||
<option value="">默认排序</option>
|
||||
<option value="created_at">创建时间</option>
|
||||
<option value="updated_at">更新时间</option>
|
||||
</select>
|
||||
<select id="sortOrder" class="form-select" style="width: 100px;" onchange="changeSort()">
|
||||
<option value="desc">降序 ↓</option>
|
||||
<option value="asc">升序 ↑</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-outline-info me-2" onclick="showAIAddModal()" title="AI自动添加">
|
||||
<i class="bi bi-robot"></i> AI添加
|
||||
@@ -1250,6 +1270,7 @@ INDEX_TEMPLATE = '''
|
||||
<script>
|
||||
const API_BASE = '/api';
|
||||
let currentFilter = { type: '', status: '', starred: null };
|
||||
let currentSort = { sort_by: '', sort_order: '' };
|
||||
let currentPage = 1;
|
||||
const pageSize = 20;
|
||||
function debounce(fn, delay) {
|
||||
@@ -1331,6 +1352,8 @@ async function loadItems(page = 1) {
|
||||
if (currentFilter.status) url += `&status=${currentFilter.status}`;
|
||||
if (currentFilter.starred !== null) url += `&starred=${currentFilter.starred ? 'true' : 'false'}`;
|
||||
if (keyword) url += `&keyword=${encodeURIComponent(keyword)}`;
|
||||
if (currentSort.sort_by) url += `&sort_by=${currentSort.sort_by}`;
|
||||
if (currentSort.sort_order) url += `&sort_order=${currentSort.sort_order}`;
|
||||
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
@@ -1362,6 +1385,7 @@ function renderItems(items) {
|
||||
</span>
|
||||
${item.content_stats && (item.content_stats.lines > 0 || item.content_stats.chars > 0) ?
|
||||
`<span style="font-size:10px; opacity:0.6; margin-left:4px;" title="有效行数/总字数">[${item.content_stats.lines}行/${item.content_stats.chars}字]</span>` : ''}
|
||||
${item.views > 0 ? `<span style="font-size:10px; opacity:0.5; margin-left:4px;" title="阅读次数">👁${item.views}</span>` : ''}
|
||||
</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) : ''}
|
||||
@@ -1450,6 +1474,14 @@ async function refreshData() {
|
||||
loadItems(currentPage);
|
||||
}
|
||||
|
||||
// 排序切换
|
||||
function changeSort() {
|
||||
const sortBy = document.getElementById('sortBy').value;
|
||||
const sortOrder = document.getElementById('sortOrder').value;
|
||||
currentSort = { sort_by: sortBy, sort_order: sortOrder };
|
||||
loadItems(1); // 切换排序时回到第一页
|
||||
}
|
||||
|
||||
// ============ 添加功能 ============
|
||||
|
||||
// 快捷添加按钮
|
||||
@@ -1618,6 +1650,10 @@ let currentDetailId = null;
|
||||
// 显示详情
|
||||
async function showDetail(id) {
|
||||
currentDetailId = id;
|
||||
|
||||
// 增加阅读数
|
||||
await fetch(`${API_BASE}/items/${id}/view`, { method: 'POST' });
|
||||
|
||||
const res = await fetch(`${API_BASE}/items/${id}`);
|
||||
const data = await res.json();
|
||||
|
||||
@@ -1633,6 +1669,9 @@ async function showDetail(id) {
|
||||
|
||||
let html = `<div class="mb-3"><strong>类型:</strong> ${getTypeLabel(item.type)}</div>`;
|
||||
|
||||
// 显示阅读数
|
||||
html += `<div class="mb-3"><strong>阅读:</strong> <span class="badge bg-secondary">👁 ${item.views || 0} 次</span></div>`;
|
||||
|
||||
// 显示重点关注状态
|
||||
if (item.is_starred) {
|
||||
html += `<div class="mb-3"><strong>状态:</strong> <span class="badge bg-warning text-dark"><i class="bi bi-star-fill"></i> 重点关注</span></div>`;
|
||||
|
||||
@@ -60,6 +60,7 @@ class Database:
|
||||
due_date TEXT,
|
||||
note TEXT,
|
||||
is_starred INTEGER DEFAULT 0,
|
||||
views INTEGER DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
@@ -124,6 +125,12 @@ class Database:
|
||||
# 创建 is_starred 索引(字段添加后再创建)
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_items_starred ON items(is_starred)")
|
||||
|
||||
# 检查并添加 views 字段(兼容旧数据库)
|
||||
try:
|
||||
cursor.execute("SELECT views FROM items LIMIT 1")
|
||||
except sqlite3.OperationalError:
|
||||
cursor.execute("ALTER TABLE items ADD COLUMN views INTEGER DEFAULT 0")
|
||||
|
||||
conn.commit()
|
||||
|
||||
# ============ Item 操作 ============
|
||||
@@ -171,8 +178,13 @@ class Database:
|
||||
return item
|
||||
|
||||
def list_items(self, type: str = None, status: str = None, tag: str = None,
|
||||
keyword: str = None, starred: bool = None, limit: int = 50, offset: int = 0) -> List[Dict[str, Any]]:
|
||||
"""列出条目"""
|
||||
keyword: str = None, starred: bool = None, sort_by: str = None,
|
||||
sort_order: str = None, limit: int = 50, offset: int = 0) -> List[Dict[str, Any]]:
|
||||
"""列出条目
|
||||
|
||||
sort_by: created_at, updated_at
|
||||
sort_order: desc, asc
|
||||
"""
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
@@ -206,8 +218,31 @@ class Database:
|
||||
if conditions:
|
||||
query += " WHERE " + " AND ".join(conditions)
|
||||
|
||||
# 重点关注优先显示
|
||||
query += " ORDER BY i.is_starred DESC, i.created_at DESC LIMIT ? OFFSET ?"
|
||||
# 排序逻辑
|
||||
if sort_by == 'updated_at':
|
||||
order_field = 'i.updated_at'
|
||||
elif sort_by == 'created_at':
|
||||
order_field = 'i.created_at'
|
||||
else:
|
||||
# 默认:重点关注优先 + 创建时间降序
|
||||
order_field = 'i.created_at'
|
||||
|
||||
order_dir = 'DESC' if (sort_order == 'asc' or sort_order is None) else 'ASC'
|
||||
# 这里反转逻辑:用户选择"降序"时用DESC,选择"升序"时用ASC
|
||||
|
||||
if sort_order == 'asc':
|
||||
order_dir = 'ASC'
|
||||
elif sort_order == 'desc':
|
||||
order_dir = 'DESC'
|
||||
else:
|
||||
order_dir = 'DESC' # 默认降序
|
||||
|
||||
# 如果有指定排序字段,按该字段排序;否则默认重点关注优先
|
||||
if sort_by:
|
||||
query += f" ORDER BY {order_field} {order_dir} LIMIT ? OFFSET ?"
|
||||
else:
|
||||
# 默认:重点关注优先,然后创建时间降序
|
||||
query += f" ORDER BY i.is_starred DESC, i.created_at DESC LIMIT ? OFFSET ?"
|
||||
params.extend([limit, offset])
|
||||
|
||||
cursor.execute(query, params)
|
||||
@@ -326,6 +361,14 @@ class Database:
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
|
||||
def increment_views(self, item_id: int) -> bool:
|
||||
"""增加阅读数"""
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("UPDATE items SET views = views + 1 WHERE id = ?", (item_id,))
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
|
||||
# ============ Tag 操作 ============
|
||||
|
||||
def create_tag(self, name: str, color: str = "#3498db") -> int:
|
||||
|
||||
Reference in New Issue
Block a user