Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 79e4eb4de0 | |||
| 70b40cb90b |
@@ -30,6 +30,8 @@ def list_items():
|
|||||||
tag=request.args.get('tag'),
|
tag=request.args.get('tag'),
|
||||||
keyword=request.args.get('keyword'),
|
keyword=request.args.get('keyword'),
|
||||||
starred=starred,
|
starred=starred,
|
||||||
|
sort_by=request.args.get('sort_by'),
|
||||||
|
sort_order=request.args.get('sort_order'),
|
||||||
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))
|
||||||
)
|
)
|
||||||
@@ -163,6 +165,54 @@ def set_star_item(item_id, status):
|
|||||||
return jsonify({'success': False, 'error': '条目不存在'}), 404
|
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/trash', methods=['GET'])
|
||||||
|
def list_trash():
|
||||||
|
"""列出回收站数据"""
|
||||||
|
limit = int(request.args.get('limit', 50))
|
||||||
|
offset = int(request.args.get('offset', 0))
|
||||||
|
items = db.list_trash(limit=limit, offset=offset)
|
||||||
|
total = db.count_trash()
|
||||||
|
|
||||||
|
# 为每个条目添加内容统计
|
||||||
|
for item in items:
|
||||||
|
item['content_stats'] = calculate_content_stats(item)
|
||||||
|
|
||||||
|
return jsonify({'success': True, 'data': items, 'total': total})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/items/<int:item_id>/restore', methods=['POST'])
|
||||||
|
def restore_item(item_id):
|
||||||
|
"""从回收站恢复数据"""
|
||||||
|
if db.restore_item(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>/permanent', methods=['DELETE'])
|
||||||
|
def delete_permanently(item_id):
|
||||||
|
"""彻底删除数据"""
|
||||||
|
if db.delete_permanently(item_id):
|
||||||
|
return jsonify({'success': True})
|
||||||
|
return jsonify({'success': False, 'error': '条目不存在'}), 404
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/trash', methods=['DELETE'])
|
||||||
|
def empty_trash():
|
||||||
|
"""清空回收站"""
|
||||||
|
deleted_count = db.empty_trash()
|
||||||
|
return jsonify({'success': True, 'deleted_count': deleted_count})
|
||||||
|
|
||||||
|
|
||||||
@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):
|
||||||
"""完成待办"""
|
"""完成待办"""
|
||||||
@@ -688,6 +738,7 @@ INDEX_TEMPLATE = '''
|
|||||||
<a href="#" onclick="showTagManager(); return false;"><i class="bi bi-tags"></i> 标签管理</a>
|
<a href="#" onclick="showTagManager(); return false;"><i class="bi bi-tags"></i> 标签管理</a>
|
||||||
<a href="#" onclick="showEmailManager(); return false;"><i class="bi bi-envelope"></i> 邮箱管理</a>
|
<a href="#" onclick="showEmailManager(); return false;"><i class="bi bi-envelope"></i> 邮箱管理</a>
|
||||||
<a href="#" onclick="showBackupManager(); return false;"><i class="bi bi-archive"></i> 备份管理</a>
|
<a href="#" onclick="showBackupManager(); return false;"><i class="bi bi-archive"></i> 备份管理</a>
|
||||||
|
<a href="#" onclick="showTrash(); return false;"><i class="bi bi-trash"></i> 回收站</a>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -714,6 +765,15 @@ INDEX_TEMPLATE = '''
|
|||||||
<option value="column">专栏</option>
|
<option value="column">专栏</option>
|
||||||
<option value="todo">待办</option>
|
<option value="todo">待办</option>
|
||||||
</select>
|
</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>
|
</div>
|
||||||
<button class="btn btn-outline-info me-2" onclick="showAIAddModal()" title="AI自动添加">
|
<button class="btn btn-outline-info me-2" onclick="showAIAddModal()" title="AI自动添加">
|
||||||
<i class="bi bi-robot"></i> AI添加
|
<i class="bi bi-robot"></i> AI添加
|
||||||
@@ -1250,6 +1310,7 @@ INDEX_TEMPLATE = '''
|
|||||||
<script>
|
<script>
|
||||||
const API_BASE = '/api';
|
const API_BASE = '/api';
|
||||||
let currentFilter = { type: '', status: '', starred: null };
|
let currentFilter = { type: '', status: '', starred: null };
|
||||||
|
let currentSort = { sort_by: '', sort_order: '' };
|
||||||
let currentPage = 1;
|
let currentPage = 1;
|
||||||
const pageSize = 20;
|
const pageSize = 20;
|
||||||
function debounce(fn, delay) {
|
function debounce(fn, delay) {
|
||||||
@@ -1331,6 +1392,8 @@ async function loadItems(page = 1) {
|
|||||||
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 (currentFilter.starred !== null) url += `&starred=${currentFilter.starred ? 'true' : 'false'}`;
|
||||||
if (keyword) url += `&keyword=${encodeURIComponent(keyword)}`;
|
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 res = await fetch(url);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -1362,6 +1425,7 @@ function renderItems(items) {
|
|||||||
</span>
|
</span>
|
||||||
${item.content_stats && (item.content_stats.lines > 0 || item.content_stats.chars > 0) ?
|
${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>` : ''}
|
`<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>
|
</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) : ''}
|
||||||
@@ -1450,6 +1514,14 @@ async function refreshData() {
|
|||||||
loadItems(currentPage);
|
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 +1690,10 @@ let currentDetailId = null;
|
|||||||
// 显示详情
|
// 显示详情
|
||||||
async function showDetail(id) {
|
async function showDetail(id) {
|
||||||
currentDetailId = id;
|
currentDetailId = id;
|
||||||
|
|
||||||
|
// 增加阅读数
|
||||||
|
await fetch(`${API_BASE}/items/${id}/view`, { method: 'POST' });
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE}/items/${id}`);
|
const res = await fetch(`${API_BASE}/items/${id}`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
@@ -1633,6 +1709,9 @@ 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>`;
|
||||||
|
|
||||||
|
// 显示阅读数
|
||||||
|
html += `<div class="mb-3"><strong>阅读:</strong> <span class="badge bg-secondary">👁 ${item.views || 0} 次</span></div>`;
|
||||||
|
|
||||||
// 显示重点关注状态
|
// 显示重点关注状态
|
||||||
if (item.is_starred) {
|
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>`;
|
html += `<div class="mb-3"><strong>状态:</strong> <span class="badge bg-warning text-dark"><i class="bi bi-star-fill"></i> 重点关注</span></div>`;
|
||||||
@@ -2232,6 +2311,130 @@ async function showEmailManager() {
|
|||||||
new bootstrap.Modal(document.getElementById('emailManagerModal')).show();
|
new bootstrap.Modal(document.getElementById('emailManagerModal')).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ 回收站 ============
|
||||||
|
|
||||||
|
let trashView = false;
|
||||||
|
|
||||||
|
async function showTrash() {
|
||||||
|
trashView = true;
|
||||||
|
await loadTrash();
|
||||||
|
|
||||||
|
// 更新侧边栏状态
|
||||||
|
document.querySelectorAll('.sidebar a').forEach(a => a.classList.remove('active'));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTrash() {
|
||||||
|
const res = await fetch(`${API_BASE}/trash`);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
renderTrash(data.data, data.total);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTrash(items, total) {
|
||||||
|
const container = document.getElementById('itemList');
|
||||||
|
|
||||||
|
// 显示回收站标题和操作按钮
|
||||||
|
let header = `
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<div>
|
||||||
|
<h5><i class="bi bi-trash"></i> 回收站 (${total} 条数据)</h5>
|
||||||
|
<small class="text-muted">删除的数据可在30天内恢复</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-outline-secondary" onclick="hideTrash()">
|
||||||
|
<i class="bi bi-arrow-left"></i> 返回列表
|
||||||
|
</button>
|
||||||
|
${total > 0 ? `<button class="btn btn-outline-danger ms-2" onclick="emptyTrash()">
|
||||||
|
<i class="bi bi-trash-fill"></i> 清空回收站
|
||||||
|
</button>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (!items.length) {
|
||||||
|
container.innerHTML = header + '<div class="text-center text-muted py-5">回收站为空</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = header + items.map(item => `
|
||||||
|
<div class="card type-${item.type} item-card" style="opacity: 0.7;">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
|
<div style="flex: 1; min-width: 0;">
|
||||||
|
<h6 class="card-title text-truncate mb-1">
|
||||||
|
${getTypeIcon(item.type)} ${item.title || truncate(item.content || item.url, 30)}
|
||||||
|
</h6>
|
||||||
|
<p class="card-text text-muted small mb-0">
|
||||||
|
删除时间: ${formatShortDate(item.deleted_at)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-1">
|
||||||
|
<button class="btn btn-sm btn-outline-success" onclick="restoreItem(${item.id})" title="恢复">
|
||||||
|
<i class="bi bi-arrow-counterclockwise"></i> 恢复
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-danger" onclick="deletePermanently(${item.id})" title="彻底删除">
|
||||||
|
<i class="bi bi-trash-fill"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hideTrash() {
|
||||||
|
trashView = false;
|
||||||
|
refreshData();
|
||||||
|
|
||||||
|
// 更新侧边栏状态
|
||||||
|
document.querySelector('.sidebar a[data-filter="all"]').classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restoreItem(id) {
|
||||||
|
if (!confirm('确认恢复这条数据?')) return;
|
||||||
|
|
||||||
|
const res = await fetch(`${API_BASE}/items/${id}/restore`, { method: 'POST' });
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
loadTrash();
|
||||||
|
loadStats();
|
||||||
|
} else {
|
||||||
|
alert('恢复失败: ' + data.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deletePermanently(id) {
|
||||||
|
if (!confirm('确认彻底删除这条数据?此操作不可恢复!')) return;
|
||||||
|
|
||||||
|
const res = await fetch(`${API_BASE}/items/${id}/permanent`, { method: 'DELETE' });
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
loadTrash();
|
||||||
|
loadStats();
|
||||||
|
} else {
|
||||||
|
alert('删除失败: ' + data.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function emptyTrash() {
|
||||||
|
if (!confirm('确认清空回收站?此操作不可恢复!')) return;
|
||||||
|
|
||||||
|
const res = await fetch(`${API_BASE}/trash`, { method: 'DELETE' });
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
alert(`已清空回收站,删除了 ${data.deleted_count} 条数据`);
|
||||||
|
loadTrash();
|
||||||
|
loadStats();
|
||||||
|
} else {
|
||||||
|
alert('清空失败: ' + data.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============ 备份管理 ============
|
// ============ 备份管理 ============
|
||||||
|
|
||||||
async function showBackupManager() {
|
async function showBackupManager() {
|
||||||
|
|||||||
128
xian_favor/db.py
128
xian_favor/db.py
@@ -60,6 +60,9 @@ class Database:
|
|||||||
due_date TEXT,
|
due_date TEXT,
|
||||||
note TEXT,
|
note TEXT,
|
||||||
is_starred INTEGER DEFAULT 0,
|
is_starred INTEGER DEFAULT 0,
|
||||||
|
views INTEGER DEFAULT 0,
|
||||||
|
is_deleted INTEGER DEFAULT 0,
|
||||||
|
deleted_at TEXT,
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
updated_at TEXT NOT NULL
|
updated_at TEXT NOT NULL
|
||||||
)
|
)
|
||||||
@@ -124,6 +127,22 @@ class Database:
|
|||||||
# 创建 is_starred 索引(字段添加后再创建)
|
# 创建 is_starred 索引(字段添加后再创建)
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_items_starred ON items(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")
|
||||||
|
|
||||||
|
# 检查并添加 is_deleted 和 deleted_at 字段(兼容旧数据库)
|
||||||
|
try:
|
||||||
|
cursor.execute("SELECT is_deleted FROM items LIMIT 1")
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
cursor.execute("ALTER TABLE items ADD COLUMN is_deleted INTEGER DEFAULT 0")
|
||||||
|
try:
|
||||||
|
cursor.execute("SELECT deleted_at FROM items LIMIT 1")
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
cursor.execute("ALTER TABLE items ADD COLUMN deleted_at TEXT")
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
# ============ Item 操作 ============
|
# ============ Item 操作 ============
|
||||||
@@ -171,8 +190,13 @@ 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, 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:
|
with self.get_conn() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
@@ -180,6 +204,9 @@ class Database:
|
|||||||
params = []
|
params = []
|
||||||
conditions = []
|
conditions = []
|
||||||
|
|
||||||
|
# 只显示未删除的数据
|
||||||
|
conditions.append("i.is_deleted = 0")
|
||||||
|
|
||||||
# 标签过滤需要JOIN
|
# 标签过滤需要JOIN
|
||||||
if tag:
|
if tag:
|
||||||
query += " JOIN item_tags it ON i.id = it.item_id JOIN tags t ON it.tag_id = t.id"
|
query += " JOIN item_tags it ON i.id = it.item_id JOIN tags t ON it.tag_id = t.id"
|
||||||
@@ -206,8 +233,31 @@ class Database:
|
|||||||
if conditions:
|
if conditions:
|
||||||
query += " WHERE " + " AND ".join(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])
|
params.extend([limit, offset])
|
||||||
|
|
||||||
cursor.execute(query, params)
|
cursor.execute(query, params)
|
||||||
@@ -294,13 +344,73 @@ class Database:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def delete_item(self, item_id: int) -> bool:
|
def delete_item(self, item_id: int) -> bool:
|
||||||
"""删除条目"""
|
"""删除条目(移动到回收站)"""
|
||||||
with self.get_conn() as conn:
|
with self.get_conn() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
now = datetime.now().isoformat()
|
||||||
|
cursor.execute("UPDATE items SET is_deleted = 1, deleted_at = ? WHERE id = ?", (now, item_id))
|
||||||
|
conn.commit()
|
||||||
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
|
def list_trash(self, limit: int = 50, offset: int = 0) -> List[Dict[str, Any]]:
|
||||||
|
"""列出回收站数据"""
|
||||||
|
with self.get_conn() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT * FROM items WHERE is_deleted = 1 ORDER BY deleted_at DESC LIMIT ? OFFSET ?", (limit, offset))
|
||||||
|
items = []
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
item = dict(row)
|
||||||
|
item['tags'] = self._get_item_tags(conn, item['id'])
|
||||||
|
items.append(item)
|
||||||
|
return items
|
||||||
|
|
||||||
|
def count_trash(self) -> int:
|
||||||
|
"""计算回收站数据总数"""
|
||||||
|
with self.get_conn() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT COUNT(*) as count FROM items WHERE is_deleted = 1")
|
||||||
|
return cursor.fetchone()['count']
|
||||||
|
|
||||||
|
def restore_item(self, item_id: int) -> bool:
|
||||||
|
"""从回收站恢复数据"""
|
||||||
|
with self.get_conn() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("UPDATE items SET is_deleted = 0, deleted_at = NULL WHERE id = ?", (item_id,))
|
||||||
|
conn.commit()
|
||||||
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
|
def delete_permanently(self, item_id: int) -> bool:
|
||||||
|
"""彻底删除数据(从数据库中删除)"""
|
||||||
|
with self.get_conn() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
# 删除标签关联
|
||||||
|
cursor.execute("DELETE FROM item_tags WHERE item_id = ?", (item_id,))
|
||||||
|
# 删除邮件发送记录
|
||||||
|
cursor.execute("DELETE FROM email_logs WHERE item_id = ?", (item_id,))
|
||||||
|
# 删除数据
|
||||||
cursor.execute("DELETE FROM items WHERE id = ?", (item_id,))
|
cursor.execute("DELETE FROM items WHERE id = ?", (item_id,))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return cursor.rowcount > 0
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
|
def empty_trash(self) -> int:
|
||||||
|
"""清空回收站"""
|
||||||
|
with self.get_conn() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
# 获取所有回收站数据ID
|
||||||
|
cursor.execute("SELECT id FROM items WHERE is_deleted = 1")
|
||||||
|
ids = [row['id'] for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
# 删除所有关联数据
|
||||||
|
for item_id in ids:
|
||||||
|
cursor.execute("DELETE FROM item_tags WHERE item_id = ?", (item_id,))
|
||||||
|
cursor.execute("DELETE FROM email_logs WHERE item_id = ?", (item_id,))
|
||||||
|
|
||||||
|
# 删除所有回收站数据
|
||||||
|
cursor.execute("DELETE FROM items WHERE is_deleted = 1")
|
||||||
|
deleted_count = cursor.rowcount
|
||||||
|
conn.commit()
|
||||||
|
return deleted_count
|
||||||
|
|
||||||
def toggle_star(self, item_id: int) -> bool:
|
def toggle_star(self, item_id: int) -> bool:
|
||||||
"""切换重点关注状态"""
|
"""切换重点关注状态"""
|
||||||
with self.get_conn() as conn:
|
with self.get_conn() as conn:
|
||||||
@@ -326,6 +436,14 @@ class Database:
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
return cursor.rowcount > 0
|
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 操作 ============
|
# ============ Tag 操作 ============
|
||||||
|
|
||||||
def create_tag(self, name: str, color: str = "#3498db") -> int:
|
def create_tag(self, name: str, color: str = "#3498db") -> int:
|
||||||
|
|||||||
Reference in New Issue
Block a user