Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a0f8d7196 | |||
| 407a63f3cf | |||
| eec8b477be | |||
| 489e95d677 | |||
| 47cbcb25bc | |||
| 527c411d87 | |||
| c8aecaeb03 | |||
| bf63610510 | |||
| 7af3a7f21d | |||
| 4783e9d88e |
File diff suppressed because it is too large
Load Diff
169
xian_favor/db.py
169
xian_favor/db.py
@@ -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_type ON items(type)")
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_items_status ON items(status)")
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_items_status ON items(status)")
|
||||||
@@ -163,6 +176,17 @@ class Database:
|
|||||||
except sqlite3.OperationalError:
|
except sqlite3.OperationalError:
|
||||||
cursor.execute("ALTER TABLE items ADD COLUMN deleted_at TEXT")
|
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()
|
conn.commit()
|
||||||
|
|
||||||
# ============ Item 操作 ============
|
# ============ Item 操作 ============
|
||||||
@@ -170,7 +194,8 @@ 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, is_starred: bool = False) -> int:
|
tags: List[str] = None, is_starred: bool = False,
|
||||||
|
folder_id: int = None) -> int:
|
||||||
"""创建新条目"""
|
"""创建新条目"""
|
||||||
self._ensure_init()
|
self._ensure_init()
|
||||||
now = datetime.now().isoformat()
|
now = datetime.now().isoformat()
|
||||||
@@ -184,9 +209,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, is_starred, created_at, updated_at)
|
INSERT INTO items (type, title, content, url, source, status, priority, due_date, note, is_starred, folder_id, created_at, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""", (type, title, content, url, source, status, priority, due_date, note, 1 if is_starred else 0, now, now))
|
""", (type, title, content, url, source, status, priority, due_date, note, 1 if is_starred else 0, folder_id, now, now))
|
||||||
item_id = cursor.lastrowid
|
item_id = cursor.lastrowid
|
||||||
|
|
||||||
# 添加标签
|
# 添加标签
|
||||||
@@ -210,12 +235,14 @@ 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, sort_by: str = None,
|
keyword: str = None, starred: bool = None, folder_id: int = None,
|
||||||
sort_order: str = None, limit: int = 50, offset: int = 0) -> List[Dict[str, Any]]:
|
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_by: created_at, updated_at
|
||||||
sort_order: desc, asc
|
sort_order: desc, asc
|
||||||
|
folder_id: 文件夹ID,None表示不限制,-1表示未分类(folder_id为null)
|
||||||
"""
|
"""
|
||||||
with self.get_conn() as conn:
|
with self.get_conn() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
@@ -245,6 +272,15 @@ class Database:
|
|||||||
conditions.append("i.is_starred = ?")
|
conditions.append("i.is_starred = ?")
|
||||||
params.append(1 if starred else 0)
|
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:
|
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}%"
|
||||||
@@ -290,7 +326,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, starred: bool = None) -> int:
|
keyword: str = None, starred: bool = None, folder_id: int = None) -> int:
|
||||||
"""计算符合条件的条目总数"""
|
"""计算符合条件的条目总数"""
|
||||||
with self.get_conn() as conn:
|
with self.get_conn() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
@@ -322,6 +358,15 @@ class Database:
|
|||||||
keyword_pattern = f"%{keyword}%"
|
keyword_pattern = f"%{keyword}%"
|
||||||
params.extend([keyword_pattern, keyword_pattern, keyword_pattern])
|
params.extend([keyword_pattern, keyword_pattern, keyword_pattern])
|
||||||
|
|
||||||
|
# 文件夹过滤
|
||||||
|
if folder_id is not None:
|
||||||
|
if folder_id == -1:
|
||||||
|
# 未分类:folder_id为NULL
|
||||||
|
conditions.append("i.folder_id IS NULL")
|
||||||
|
else:
|
||||||
|
conditions.append("i.folder_id = ?")
|
||||||
|
params.append(folder_id)
|
||||||
|
|
||||||
if conditions:
|
if conditions:
|
||||||
query += " WHERE " + " AND ".join(conditions)
|
query += " WHERE " + " AND ".join(conditions)
|
||||||
|
|
||||||
@@ -464,6 +509,116 @@ class Database:
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
return cursor.rowcount > 0
|
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 草稿操作 ============
|
# ============ Draft 草稿操作 ============
|
||||||
|
|
||||||
def save_draft(self, type: str = "text", title: str = None, content: str = None,
|
def save_draft(self, type: str = "text", title: str = None, content: str = None,
|
||||||
|
|||||||
Reference in New Issue
Block a user