feat: 添加置顶功能,置顶优先级最高

This commit is contained in:
2026-04-24 12:16:39 +08:00
parent 70dee494f3
commit 50d2e62694
2 changed files with 79 additions and 11 deletions

View File

@@ -176,6 +176,25 @@ def set_star_item(item_id, status):
return jsonify({'success': False, 'error': '条目不存在'}), 404
@app.route('/api/items/<int:item_id>/pin', methods=['POST'])
def toggle_pin_item(item_id):
"""切换置顶状态"""
if db.toggle_pin(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>/pin/<int:status>', methods=['POST'])
def set_pin_item(item_id, status):
"""设置置顶状态 (status: 1=置顶, 0=取消置顶)"""
pinned = status == 1
if db.set_pin(item_id, pinned):
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>/view', methods=['POST'])
def increment_views(item_id):
"""增加阅读数"""
@@ -1206,6 +1225,8 @@ INDEX_TEMPLATE = '''
.type-link { border-left: 4px solid #28a745; }
.type-column { border-left: 4px solid #6f42c1; }
.type-todo { border-left: 4px solid #ffc107; }
.is-pinned { border-left: 4px solid #0d6efd; background: #e8f4ff; }
.is-pinned:hover { background: #d0e8ff; }
.is-starred { border-left: 4px solid #ffc107; background: #fffbe6; }
.is-starred:hover { background: #fff9e0; }
/* 未读数据样式 */
@@ -2624,14 +2645,15 @@ function renderItems(items) {
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>' : '';
const pinnedClass = item.is_pinned ? 'is-pinned' : '';
return `<div class="card type-${item.type} item-card ${modeClass} ${item.is_starred ? 'is-starred' : ''} ${unreadClass}" style="cursor: pointer;" onclick="showDetail(${item.id})">
return `<div class="card type-${item.type} item-card ${modeClass} ${pinnedClass} ${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.is_pinned ? '📌' : ''} ${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;">
${formatShortDate(item.created_at)}${item.updated_at && item.updated_at !== item.created_at ? '' + formatShortDate(item.updated_at) : ''}
@@ -2647,6 +2669,9 @@ function renderItems(items) {
</div>
<div class="card-actions" onclick="event.stopPropagation();">
${item.tags.slice(0, 2).map(t => `<span class="badge bg-secondary" style="font-size:10px;">${t}</span>`).join('')}
<button class="btn btn-sm btn-outline-primary py-0 px-1 pin-btn" onclick="togglePin(${item.id})" title="${item.is_pinned ? '取消置顶' : '置顶'}">
${item.is_pinned ? '📌' : '📍'}
</button>
<button class="btn btn-sm btn-outline-warning py-0 px-1 star-btn" onclick="toggleStar(${item.id})" title="${item.is_starred ? '取消重点关注' : '设为重点关注'}">
<i class="bi bi-star${item.is_starred ? '-fill' : ''}" style="font-size:11px; ${item.is_starred ? 'color:#ffc107;' : ''}"></i>
</button>
@@ -3860,6 +3885,17 @@ async function toggleStar(id) {
}
}
// 切换置顶状态
async function togglePin(id) {
// 离线检查
if (!checkOnlineBeforeAction('切换置顶状态')) return;
const res = await fetch(`${API_BASE}/items/${id}/pin`, { method: 'POST' });
if (res.ok) {
refreshData();
}
}
// 工具函数
function getTypeIcon(type) {
const icons = { text: '📝', link: '🔗', column: '📰', todo: '' };

View File

@@ -187,6 +187,15 @@ class Database:
cursor.execute("CREATE INDEX IF NOT EXISTS idx_folders_type ON folders(type)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_folders_parent ON folders(parent_id)")
# 检查并添加 is_pinned 字段(置顶功能,兼容旧数据库)
try:
cursor.execute("SELECT is_pinned FROM items LIMIT 1")
except sqlite3.OperationalError:
cursor.execute("ALTER TABLE items ADD COLUMN is_pinned INTEGER DEFAULT 0")
# 创建 is_pinned 索引
cursor.execute("CREATE INDEX IF NOT EXISTS idx_items_pinned ON items(is_pinned)")
# 待办事务表关联到文本类别的items
cursor.execute("""
CREATE TABLE IF NOT EXISTS todo_events (
@@ -305,18 +314,16 @@ class Database:
if conditions:
query += " WHERE " + " AND ".join(conditions)
# 排序逻辑
# 排序逻辑:置顶 > 关注 > 创建时间
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':
@@ -324,12 +331,12 @@ class Database:
else:
order_dir = 'DESC' # 默认降序
# 如果有指定排序字段,按该字段排序;否则默认重点关注优先
# 如果有指定排序字段,按该字段排序,但置顶始终优先
if sort_by:
query += f" ORDER BY {order_field} {order_dir} LIMIT ? OFFSET ?"
query += f" ORDER BY i.is_pinned DESC, {order_field} {order_dir} LIMIT ? OFFSET ?"
else:
# 默认:重点关注优先,然后创建时间降序
query += f" ORDER BY i.is_starred DESC, i.created_at DESC LIMIT ? OFFSET ?"
# 默认:置顶 > 关注 > 创建时间降序
query += f" ORDER BY i.is_pinned DESC, i.is_starred DESC, i.created_at DESC LIMIT ? OFFSET ?"
params.extend([limit, offset])
cursor.execute(query, params)
@@ -517,6 +524,31 @@ class Database:
conn.commit()
return cursor.rowcount > 0
def toggle_pin(self, item_id: int) -> bool:
"""切换置顶状态"""
with self.get_conn() as conn:
cursor = conn.cursor()
# 先获取当前状态
cursor.execute("SELECT is_pinned FROM items WHERE id = ?", (item_id,))
row = cursor.fetchone()
if not row:
return False
new_status = 0 if row['is_pinned'] else 1
now = datetime.now().isoformat()
cursor.execute("UPDATE items SET is_pinned = ?, updated_at = ? WHERE id = ?", (new_status, now, item_id))
conn.commit()
return True
def set_pin(self, item_id: int, pinned: bool = True) -> bool:
"""设置置顶状态"""
with self.get_conn() as conn:
cursor = conn.cursor()
now = datetime.now().isoformat()
cursor.execute("UPDATE items SET is_pinned = ?, updated_at = ? WHERE id = ?", (1 if pinned else 0, now, item_id))
conn.commit()
return cursor.rowcount > 0
def increment_views(self, item_id: int) -> bool:
"""增加阅读数"""
with self.get_conn() as conn: