Compare commits

..

4 Commits

Author SHA1 Message Date
c3791ce961 fix: 重点关注图标样式优化,只有五角星变实心,按钮保持outline样式 2026-04-19 09:03:44 +08:00
27e24e2a86 fix: 修复数据库初始化索引创建顺序 2026-04-19 00:13:35 +08:00
0864c99b75 feat: 新增重点关注功能
- 数据库新增 is_starred 字段,兼容旧数据库自动添加
- 所有类别数据支持一键设置/取消重点关注
- 侧边栏新增"重点关注"过滤选项,重点关注数据优先显示
- 新增数据时可直接勾选"设为重点关注"开关
- 编辑时可切换重点关注状态
- 卡片显示重点关注标记(星标图标)和特殊样式
- API新增 /api/items/<id>/star 接口用于切换重点关注状态
- 重点关注数据按创建时间倒序排列并优先显示
2026-04-19 00:11:24 +08:00
0c6057de28 feat: 内容统计信息显示(有效行数/总字数) 2026-04-17 10:51:58 +08:00
2 changed files with 172 additions and 20 deletions

View File

@@ -17,21 +17,34 @@ CORS(app)
@app.route('/api/items', methods=['GET']) @app.route('/api/items', methods=['GET'])
def list_items(): def list_items():
"""列出条目""" """列出条目"""
starred_param = request.args.get('starred')
starred = None
if starred_param == 'true' or starred_param == '1':
starred = True
elif starred_param == 'false' or starred_param == '0':
starred = False
items = db.list_items( items = db.list_items(
type=request.args.get('type'), type=request.args.get('type'),
status=request.args.get('status'), status=request.args.get('status'),
tag=request.args.get('tag'), tag=request.args.get('tag'),
keyword=request.args.get('keyword'), keyword=request.args.get('keyword'),
starred=starred,
limit=int(request.args.get('limit', 50)), limit=int(request.args.get('limit', 50)),
offset=int(request.args.get('offset', 0)) offset=int(request.args.get('offset', 0))
) )
# 为每个条目添加内容统计
for item in items:
item['content_stats'] = calculate_content_stats(item)
# 获取符合条件的总数(用于分页) # 获取符合条件的总数(用于分页)
total = db.count_items( total = db.count_items(
type=request.args.get('type'), type=request.args.get('type'),
status=request.args.get('status'), status=request.args.get('status'),
tag=request.args.get('tag'), tag=request.args.get('tag'),
keyword=request.args.get('keyword') keyword=request.args.get('keyword'),
starred=starred
) )
return jsonify({'success': True, 'data': items, 'total': total}) return jsonify({'success': True, 'data': items, 'total': total})
@@ -60,7 +73,8 @@ def create_item():
priority=data.get('priority', 'medium'), priority=data.get('priority', 'medium'),
due_date=data.get('due_date'), due_date=data.get('due_date'),
note=data.get('note'), note=data.get('note'),
tags=data.get('tags', []) tags=data.get('tags', []),
is_starred=data.get('is_starred', False)
) )
item = db.get_item(item_id) item = db.get_item(item_id)
return jsonify({'success': True, 'data': item}), 201 return jsonify({'success': True, 'data': item}), 201
@@ -77,9 +91,33 @@ def get_item(item_id):
# 获取邮件发送历史 # 获取邮件发送历史
email_logs = db.get_email_logs(item_id) email_logs = db.get_email_logs(item_id)
item['email_logs'] = email_logs item['email_logs'] = email_logs
# 添加内容统计
item['content_stats'] = calculate_content_stats(item)
return jsonify({'success': True, 'data': item}) return jsonify({'success': True, 'data': item})
def calculate_content_stats(item):
"""计算内容统计:有效行数、总字数"""
# 合计所有文本内容
all_text = ''
if item.get('content'):
all_text += item['content'] + '\n'
if item.get('note'):
all_text += item['note'] + '\n'
if not all_text.strip():
return {'lines': 0, 'chars': 0}
# 有效行数(去除空行)
lines = [line for line in all_text.split('\n') if line.strip()]
line_count = len(lines)
# 总字数(去除空格后的字符数)
char_count = len(all_text.replace(' ', '').replace('\n', '').replace('\t', '').strip())
return {'lines': line_count, 'chars': char_count}
@app.route('/api/items/<int:item_id>', methods=['PUT']) @app.route('/api/items/<int:item_id>', methods=['PUT'])
def update_item(item_id): def update_item(item_id):
"""更新条目""" """更新条目"""
@@ -106,6 +144,25 @@ def delete_item(item_id):
return jsonify({'success': False, 'error': '条目不存在'}), 404 return jsonify({'success': False, 'error': '条目不存在'}), 404
@app.route('/api/items/<int:item_id>/star', methods=['POST'])
def toggle_star_item(item_id):
"""切换重点关注状态"""
if db.toggle_star(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>/star/<int:status>', methods=['POST'])
def set_star_item(item_id, status):
"""设置重点关注状态 (status: 1=重点关注, 0=取消重点关注)"""
starred = status == 1
if db.set_star(item_id, starred):
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']) @app.route('/api/items/<int:item_id>/done', methods=['POST'])
def complete_item(item_id): def complete_item(item_id):
"""完成待办""" """完成待办"""
@@ -596,6 +653,9 @@ INDEX_TEMPLATE = '''
.type-link { border-left: 4px solid #28a745; } .type-link { border-left: 4px solid #28a745; }
.type-column { border-left: 4px solid #6f42c1; } .type-column { border-left: 4px solid #6f42c1; }
.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:hover { background: #fff9e0; }
.star-btn { font-size: 11px; }
.status-pending { color: #ffc107; } .status-pending { color: #ffc107; }
.status-in_progress { color: #17a2b8; } .status-in_progress { color: #17a2b8; }
.status-completed { color: #28a745; text-decoration: line-through; } .status-completed { color: #28a745; text-decoration: line-through; }
@@ -615,6 +675,7 @@ INDEX_TEMPLATE = '''
</div> </div>
<nav> <nav>
<a href="#" class="active" data-filter="all"><i class="bi bi-inbox"></i> 全部</a> <a href="#" class="active" data-filter="all"><i class="bi bi-inbox"></i> 全部</a>
<a href="#" data-filter="starred"><i class="bi bi-star-fill" style="color:#ffc107;"></i> 重点关注</a>
<a href="#" data-filter="text"><i class="bi bi-file-text"></i> 文本</a> <a href="#" data-filter="text"><i class="bi bi-file-text"></i> 文本</a>
<a href="#" data-filter="link"><i class="bi bi-link-45deg"></i> 链接</a> <a href="#" data-filter="link"><i class="bi bi-link-45deg"></i> 链接</a>
<a href="#" data-filter="column"><i class="bi bi-newspaper"></i> 专栏</a> <a href="#" data-filter="column"><i class="bi bi-newspaper"></i> 专栏</a>
@@ -784,6 +845,14 @@ INDEX_TEMPLATE = '''
<label class="form-label">详情/备注</label> <label class="form-label">详情/备注</label>
<textarea id="addNote" class="form-control" rows="5"></textarea> <textarea id="addNote" class="form-control" rows="5"></textarea>
</div> </div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="addStarred">
<label class="form-check-label" for="addStarred">
<i class="bi bi-star-fill" style="color:#ffc107;"></i> 设为重点关注
</label>
</div>
</div>
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@@ -890,6 +959,14 @@ INDEX_TEMPLATE = '''
<label class="form-label">详情/备注</label> <label class="form-label">详情/备注</label>
<textarea id="editNote" class="form-control" rows="5"></textarea> <textarea id="editNote" class="form-control" rows="5"></textarea>
</div> </div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="editStarred">
<label class="form-check-label" for="editStarred">
<i class="bi bi-star-fill" style="color:#ffc107;"></i> 重点关注
</label>
</div>
</div>
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@@ -1172,7 +1249,7 @@ INDEX_TEMPLATE = '''
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script> <script>
const API_BASE = '/api'; const API_BASE = '/api';
let currentFilter = { type: '', status: '' }; let currentFilter = { type: '', status: '', starred: null };
let currentPage = 1; let currentPage = 1;
const pageSize = 20; const pageSize = 20;
function debounce(fn, delay) { function debounce(fn, delay) {
@@ -1188,7 +1265,7 @@ document.addEventListener('DOMContentLoaded', async () => {
// 确保初始状态清空 // 确保初始状态清空
document.getElementById('searchInput').value = ''; document.getElementById('searchInput').value = '';
document.getElementById('typeFilter').value = ''; document.getElementById('typeFilter').value = '';
currentFilter = { type: '', status: '' }; currentFilter = { type: '', status: '', starred: null };
await loadStats(); // 先加载统计,确保总数可用 await loadStats(); // 先加载统计,确保总数可用
loadItems(); loadItems();
@@ -1231,12 +1308,14 @@ document.addEventListener('DOMContentLoaded', async () => {
a.classList.add('active'); a.classList.add('active');
const filter = a.dataset.filter; const filter = a.dataset.filter;
if (['text', 'link', 'column', 'todo'].includes(filter)) { if (filter === 'starred') {
currentFilter = { type: filter, status: '' }; currentFilter = { type: '', status: '', starred: true };
} else if (['text', 'link', 'column', 'todo'].includes(filter)) {
currentFilter = { type: filter, status: '', starred: null };
} else if (['pending', 'in_progress', 'completed'].includes(filter)) { } else if (['pending', 'in_progress', 'completed'].includes(filter)) {
currentFilter = { type: 'todo', status: filter }; currentFilter = { type: 'todo', status: filter, starred: null };
} else { } else {
currentFilter = { type: '', status: '' }; currentFilter = { type: '', status: '', starred: null };
} }
loadItems(); loadItems();
}); });
@@ -1250,6 +1329,7 @@ async function loadItems(page = 1) {
let url = `${API_BASE}/items?limit=${pageSize}&offset=${(page-1)*pageSize}`; let url = `${API_BASE}/items?limit=${pageSize}&offset=${(page-1)*pageSize}`;
if (currentFilter.type) url += `&type=${currentFilter.type}`; if (currentFilter.type) url += `&type=${currentFilter.type}`;
if (currentFilter.status) url += `&status=${currentFilter.status}`; if (currentFilter.status) url += `&status=${currentFilter.status}`;
if (currentFilter.starred !== null) url += `&starred=${currentFilter.starred ? 'true' : 'false'}`;
if (keyword) url += `&keyword=${encodeURIComponent(keyword)}`; if (keyword) url += `&keyword=${encodeURIComponent(keyword)}`;
const res = await fetch(url); const res = await fetch(url);
@@ -1270,16 +1350,18 @@ function renderItems(items) {
} }
container.innerHTML = items.map(item => ` container.innerHTML = items.map(item => `
<div class="card type-${item.type} item-card" style="cursor: pointer;" onclick="showDetail(${item.id})"> <div class="card type-${item.type} item-card ${item.is_starred ? 'is-starred' : ''}" style="cursor: pointer;" onclick="showDetail(${item.id})">
<div class="card-body"> <div class="card-body">
<div class="d-flex justify-content-between align-items-start"> <div class="d-flex justify-content-between align-items-start">
<div style="flex: 1; min-width: 0;"> <div style="flex: 1; min-width: 0;">
<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' : ''}">
${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;">
${formatShortDate(item.created_at)}${item.updated_at && item.updated_at !== item.created_at ? '' + formatShortDate(item.updated_at) : ''} ${formatShortDate(item.created_at)}${item.updated_at && item.updated_at !== item.created_at ? '' + formatShortDate(item.updated_at) : ''}
</span> </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>` : ''}
</h6> </h6>
<p class="card-text text-muted small mb-0 text-truncate" style="font-size:12px;"> <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) : ''} ${item.url ? truncate(item.url, 50) : item.content ? truncate(item.content, 50) : item.note ? truncate(item.note, 50) : ''}
@@ -1288,6 +1370,9 @@ function renderItems(items) {
</div> </div>
<div class="d-flex align-items-center gap-1 flex-wrap ms-2" onclick="event.stopPropagation();"> <div class="d-flex align-items-center gap-1 flex-wrap ms-2" onclick="event.stopPropagation();">
${item.tags.slice(0, 2).map(t => `<span class="badge bg-secondary" style="font-size:10px;">${t}</span>`).join('')} ${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-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>
${item.type !== 'todo' ? `<button class="btn btn-sm btn-outline-secondary py-0 px-1" onclick="showConvertModal(${item.id})" title="转为待办"><i class="bi bi-arrow-repeat" style="font-size:11px;"></i></button>` : ''} ${item.type !== 'todo' ? `<button class="btn btn-sm btn-outline-secondary py-0 px-1" onclick="showConvertModal(${item.id})" title="转为待办"><i class="bi bi-arrow-repeat" style="font-size:11px;"></i></button>` : ''}
<button class="btn btn-sm btn-outline-info py-0 px-1" onclick="showSendEmailModal(${item.id})" title="发送邮件"><i class="bi bi-envelope" style="font-size:11px;"></i></button> <button class="btn btn-sm btn-outline-info py-0 px-1" onclick="showSendEmailModal(${item.id})" title="发送邮件"><i class="bi bi-envelope" style="font-size:11px;"></i></button>
<button class="btn btn-sm btn-outline-primary py-0 px-1" onclick="openEditModal(${item.id})" title="编辑"><i class="bi bi-pencil" style="font-size:11px;"></i></button> <button class="btn btn-sm btn-outline-primary py-0 px-1" onclick="openEditModal(${item.id})" title="编辑"><i class="bi bi-pencil" style="font-size:11px;"></i></button>
@@ -1409,7 +1494,8 @@ async function addItem() {
priority: type === 'todo' ? document.getElementById('addPriority').value : null, priority: type === 'todo' ? document.getElementById('addPriority').value : null,
due_date: type === 'todo' ? document.getElementById('addDueDate').value : null, due_date: type === 'todo' ? document.getElementById('addDueDate').value : null,
note: document.getElementById('addNote').value, note: document.getElementById('addNote').value,
tags: document.getElementById('addTags').value.split(',').map(t => t.trim()).filter(t => t) tags: document.getElementById('addTags').value.split(',').map(t => t.trim()).filter(t => t),
is_starred: document.getElementById('addStarred').checked
}; };
const res = await fetch(`${API_BASE}/items`, { const res = await fetch(`${API_BASE}/items`, {
@@ -1547,6 +1633,16 @@ async function showDetail(id) {
let html = `<div class="mb-3"><strong>类型:</strong> ${getTypeLabel(item.type)}</div>`; let html = `<div class="mb-3"><strong>类型:</strong> ${getTypeLabel(item.type)}</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>`;
}
// 显示内容统计
if (item.content_stats && (item.content_stats.lines > 0 || item.content_stats.chars > 0)) {
html += `<div class="mb-3"><strong>统计:</strong> <span class="badge bg-info">${item.content_stats.lines} 有效行 / ${item.content_stats.chars} 字</span></div>`;
}
if (item.url) { if (item.url) {
html += `<div class="mb-3"><strong>URL:</strong> <a href="${item.url}" target="_blank">${item.url}</a></div>`; html += `<div class="mb-3"><strong>URL:</strong> <a href="${item.url}" target="_blank">${item.url}</a></div>`;
} }
@@ -1652,6 +1748,9 @@ async function openEditModal(id) {
} }
} }
// 设置重点关注状态
document.getElementById('editStarred').checked = item.is_starred === 1;
new bootstrap.Modal(document.getElementById('editModal')).show(); new bootstrap.Modal(document.getElementById('editModal')).show();
} }
@@ -1678,7 +1777,8 @@ async function saveEdit() {
priority: type === 'todo' ? document.getElementById('editPriority').value : null, priority: type === 'todo' ? document.getElementById('editPriority').value : null,
due_date: type === 'todo' ? document.getElementById('editDueDate').value : null, due_date: type === 'todo' ? document.getElementById('editDueDate').value : null,
note: document.getElementById('editNote').value, note: document.getElementById('editNote').value,
tags: document.getElementById('editTags').value.split(',').map(t => t.trim()).filter(t => t) tags: document.getElementById('editTags').value.split(',').map(t => t.trim()).filter(t => t),
is_starred: document.getElementById('editStarred').checked ? 1 : 0
}; };
const res = await fetch(`${API_BASE}/items/${id}`, { const res = await fetch(`${API_BASE}/items/${id}`, {
@@ -1693,6 +1793,14 @@ async function saveEdit() {
} }
} }
// 切换重点关注状态
async function toggleStar(id) {
const res = await fetch(`${API_BASE}/items/${id}/star`, { method: 'POST' });
if (res.ok) {
refreshData();
}
}
// 工具函数 // 工具函数
function getTypeIcon(type) { function getTypeIcon(type) {
const icons = { text: '📝', link: '🔗', column: '📰', todo: '' }; const icons = { text: '📝', link: '🔗', column: '📰', todo: '' };

View File

@@ -59,6 +59,7 @@ class Database:
priority TEXT DEFAULT 'medium', priority TEXT DEFAULT 'medium',
due_date TEXT, due_date TEXT,
note TEXT, note TEXT,
is_starred INTEGER DEFAULT 0,
created_at TEXT NOT NULL, created_at TEXT NOT NULL,
updated_at TEXT NOT NULL updated_at TEXT NOT NULL
) )
@@ -114,6 +115,15 @@ class Database:
cursor.execute("CREATE INDEX IF NOT EXISTS idx_item_tags_item ON item_tags(item_id)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_item_tags_item ON item_tags(item_id)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_item_tags_tag ON item_tags(tag_id)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_item_tags_tag ON item_tags(tag_id)")
# 检查并添加 is_starred 字段(兼容旧数据库)
try:
cursor.execute("SELECT is_starred FROM items LIMIT 1")
except sqlite3.OperationalError:
cursor.execute("ALTER TABLE items ADD COLUMN is_starred INTEGER DEFAULT 0")
# 创建 is_starred 索引(字段添加后再创建)
cursor.execute("CREATE INDEX IF NOT EXISTS idx_items_starred ON items(is_starred)")
conn.commit() conn.commit()
# ============ Item 操作 ============ # ============ Item 操作 ============
@@ -121,7 +131,7 @@ class Database:
def create_item(self, type: str = "text", title: str = None, content: str = None, def create_item(self, type: str = "text", title: str = None, content: str = None,
url: str = None, source: str = None, status: str = "pending", url: str = None, source: str = None, status: str = "pending",
priority: str = "medium", due_date: str = None, note: str = None, priority: str = "medium", due_date: str = None, note: str = None,
tags: List[str] = None) -> int: tags: List[str] = None, is_starred: bool = False) -> int:
"""创建新条目""" """创建新条目"""
self._ensure_init() self._ensure_init()
now = datetime.now().isoformat() now = datetime.now().isoformat()
@@ -135,9 +145,9 @@ class Database:
with self.get_conn() as conn: with self.get_conn() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cursor.execute("""
INSERT INTO items (type, title, content, url, source, status, priority, due_date, note, created_at, updated_at) INSERT INTO items (type, title, content, url, source, status, priority, due_date, note, is_starred, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (type, title, content, url, source, status, priority, due_date, note, now, now)) """, (type, title, content, url, source, status, priority, due_date, note, 1 if is_starred else 0, now, now))
item_id = cursor.lastrowid item_id = cursor.lastrowid
# 添加标签 # 添加标签
@@ -161,7 +171,7 @@ class Database:
return item return item
def list_items(self, type: str = None, status: str = None, tag: str = None, def list_items(self, type: str = None, status: str = None, tag: str = None,
keyword: str = None, limit: int = 50, offset: int = 0) -> List[Dict[str, Any]]: keyword: str = None, starred: bool = None, limit: int = 50, offset: int = 0) -> List[Dict[str, Any]]:
"""列出条目""" """列出条目"""
with self.get_conn() as conn: with self.get_conn() as conn:
cursor = conn.cursor() cursor = conn.cursor()
@@ -184,6 +194,10 @@ class Database:
conditions.append("i.status = ?") conditions.append("i.status = ?")
params.append(status) params.append(status)
if starred is not None:
conditions.append("i.is_starred = ?")
params.append(1 if starred else 0)
if keyword: if keyword:
conditions.append("(i.title LIKE ? OR i.content LIKE ? OR i.note LIKE ?)") conditions.append("(i.title LIKE ? OR i.content LIKE ? OR i.note LIKE ?)")
keyword_pattern = f"%{keyword}%" keyword_pattern = f"%{keyword}%"
@@ -192,7 +206,8 @@ class Database:
if conditions: if conditions:
query += " WHERE " + " AND ".join(conditions) query += " WHERE " + " AND ".join(conditions)
query += " ORDER BY i.created_at DESC LIMIT ? OFFSET ?" # 重点关注优先显示
query += " ORDER BY i.is_starred DESC, i.created_at DESC LIMIT ? OFFSET ?"
params.extend([limit, offset]) params.extend([limit, offset])
cursor.execute(query, params) cursor.execute(query, params)
@@ -205,7 +220,7 @@ class Database:
return items return items
def count_items(self, type: str = None, status: str = None, tag: str = None, def count_items(self, type: str = None, status: str = None, tag: str = None,
keyword: str = None) -> int: keyword: str = None, starred: bool = None) -> int:
"""计算符合条件的条目总数""" """计算符合条件的条目总数"""
with self.get_conn() as conn: with self.get_conn() as conn:
cursor = conn.cursor() cursor = conn.cursor()
@@ -228,6 +243,10 @@ class Database:
conditions.append("i.status = ?") conditions.append("i.status = ?")
params.append(status) params.append(status)
if starred is not None:
conditions.append("i.is_starred = ?")
params.append(1 if starred else 0)
if keyword: if keyword:
conditions.append("(i.title LIKE ? OR i.content LIKE ? OR i.note LIKE ?)") conditions.append("(i.title LIKE ? OR i.content LIKE ? OR i.note LIKE ?)")
keyword_pattern = f"%{keyword}%" keyword_pattern = f"%{keyword}%"
@@ -241,7 +260,7 @@ class Database:
def update_item(self, item_id: int, **kwargs) -> bool: def update_item(self, item_id: int, **kwargs) -> bool:
"""更新条目""" """更新条目"""
allowed_fields = ['type', 'title', 'content', 'url', 'source', 'status', 'priority', 'due_date', 'note'] allowed_fields = ['type', 'title', 'content', 'url', 'source', 'status', 'priority', 'due_date', 'note', 'is_starred']
update_fields = {k: v for k, v in kwargs.items() if k in allowed_fields} update_fields = {k: v for k, v in kwargs.items() if k in allowed_fields}
if not update_fields and 'tags' not in kwargs: if not update_fields and 'tags' not in kwargs:
@@ -276,6 +295,31 @@ class Database:
conn.commit() conn.commit()
return cursor.rowcount > 0 return cursor.rowcount > 0
def toggle_star(self, item_id: int) -> bool:
"""切换重点关注状态"""
with self.get_conn() as conn:
cursor = conn.cursor()
# 先获取当前状态
cursor.execute("SELECT is_starred FROM items WHERE id = ?", (item_id,))
row = cursor.fetchone()
if not row:
return False
new_status = 0 if row['is_starred'] else 1
now = datetime.now().isoformat()
cursor.execute("UPDATE items SET is_starred = ?, updated_at = ? WHERE id = ?", (new_status, now, item_id))
conn.commit()
return True
def set_star(self, item_id: int, starred: bool = True) -> bool:
"""设置重点关注状态"""
with self.get_conn() as conn:
cursor = conn.cursor()
now = datetime.now().isoformat()
cursor.execute("UPDATE items SET is_starred = ?, updated_at = ? WHERE id = ?", (1 if starred else 0, now, item_id))
conn.commit()
return cursor.rowcount > 0
# ============ Tag 操作 ============ # ============ Tag 操作 ============
def create_tag(self, name: str, color: str = "#3498db") -> int: def create_tag(self, name: str, color: str = "#3498db") -> int: