diff --git a/data_management/app.py b/data_management/app.py index 252af20..2748679 100644 --- a/data_management/app.py +++ b/data_management/app.py @@ -78,7 +78,9 @@ class DataManagementApp: self.preview_panel = PreviewPanel( main_container, sqlite_manager=self.sqlite_manager, - on_download_callback=self._on_download + mysql_manager=self.mysql_manager, + on_download_callback=self._on_download, + on_delete_callback=self._on_refresh ) self.preview_panel.pack(side="left", fill="both", expand=True, padx=5) diff --git a/data_management/config.py b/data_management/config.py index 323236f..f92fdec 100644 --- a/data_management/config.py +++ b/data_management/config.py @@ -71,3 +71,7 @@ TIME_SHORTCUTS = { '最近7天': 7, '最近30天': 30 } + +# ======================== 删除配置 ======================== +# 删除模式: 'remote' (仅删除MySQL生产库) +DELETE_MODE = 'remote' diff --git a/data_management/database/mysql_manager.py b/data_management/database/mysql_manager.py index 9a42f9f..45aa217 100644 --- a/data_management/database/mysql_manager.py +++ b/data_management/database/mysql_manager.py @@ -306,6 +306,39 @@ class MySQLManager: finally: session.close() + def delete_records(self, datasource: str, record_ids: List[int]) -> int: + """ + 删除指定记录 + + Args: + datasource: 数据源 + record_ids: 要删除的记录ID列表 + + Returns: + 成功删除的记录数 + """ + session: Session = self.Session() + try: + Model = self.get_model_by_datasource(datasource) + if Model is None: + return 0 + + # 批量删除 + deleted_count = session.query(Model)\ + .filter(Model.id.in_(record_ids))\ + .delete(synchronize_session=False) + + session.commit() + print(f"成功从MySQL删除 {deleted_count} 条记录 (数据源: {datasource})") + return deleted_count + + except Exception as e: + session.rollback() + print(f"删除MySQL记录失败: {e}") + return 0 + finally: + session.close() + def close(self): """关闭数据库连接""" if self.engine: diff --git a/data_management/database/sqlite_manager.py b/data_management/database/sqlite_manager.py index fb5db7f..dd5dc72 100644 --- a/data_management/database/sqlite_manager.py +++ b/data_management/database/sqlite_manager.py @@ -224,6 +224,79 @@ class SQLiteManager: print(f"获取下载统计失败: {e}") return {} + def get_local_path(self, datasource: str, image_url: str) -> Optional[str]: + """ + 获取图片的本地文件路径 + + Args: + datasource: 数据源 + image_url: 图片URL + + Returns: + 本地文件路径,如果未下载则返回None + """ + try: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute(''' + SELECT local_path FROM download_history + WHERE datasource = ? AND image_url = ? + ''', (datasource, image_url)) + + result = cursor.fetchone() + conn.close() + + return result[0] if result else None + + except Exception as e: + print(f"获取本地路径失败: {e}") + return None + + def delete_download_record(self, datasource: str, image_url: str, delete_file: bool = True) -> bool: + """ + 删除下载记录(可选删除物理文件) + + Args: + datasource: 数据源 + image_url: 图片URL + delete_file: 是否删除物理文件 + + Returns: + 是否成功 + """ + try: + # 如果需要删除文件,先获取文件路径 + local_path = None + if delete_file: + local_path = self.get_local_path(datasource, image_url) + + # 删除数据库记录 + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute(''' + DELETE FROM download_history + WHERE datasource = ? AND image_url = ? + ''', (datasource, image_url)) + + conn.commit() + conn.close() + + # 删除物理文件 + if delete_file and local_path and os.path.exists(local_path): + try: + os.remove(local_path) + print(f"已删除本地文件: {local_path}") + except Exception as e: + print(f"删除本地文件失败: {e}") + + return True + + except Exception as e: + print(f"删除下载记录失败: {e}") + return False + def clear_invalid_records(self) -> int: """清理文件不存在的下载记录""" try: diff --git a/data_management/ui/preview_panel.py b/data_management/ui/preview_panel.py index 2b0e932..f06debe 100644 --- a/data_management/ui/preview_panel.py +++ b/data_management/ui/preview_panel.py @@ -151,12 +151,16 @@ class PreviewPanel(ctk.CTkFrame): parent, sqlite_manager, on_download_callback: Callable, + mysql_manager=None, + on_delete_callback: Callable = None, **kwargs ): super().__init__(parent, **kwargs) self.sqlite_manager = sqlite_manager + self.mysql_manager = mysql_manager self.on_download_callback = on_download_callback + self.on_delete_callback = on_delete_callback # 数据 self.current_data = [] @@ -235,6 +239,17 @@ class PreviewPanel(ctk.CTkFrame): ) self.download_btn.pack(side="left", padx=5) + self.delete_btn = ctk.CTkButton( + left_buttons, + text="🗑️ 删除选中", + command=self._on_delete, + width=100, + height=30, + fg_color="red", + hover_color="darkred" + ) + self.delete_btn.pack(side="left", padx=5) + # 分页控件 page_frame = ctk.CTkFrame(bottom_frame) page_frame.pack(side="right", padx=10, pady=10) @@ -383,6 +398,94 @@ class PreviewPanel(ctk.CTkFrame): if self.on_download_callback: self.on_download_callback(selected_data) + def _on_delete(self): + """删除选中的图片""" + if not self.selected_ids: + messagebox.showwarning("提示", "请先选择要删除的图片") + return + + # 获取选中的图片数据 + selected_data = [ + data for data in self.current_data + if data['id'] in self.selected_ids + ] + + # 二次确认(简化版,不显示删除范围选项) + count = len(selected_data) + confirm = messagebox.askyesno( + "确认删除", + f"确定要删除选中的 {count} 张图片吗?\n\n" + "此操作将:\n" + "1. 从MySQL数据库中删除记录\n" + "2. 删除已下载的本地文件(如有)\n\n" + "删除后无法恢复!", + icon='warning' + ) + + if not confirm: + return + + # 在后台线程执行删除 + def delete_thread(): + try: + from config import DELETE_MODE + + success_count = 0 + failed_ids = [] + + for data in selected_data: + try: + # 模式B: 仅删除MySQL + if DELETE_MODE == 'remote': + # 删除MySQL记录 + if self.mysql_manager: + deleted = self.mysql_manager.delete_records( + data['datasource'], + [data['id']] + ) + if deleted > 0: + # 删除SQLite记录和本地文件 + self.sqlite_manager.delete_download_record( + data['datasource'], + data['image_url'], + delete_file=True + ) + success_count += 1 + else: + failed_ids.append(data['id']) + else: + failed_ids.append(data['id']) + except Exception as e: + print(f"删除记录失败 (ID: {data['id']}): {e}") + failed_ids.append(data['id']) + + # 更新UI(在主线程) + self.after(0, lambda: self._on_delete_complete(success_count, len(failed_ids))) + + except Exception as e: + print(f"删除操作失败: {e}") + self.after(0, lambda: messagebox.showerror("错误", f"删除失败: {e}")) + + threading.Thread(target=delete_thread, daemon=True).start() + + def _on_delete_complete(self, success_count: int, failed_count: int): + """删除完成回调""" + # 显示结果 + if failed_count == 0: + messagebox.showinfo("删除成功", f"成功删除 {success_count} 张图片") + else: + messagebox.showwarning( + "部分删除失败", + f"成功删除 {success_count} 张图片\n失败 {failed_count} 张" + ) + + # 清空选中 + self.selected_ids.clear() + + # 调用删除回调,刷新数据 + if self.on_delete_callback: + self.on_delete_callback() + def _on_preview(self, data: Dict): """查看大图""" # 创建预览窗口