Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c6057de28 | |||
| 6f20e5978d | |||
| 56ff1e8163 | |||
| 9ec479415a |
@@ -141,6 +141,13 @@ xian-favor/
|
||||
|
||||
## 版本历史
|
||||
|
||||
- **v2.4.0** (2026-04-16): 数据库备份机制
|
||||
- 自动备份:每天 04:00 执行
|
||||
- 手动备份:页面一键操作
|
||||
- 备份清理规则:保留30天 + 每月第一天永久保留
|
||||
- 手动备份最多保留10个
|
||||
- 支持恢复备份和删除备份
|
||||
- 备份管理页面入口在侧边栏
|
||||
- **v2.3.2** (2026-04-16): 搜索功能修复
|
||||
- 修复 debounce 函数定义顺序问题
|
||||
- 搜索框输入后可正常过滤列表
|
||||
|
||||
@@ -25,7 +25,20 @@ def list_items():
|
||||
limit=int(request.args.get('limit', 50)),
|
||||
offset=int(request.args.get('offset', 0))
|
||||
)
|
||||
return jsonify({'success': True, 'data': items})
|
||||
|
||||
# 为每个条目添加内容统计
|
||||
for item in items:
|
||||
item['content_stats'] = calculate_content_stats(item)
|
||||
|
||||
# 获取符合条件的总数(用于分页)
|
||||
total = db.count_items(
|
||||
type=request.args.get('type'),
|
||||
status=request.args.get('status'),
|
||||
tag=request.args.get('tag'),
|
||||
keyword=request.args.get('keyword')
|
||||
)
|
||||
|
||||
return jsonify({'success': True, 'data': items, 'total': total})
|
||||
|
||||
|
||||
@app.route('/api/items', methods=['POST'])
|
||||
@@ -68,9 +81,33 @@ def get_item(item_id):
|
||||
# 获取邮件发送历史
|
||||
email_logs = db.get_email_logs(item_id)
|
||||
item['email_logs'] = email_logs
|
||||
# 添加内容统计
|
||||
item['content_stats'] = calculate_content_stats(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'])
|
||||
def update_item(item_id):
|
||||
"""更新条目"""
|
||||
@@ -1201,8 +1238,12 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
updateEditFieldsByType(e.target.value);
|
||||
});
|
||||
|
||||
// 搜索
|
||||
document.getElementById('searchInput').addEventListener('input', debounce(loadItems, 300));
|
||||
// 搜索 - 直接绑定,不用 debounce
|
||||
let searchTimer;
|
||||
document.getElementById('searchInput').addEventListener('input', (e) => {
|
||||
clearTimeout(searchTimer);
|
||||
searchTimer = setTimeout(() => loadItems(), 300);
|
||||
});
|
||||
|
||||
// 类型过滤
|
||||
document.getElementById('typeFilter').addEventListener('change', (e) => {
|
||||
@@ -1244,7 +1285,7 @@ async function loadItems(page = 1) {
|
||||
|
||||
if (data.success) {
|
||||
renderItems(data.data);
|
||||
renderPagination(data.data.length, page);
|
||||
renderPagination(data.total, page); // 使用API返回的total
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1267,6 +1308,8 @@ function renderItems(items) {
|
||||
<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) : ''}
|
||||
</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>
|
||||
<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) : ''}
|
||||
@@ -1289,9 +1332,8 @@ function renderItems(items) {
|
||||
}
|
||||
|
||||
// 渲染分页
|
||||
function renderPagination(itemCount, page) {
|
||||
function renderPagination(total, page) {
|
||||
const container = document.getElementById('pagination');
|
||||
const total = parseInt(document.getElementById('statTotal').textContent);
|
||||
const totalPages = Math.ceil(total / pageSize);
|
||||
|
||||
if (totalPages <= 1) {
|
||||
@@ -1535,6 +1577,11 @@ async function showDetail(id) {
|
||||
|
||||
let html = `<div class="mb-3"><strong>类型:</strong> ${getTypeLabel(item.type)}</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) {
|
||||
html += `<div class="mb-3"><strong>URL:</strong> <a href="${item.url}" target="_blank">${item.url}</a></div>`;
|
||||
}
|
||||
|
||||
@@ -204,6 +204,41 @@ class Database:
|
||||
|
||||
return items
|
||||
|
||||
def count_items(self, type: str = None, status: str = None, tag: str = None,
|
||||
keyword: str = None) -> int:
|
||||
"""计算符合条件的条目总数"""
|
||||
with self.get_conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
query = "SELECT COUNT(DISTINCT i.id) as count FROM items i"
|
||||
params = []
|
||||
conditions = []
|
||||
|
||||
# 标签过滤需要JOIN
|
||||
if tag:
|
||||
query += " JOIN item_tags it ON i.id = it.item_id JOIN tags t ON it.tag_id = t.id"
|
||||
conditions.append("t.name = ?")
|
||||
params.append(tag)
|
||||
|
||||
if type:
|
||||
conditions.append("i.type = ?")
|
||||
params.append(type)
|
||||
|
||||
if status:
|
||||
conditions.append("i.status = ?")
|
||||
params.append(status)
|
||||
|
||||
if keyword:
|
||||
conditions.append("(i.title LIKE ? OR i.content LIKE ? OR i.note LIKE ?)")
|
||||
keyword_pattern = f"%{keyword}%"
|
||||
params.extend([keyword_pattern, keyword_pattern, keyword_pattern])
|
||||
|
||||
if conditions:
|
||||
query += " WHERE " + " AND ".join(conditions)
|
||||
|
||||
cursor.execute(query, params)
|
||||
return cursor.fetchone()['count']
|
||||
|
||||
def update_item(self, item_id: int, **kwargs) -> bool:
|
||||
"""更新条目"""
|
||||
allowed_fields = ['type', 'title', 'content', 'url', 'source', 'status', 'priority', 'due_date', 'note']
|
||||
|
||||
Reference in New Issue
Block a user