feat: 文件夹功能

- 数据库添加 folders 表和 items.folder_id 字段
- API 新增文件夹 CRUD 接口和条目移动接口
- 侧边栏每个类别下显示文件夹列表
- 新建文件夹按钮和模态框
- 条目卡片添加「移动到文件夹」按钮
- 点击文件夹过滤显示该文件夹下的数据
This commit is contained in:
2026-04-21 22:23:20 +08:00
parent ccbd24be11
commit 4783e9d88e
2 changed files with 509 additions and 19 deletions

View File

@@ -24,12 +24,21 @@ def list_items():
elif starred_param == 'false' or starred_param == '0':
starred = False
# 文件夹ID参数
folder_id_param = request.args.get('folder_id')
folder_id = None
if folder_id_param == '-1':
folder_id = -1 # 未分类
elif folder_id_param:
folder_id = int(folder_id_param)
items = db.list_items(
type=request.args.get('type'),
status=request.args.get('status'),
tag=request.args.get('tag'),
keyword=request.args.get('keyword'),
starred=starred,
folder_id=folder_id,
sort_by=request.args.get('sort_by'),
sort_order=request.args.get('sort_order'),
limit=int(request.args.get('limit', 50)),
@@ -76,7 +85,8 @@ def create_item():
due_date=data.get('due_date'),
note=data.get('note'),
tags=data.get('tags', []),
is_starred=data.get('is_starred', False)
is_starred=data.get('is_starred', False),
folder_id=data.get('folder_id')
)
item = db.get_item(item_id)
return jsonify({'success': True, 'data': item}), 201
@@ -742,6 +752,84 @@ def delete_backup(backup_name):
return jsonify({'success': False, 'error': '备份不存在'}), 404
# ============ Folder 文件夹管理 API ============
@app.route('/api/folders', methods=['GET'])
def list_folders():
"""列出文件夹"""
type_filter = request.args.get('type')
folders = db.list_folders(type=type_filter)
return jsonify({'success': True, 'data': folders})
@app.route('/api/folders', methods=['POST'])
def create_folder():
"""创建文件夹"""
data = request.get_json()
name = data.get('name', '').strip()
type_val = data.get('type', 'text')
if not name:
return jsonify({'success': False, 'error': '文件夹名称不能为空'}), 400
if type_val not in ITEM_TYPES:
return jsonify({'success': False, 'error': f'无效类型: {type_val}'}), 400
try:
folder_id = db.create_folder(name=name, type=type_val)
folder = db.get_folder(folder_id)
return jsonify({'success': True, 'data': folder}), 201
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/folders/<int:folder_id>', methods=['GET'])
def get_folder(folder_id):
"""获取文件夹"""
folder = db.get_folder(folder_id)
if not folder:
return jsonify({'success': False, 'error': '文件夹不存在'}), 404
return jsonify({'success': True, 'data': folder})
@app.route('/api/folders/<int:folder_id>', methods=['PUT'])
def update_folder(folder_id):
"""更新文件夹"""
data = request.get_json()
name = data.get('name', '').strip()
if not name:
return jsonify({'success': False, 'error': '文件夹名称不能为空'}), 400
if db.update_folder(folder_id, name=name):
folder = db.get_folder(folder_id)
return jsonify({'success': True, 'data': folder})
return jsonify({'success': False, 'error': '文件夹不存在'}), 404
@app.route('/api/folders/<int:folder_id>', methods=['DELETE'])
def delete_folder(folder_id):
"""删除文件夹"""
move_to_root = request.args.get('move_items', 'true') == 'true'
if db.delete_folder(folder_id, move_items_to_root=move_to_root):
return jsonify({'success': True})
return jsonify({'success': False, 'error': '文件夹不存在'}), 404
@app.route('/api/items/<int:item_id>/move', methods=['POST'])
def move_item_to_folder(item_id):
"""将条目移动到文件夹"""
data = request.get_json()
folder_id = data.get('folder_id') # None 表示移出文件夹
if db.move_item_to_folder(item_id, folder_id):
item = db.get_item(item_id)
return jsonify({'success': True, 'data': item})
return jsonify({'success': False, 'error': '条目不存在'}), 404
# ============ Web 页面 ============
@app.route('/')
@@ -841,6 +929,30 @@ INDEX_TEMPLATE = '''
opacity: 0.8;
}
/* 文件夹列表样式 */
.sidebar-section .section-header { font-weight: 500; }
.folder-list {
padding-left: 10px;
max-height: 150px;
overflow-y: auto;
}
.folder-list a {
padding: 6px 20px;
font-size: 13px;
color: #adb5bd;
}
.folder-list a:hover, .folder-list a.active {
background: #495057;
color: #fff;
}
.folder-list a i { margin-right: 5px; }
.folder-action {
font-size: 12px;
color: #6c757d;
padding: 4px 20px;
}
.folder-action:hover { color: #adb5bd; }
/* 离线遮罩 */
.offline-overlay {
position: fixed;
@@ -927,10 +1039,35 @@ INDEX_TEMPLATE = '''
<nav>
<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="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="todo"><i class="bi bi-check2-square"></i> 待办</a>
<!-- 文本类别 -->
<div class="sidebar-section">
<a href="#" data-filter="text" class="section-header"><i class="bi bi-file-text"></i> 文本</a>
<div class="folder-list" id="folderList-text"></div>
<a href="#" class="folder-action" onclick="showNewFolderModal('text'); return false;"><i class="bi bi-folder-plus"></i> 新建文件夹</a>
</div>
<!-- 链接类别 -->
<div class="sidebar-section">
<a href="#" data-filter="link" class="section-header"><i class="bi bi-link-45deg"></i> 链接</a>
<div class="folder-list" id="folderList-link"></div>
<a href="#" class="folder-action" onclick="showNewFolderModal('link'); return false;"><i class="bi bi-folder-plus"></i> 新建文件夹</a>
</div>
<!-- 专栏类别 -->
<div class="sidebar-section">
<a href="#" data-filter="column" class="section-header"><i class="bi bi-newspaper"></i> 专栏</a>
<div class="folder-list" id="folderList-column"></div>
<a href="#" class="folder-action" onclick="showNewFolderModal('column'); return false;"><i class="bi bi-folder-plus"></i> 新建文件夹</a>
</div>
<!-- 待办类别 -->
<div class="sidebar-section">
<a href="#" data-filter="todo" class="section-header"><i class="bi bi-check2-square"></i> 待办</a>
<div class="folder-list" id="folderList-todo"></div>
<a href="#" class="folder-action" onclick="showNewFolderModal('todo'); return false;"><i class="bi bi-folder-plus"></i> 新建文件夹</a>
</div>
<hr class="border-secondary">
<a href="#" data-filter="pending"><i class="bi bi-clock"></i> 待处理</a>
<a href="#" data-filter="in_progress"><i class="bi bi-arrow-repeat"></i> 进行中</a>
@@ -1509,15 +1646,73 @@ INDEX_TEMPLATE = '''
</div>
</div>
<!-- 新建文件夹模态框 -->
<div class="modal fade" id="newFolderModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-folder-plus"></i> 新建文件夹</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="newFolderType">
<div class="mb-3">
<label class="form-label">文件夹名称</label>
<input type="text" id="newFolderName" class="form-control" placeholder="输入文件夹名称">
</div>
<div class="mb-3">
<label class="form-label">所属类别</label>
<input type="text" id="newFolderTypeName" class="form-control" readonly>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="createFolder()">
<i class="bi bi-check"></i> 创建
</button>
</div>
</div>
</div>
</div>
<!-- 移动到文件夹模态框 -->
<div class="modal fade" id="moveToFolderModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-folder2-open"></i> 移动到文件夹</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="moveItemId">
<div class="mb-3">
<label class="form-label">选择目标文件夹</label>
<select id="moveFolderSelect" class="form-select">
<option value="">-- 移出文件夹(未分类)--</option>
</select>
</div>
<div id="moveFolderList" class="border rounded p-2 bg-light" style="max-height: 200px; overflow-y: auto;">
<!-- 动态填充文件夹列表 -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="moveItemToFolder()">
<i class="bi bi-check"></i> 移动
</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
const API_BASE = '/api';
let currentFilter = { type: '', status: '', starred: null };
let currentFilter = { type: '', status: '', starred: null, folder_id: null };
let currentSort = { sort_by: '', sort_order: '' };
let currentPage = 1;
const pageSize = 20;
// ============ 连接状态检测 ============
let allFolders = {}; // 按类型存储文件夹
let isOnline = true;
let connectionCheckTimer = null;
const CONNECTION_CHECK_INTERVAL = 5000; // 5秒检测一次
@@ -1649,11 +1844,12 @@ document.addEventListener('DOMContentLoaded', async () => {
// 确保初始状态清空
document.getElementById('searchInput').value = '';
document.getElementById('typeFilter').value = '';
currentFilter = { type: '', status: '', starred: null };
currentFilter = { type: '', status: '', starred: null, folder_id: null };
await loadStats(); // 先加载统计,确保总数可用
loadItems();
loadTags();
loadFolders(); // 加载文件夹
loadReminders(); // 加载提醒
// 定时刷新提醒每5分钟
@@ -1699,13 +1895,13 @@ document.addEventListener('DOMContentLoaded', async () => {
const filter = a.dataset.filter;
if (filter === 'starred') {
currentFilter = { type: '', status: '', starred: true };
currentFilter = { type: '', status: '', starred: true, folder_id: null };
} else if (['text', 'link', 'column', 'todo'].includes(filter)) {
currentFilter = { type: filter, status: '', starred: null };
currentFilter = { type: filter, status: '', starred: null, folder_id: null };
} else if (['pending', 'in_progress', 'completed'].includes(filter)) {
currentFilter = { type: 'todo', status: filter, starred: null };
currentFilter = { type: 'todo', status: filter, starred: null, folder_id: null };
} else {
currentFilter = { type: '', status: '', starred: null };
currentFilter = { type: '', status: '', starred: null, folder_id: null };
}
loadItems();
});
@@ -1720,6 +1916,7 @@ async function loadItems(page = 1) {
if (currentFilter.type) url += `&type=${currentFilter.type}`;
if (currentFilter.status) url += `&status=${currentFilter.status}`;
if (currentFilter.starred !== null) url += `&starred=${currentFilter.starred ? 'true' : 'false'}`;
if (currentFilter.folder_id !== null) url += `&folder_id=${currentFilter.folder_id}`;
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}`;
@@ -1767,6 +1964,7 @@ function renderItems(items) {
<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>` : ''}
<button class="btn btn-sm btn-outline-dark py-0 px-1" onclick="showMoveToFolderModal(${item.id}, '${item.type}')" title="移动到文件夹"><i class="bi bi-folder2-open" 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>
${item.type === 'todo' && item.status !== 'completed' ? `<button class="btn btn-sm btn-outline-success py-0 px-1" onclick="completeItem(${item.id})" title="完成"><i class="bi bi-check-lg" style="font-size:11px;"></i></button>` : ''}
@@ -3154,6 +3352,152 @@ async function emptyTrash() {
}
}
// ============ Folder 文件夹管理 ============
async function loadFolders() {
const res = await fetch(`${API_BASE}/folders`);
const data = await res.json();
if (data.success) {
// 按类型分组
allFolders = {};
data.data.forEach(f => {
if (!allFolders[f.type]) allFolders[f.type] = [];
allFolders[f.type].push(f);
});
// 渲染每个类型的文件夹列表
['text', 'link', 'column', 'todo'].forEach(type => {
renderFolderList(type);
});
}
}
function renderFolderList(type) {
const container = document.getElementById(`folderList-${type}`);
if (!container) return;
const folders = allFolders[type] || [];
if (folders.length === 0) {
container.innerHTML = '';
return;
}
container.innerHTML = folders.map(f => `
<a href="#" data-folder="${f.id}" onclick="filterByFolder('${type}', ${f.id}); return false;">
<i class="bi bi-folder"></i> ${f.name} <small class="text-muted">(${f.item_count || 0})</small>
</a>
`).join('');
}
async function showNewFolderModal(type) {
// 离线检查
if (!checkOnlineBeforeAction('新建文件夹')) return;
document.getElementById('newFolderType').value = type;
document.getElementById('newFolderName').value = '';
const typeLabels = { text: '📝 文本', link: '🔗 链接', column: '📰 专栏', todo: '✅ 待办' };
document.getElementById('newFolderTypeName').value = typeLabels[type] || type;
new bootstrap.Modal(document.getElementById('newFolderModal')).show();
}
async function createFolder() {
const type = document.getElementById('newFolderType').value;
const name = document.getElementById('newFolderName').value.trim();
if (!name) {
alert('请输入文件夹名称');
return;
}
const res = await fetch(`${API_BASE}/folders`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, type })
});
const data = await res.json();
if (data.success) {
bootstrap.Modal.getInstance(document.getElementById('newFolderModal')).hide();
loadFolders();
loadStats();
} else {
alert('创建失败: ' + data.error);
}
}
function filterByFolder(type, folderId) {
// 更新侧边栏选中状态
document.querySelectorAll('.sidebar a').forEach(a => a.classList.remove('active'));
document.querySelector(`a[data-folder="${folderId}"]`)?.classList.add('active');
// 设置过滤条件
currentFilter = { type, status: '', starred: null, folder_id: folderId };
loadItems(1);
}
// 显示移动到文件夹模态框
async function showMoveToFolderModal(itemId, itemType) {
// 离线检查
if (!checkOnlineBeforeAction('移动数据')) return;
document.getElementById('moveItemId').value = itemId;
// 加载该类型的文件夹列表
const res = await fetch(`${API_BASE}/folders?type=${itemType}`);
const data = await res.json();
if (data.success) {
const select = document.getElementById('moveFolderSelect');
const listContainer = document.getElementById('moveFolderList');
select.innerHTML = '<option value="">-- 移出文件夹(未分类)--</option>' +
data.data.map(f => `<option value="${f.id}">${f.name} (${f.item_count || 0}条数据)</option>`).join('');
// 可点击的列表
listContainer.innerHTML = data.data.map(f => `
<div class="p-2 border-bottom" style="cursor: pointer;" onclick="selectMoveFolder(${f.id}, '${f.name}')">
<i class="bi bi-folder"></i> ${f.name} <span class="text-muted small">(${f.item_count || 0}条)</span>
</div>
`).join('');
if (data.data.length === 0) {
listContainer.innerHTML = '<div class="text-center text-muted py-2">该类型暂无文件夹</div>';
}
}
new bootstrap.Modal(document.getElementById('moveToFolderModal')).show();
}
function selectMoveFolder(folderId, folderName) {
document.getElementById('moveFolderSelect').value = folderId;
}
async function moveItemToFolder() {
const itemId = document.getElementById('moveItemId').value;
const folderId = document.getElementById('moveFolderSelect').value || null;
const res = await fetch(`${API_BASE}/items/${itemId}/move`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ folder_id: folderId ? parseInt(folderId) : null })
});
const data = await res.json();
if (data.success) {
bootstrap.Modal.getInstance(document.getElementById('moveToFolderModal')).hide();
loadFolders(); // 更新文件夹计数
loadItems(currentPage);
} else {
alert('移动失败: ' + data.error);
}
}
// ============ 备份管理 ============
async function showBackupManager() {

View File

@@ -131,6 +131,19 @@ class Database:
)
""")
# 文件夹表
cursor.execute("""
CREATE TABLE IF NOT EXISTS folders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
type TEXT NOT NULL,
parent_id INTEGER DEFAULT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (parent_id) REFERENCES folders(id) ON DELETE CASCADE
)
""")
# 创建索引
cursor.execute("CREATE INDEX IF NOT EXISTS idx_items_type ON items(type)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_items_status ON items(status)")
@@ -163,6 +176,17 @@ class Database:
except sqlite3.OperationalError:
cursor.execute("ALTER TABLE items ADD COLUMN deleted_at TEXT")
# 检查并添加 folder_id 字段(兼容旧数据库)
try:
cursor.execute("SELECT folder_id FROM items LIMIT 1")
except sqlite3.OperationalError:
cursor.execute("ALTER TABLE items ADD COLUMN folder_id INTEGER DEFAULT NULL")
# 创建 folder 相关索引
cursor.execute("CREATE INDEX IF NOT EXISTS idx_items_folder ON items(folder_id)")
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)")
conn.commit()
# ============ Item 操作 ============
@@ -170,7 +194,8 @@ class Database:
def create_item(self, type: str = "text", title: str = None, content: str = None,
url: str = None, source: str = None, status: str = "pending",
priority: str = "medium", due_date: str = None, note: str = None,
tags: List[str] = None, is_starred: bool = False) -> int:
tags: List[str] = None, is_starred: bool = False,
folder_id: int = None) -> int:
"""创建新条目"""
self._ensure_init()
now = datetime.now().isoformat()
@@ -184,9 +209,9 @@ class Database:
with self.get_conn() as conn:
cursor = conn.cursor()
cursor.execute("""
INSERT INTO items (type, title, content, url, source, status, priority, due_date, note, is_starred, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (type, title, content, url, source, status, priority, due_date, note, 1 if is_starred else 0, now, now))
INSERT INTO items (type, title, content, url, source, status, priority, due_date, note, is_starred, folder_id, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (type, title, content, url, source, status, priority, due_date, note, 1 if is_starred else 0, folder_id, now, now))
item_id = cursor.lastrowid
# 添加标签
@@ -210,12 +235,14 @@ class Database:
return item
def list_items(self, type: str = None, status: str = None, tag: str = None,
keyword: str = None, starred: bool = None, sort_by: str = None,
sort_order: str = None, limit: int = 50, offset: int = 0) -> List[Dict[str, Any]]:
keyword: str = None, starred: bool = None, folder_id: int = 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
folder_id: 文件夹IDNone表示不限制-1表示未分类folder_id为null
"""
with self.get_conn() as conn:
cursor = conn.cursor()
@@ -245,6 +272,15 @@ class Database:
conditions.append("i.is_starred = ?")
params.append(1 if starred else 0)
# 文件夹过滤
if folder_id is not None:
if folder_id == -1:
# -1 表示未分类folder_id 为 null
conditions.append("i.folder_id IS NULL")
else:
conditions.append("i.folder_id = ?")
params.append(folder_id)
if keyword:
conditions.append("(i.title LIKE ? OR i.content LIKE ? OR i.note LIKE ?)")
keyword_pattern = f"%{keyword}%"
@@ -464,6 +500,116 @@ class Database:
conn.commit()
return cursor.rowcount > 0
def move_item_to_folder(self, item_id: int, folder_id: int) -> bool:
"""将条目移动到文件夹"""
with self.get_conn() as conn:
cursor = conn.cursor()
now = datetime.now().isoformat()
cursor.execute("UPDATE items SET folder_id = ?, updated_at = ? WHERE id = ?", (folder_id, now, item_id))
conn.commit()
return cursor.rowcount > 0
# ============ Folder 文件夹操作 ============
def create_folder(self, name: str, type: str, parent_id: int = None) -> int:
"""创建文件夹
Args:
name: 文件夹名称
type: 类别类型text/link/column/todo
parent_id: 父文件夹ID目前不支持嵌套预留
"""
self._ensure_init()
now = datetime.now().isoformat()
with self.get_conn() as conn:
cursor = conn.cursor()
cursor.execute("""
INSERT INTO folders (name, type, parent_id, created_at, updated_at)
VALUES (?, ?, ?, ?, ?)
""", (name, type, parent_id, now, now))
folder_id = cursor.lastrowid
conn.commit()
return folder_id
def get_folder(self, folder_id: int) -> Optional[Dict[str, Any]]:
"""获取文件夹信息"""
with self.get_conn() as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM folders WHERE id = ?", (folder_id,))
row = cursor.fetchone()
if not row:
return None
folder = dict(row)
# 获取文件夹内条目数量
cursor.execute("SELECT COUNT(*) FROM items WHERE folder_id = ? AND is_deleted = 0", (folder_id,))
folder['item_count'] = cursor.fetchone()[0]
return folder
def list_folders(self, type: str = None) -> List[Dict[str, Any]]:
"""列出文件夹
Args:
type: 按类型过滤None表示列出所有
"""
with self.get_conn() as conn:
cursor = conn.cursor()
if type:
cursor.execute("SELECT * FROM folders WHERE type = ? ORDER BY created_at DESC", (type,))
else:
cursor.execute("SELECT * FROM folders ORDER BY created_at DESC")
folders = []
for row in cursor.fetchall():
folder = dict(row)
# 获取文件夹内条目数量
cursor.execute("SELECT COUNT(*) FROM items WHERE folder_id = ? AND is_deleted = 0", (folder['id'],))
folder['item_count'] = cursor.fetchone()[0]
folders.append(folder)
return folders
def update_folder(self, folder_id: int, name: str = None) -> bool:
"""更新文件夹"""
with self.get_conn() as conn:
cursor = conn.cursor()
now = datetime.now().isoformat()
if name:
cursor.execute("UPDATE folders SET name = ?, updated_at = ? WHERE id = ?", (name, now, folder_id))
conn.commit()
return cursor.rowcount > 0
return False
def delete_folder(self, folder_id: int, move_items_to_root: bool = True) -> bool:
"""删除文件夹
Args:
folder_id: 文件夹ID
move_items_to_root: 是否将条目移出文件夹到未分类True则移动False则一起删除
"""
with self.get_conn() as conn:
cursor = conn.cursor()
if move_items_to_root:
# 将条目移出文件夹folder_id设为null
cursor.execute("UPDATE items SET folder_id = NULL WHERE folder_id = ?", (folder_id,))
else:
# 删除文件夹内的所有条目
cursor.execute("DELETE FROM items WHERE folder_id = ?", (folder_id,))
# 删除文件夹
cursor.execute("DELETE FROM folders WHERE id = ?", (folder_id,))
conn.commit()
return cursor.rowcount > 0
def count_items_by_folder(self, folder_id: int) -> int:
"""统计文件夹内的条目数量"""
with self.get_conn() as conn:
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM items WHERE folder_id = ? AND is_deleted = 0", (folder_id,))
return cursor.fetchone()[0]
# ============ Draft 草稿操作 ============
def save_draft(self, type: str = "text", title: str = None, content: str = None,