# -*- coding: utf-8 -*- """主窗口""" from PySide6.QtWidgets import ( QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QStackedWidget, QListWidget, QListWidgetItem, QStatusBar, QLabel, QMessageBox, QSplitter ) from PySide6.QtCore import Qt, QSize, Signal from PySide6.QtGui import QIcon, QAction from .widgets.task_panel import TaskPanel from .widgets.task_manager import TaskManager from .widgets.env_editor import EnvEditor from .widgets.log_viewer import LogViewer from .widgets.db_viewer import DBViewer from .widgets.status_panel import StatusPanel from .resources import load_stylesheet class MainWindow(QMainWindow): """ETL GUI 主窗口""" # 信号 status_message = Signal(str, int) # message, timeout_ms def __init__(self): super().__init__() self.setWindowTitle("飞球 ETL 管理系统") self.setMinimumSize(1200, 800) self.resize(1400, 900) # 应用样式 self.setStyleSheet(load_stylesheet()) # 保存分割器引用 self.splitter = None # 首次显示标记(必须在 _restore_state 之前初始化,因为 showMaximized 会触发 showEvent) self._first_show = True # 初始化 UI self._init_ui() self._init_menu() self._init_status_bar() self._connect_signals() # 恢复保存的状态 self._restore_state() def showEvent(self, event): """窗口显示事件""" super().showEvent(event) if self._first_show: self._first_show = False # 延迟检查配置,让窗口先显示 from PySide6.QtCore import QTimer QTimer.singleShot(100, self._check_config_on_startup) def _init_ui(self): """初始化界面""" # 中央部件 central_widget = QWidget() self.setCentralWidget(central_widget) # 主布局 main_layout = QHBoxLayout(central_widget) main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setSpacing(0) # 创建分割器 self.splitter = QSplitter(Qt.Horizontal) main_layout.addWidget(self.splitter) # 左侧导航栏 nav_widget = self._create_nav_widget() self.splitter.addWidget(nav_widget) # 右侧内容区 self.content_stack = QStackedWidget() self.splitter.addWidget(self.content_stack) # 设置分割比例 self.splitter.setSizes([200, 1200]) self.splitter.setStretchFactor(0, 0) self.splitter.setStretchFactor(1, 1) # 创建各个面板 self._create_panels() def _create_nav_widget(self) -> QWidget: """创建导航侧边栏""" nav_widget = QWidget() nav_widget.setMaximumWidth(220) nav_widget.setMinimumWidth(180) layout = QVBoxLayout(nav_widget) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) # 标题 title_label = QLabel(" ETL 控制台") title_label.setProperty("heading", True) title_label.setFixedHeight(60) title_label.setAlignment(Qt.AlignVCenter) layout.addWidget(title_label) # 导航列表 self.nav_list = QListWidget() self.nav_list.setObjectName("navList") layout.addWidget(self.nav_list) # 添加导航项 nav_items = [ ("任务配置", "配置并执行 ETL 任务"), ("任务管理", "管理任务队列和历史记录"), ("环境配置", "编辑 .env 配置文件"), ("数据库", "查看数据库和执行查询"), ("ETL 状态", "查看 ETL 运行状态"), ("日志", "查看执行日志"), ] for name, tooltip in nav_items: item = QListWidgetItem(name) item.setToolTip(tooltip) item.setSizeHint(QSize(0, 44)) self.nav_list.addItem(item) return nav_widget def _create_panels(self): """创建各个功能面板""" # 任务配置面板 self.task_panel = TaskPanel() self.content_stack.addWidget(self.task_panel) # 任务管理面板 self.task_manager = TaskManager() self.content_stack.addWidget(self.task_manager) # 环境配置面板 self.env_editor = EnvEditor() self.content_stack.addWidget(self.env_editor) # 数据库查看器 self.db_viewer = DBViewer() self.content_stack.addWidget(self.db_viewer) # ETL 状态面板 self.status_panel = StatusPanel() self.content_stack.addWidget(self.status_panel) # 日志面板 self.log_viewer = LogViewer() self.content_stack.addWidget(self.log_viewer) def _init_menu(self): """初始化菜单栏""" menubar = self.menuBar() # 文件菜单 file_menu = menubar.addMenu("文件(&F)") refresh_action = QAction("刷新配置(&R)", self) refresh_action.setShortcut("Ctrl+R") refresh_action.triggered.connect(self._refresh_config) file_menu.addAction(refresh_action) settings_action = QAction("设置(&S)...", self) settings_action.setShortcut("Ctrl+,") settings_action.triggered.connect(self._show_settings) file_menu.addAction(settings_action) file_menu.addSeparator() exit_action = QAction("退出(&X)", self) exit_action.setShortcut("Ctrl+Q") exit_action.triggered.connect(self.close) file_menu.addAction(exit_action) # 视图菜单 view_menu = menubar.addMenu("视图(&V)") task_config_action = QAction("任务配置(&T)", self) task_config_action.setShortcut("Ctrl+1") task_config_action.triggered.connect(lambda: self._switch_panel(0)) view_menu.addAction(task_config_action) task_manager_action = QAction("任务管理(&M)", self) task_manager_action.setShortcut("Ctrl+2") task_manager_action.triggered.connect(lambda: self._switch_panel(1)) view_menu.addAction(task_manager_action) env_action = QAction("环境配置(&E)", self) env_action.setShortcut("Ctrl+3") env_action.triggered.connect(lambda: self._switch_panel(2)) view_menu.addAction(env_action) db_action = QAction("数据库(&D)", self) db_action.setShortcut("Ctrl+4") db_action.triggered.connect(lambda: self._switch_panel(3)) view_menu.addAction(db_action) status_action = QAction("ETL 状态(&S)", self) status_action.setShortcut("Ctrl+5") status_action.triggered.connect(lambda: self._switch_panel(4)) view_menu.addAction(status_action) log_action = QAction("日志(&L)", self) log_action.setShortcut("Ctrl+6") log_action.triggered.connect(lambda: self._switch_panel(5)) view_menu.addAction(log_action) # 帮助菜单 help_menu = menubar.addMenu("帮助(&H)") about_action = QAction("关于(&A)", self) about_action.triggered.connect(self._show_about) help_menu.addAction(about_action) def _init_status_bar(self): """初始化状态栏""" self.status_bar = QStatusBar() self.setStatusBar(self.status_bar) # 连接状态 self.conn_status_label = QLabel("数据库: 未连接") self.conn_status_label.setProperty("status", "warning") self.status_bar.addPermanentWidget(self.conn_status_label) # 任务状态 self.task_status_label = QLabel("任务: 空闲") self.status_bar.addPermanentWidget(self.task_status_label) # 默认消息 self.status_bar.showMessage("就绪", 3000) def _connect_signals(self): """连接信号""" # 导航切换 self.nav_list.currentRowChanged.connect(self._on_nav_changed) # 任务面板信号 self.task_panel.task_started.connect(self._on_task_started) self.task_panel.task_finished.connect(self._on_task_finished) self.task_panel.log_message.connect(self.log_viewer.append_log) self.task_panel.add_to_queue.connect(self._on_add_to_queue) self.task_panel.create_schedule.connect(self._on_create_schedule) # 任务管理器信号 self.task_manager.task_started.connect(self._on_task_started) self.task_manager.task_finished.connect(self._on_task_finished) self.task_manager.log_message.connect(self.log_viewer.append_log) # 数据库连接状态 self.db_viewer.connection_changed.connect(self._on_db_connection_changed) # 状态消息 self.status_message.connect(self._show_status_message) def _on_nav_changed(self, index: int): """导航项切换""" self.content_stack.setCurrentIndex(index) def _switch_panel(self, index: int): """切换到指定面板""" self.nav_list.setCurrentRow(index) def _refresh_config(self): """刷新配置""" self.env_editor.load_config() self.task_panel.refresh_tasks() self.status_bar.showMessage("配置已刷新", 3000) def _on_task_started(self, task_info: str): """任务开始时""" self.task_status_label.setText(f"任务: 执行中 - {task_info}") self.task_status_label.setProperty("status", "info") self.task_status_label.style().unpolish(self.task_status_label) self.task_status_label.style().polish(self.task_status_label) def _on_task_finished(self, success: bool, message: str): """任务完成时""" if success: self.task_status_label.setText("任务: 完成") self.task_status_label.setProperty("status", "success") else: self.task_status_label.setText("任务: 失败") self.task_status_label.setProperty("status", "error") self.task_status_label.style().unpolish(self.task_status_label) self.task_status_label.style().polish(self.task_status_label) self.status_bar.showMessage(message, 5000) def _on_db_connection_changed(self, connected: bool, message: str): """数据库连接状态变化""" if connected: self.conn_status_label.setText("数据库: 已连接") self.conn_status_label.setProperty("status", "success") else: self.conn_status_label.setText("数据库: 未连接") self.conn_status_label.setProperty("status", "warning") self.conn_status_label.style().unpolish(self.conn_status_label) self.conn_status_label.style().polish(self.conn_status_label) if message: self.status_bar.showMessage(message, 3000) def _show_status_message(self, message: str, timeout: int): """显示状态栏消息""" self.status_bar.showMessage(message, timeout) def _on_add_to_queue(self, config): """添加任务到队列并自动执行""" task_id = self.task_manager.add_task(config) self.status_bar.showMessage(f"任务已添加到队列 (ID: {task_id})", 3000) # 自动切换到任务管理面板 self._switch_panel(1) # 如果当前没有任务在运行,自动开始执行 if not self.task_manager._is_running(): from PySide6.QtCore import QTimer # 稍微延迟以确保 UI 更新 QTimer.singleShot(100, self.task_manager._run_next) def _on_create_schedule(self, name: str, task_codes: list, task_config: dict): """创建调度任务""" # 打开调度编辑对话框 from .widgets.task_manager import ScheduleEditDialog from .models.schedule_model import ScheduledTask, ScheduleConfig import uuid # 创建一个预填充的调度任务 task = ScheduledTask( id=str(uuid.uuid4())[:8], name=name, task_codes=task_codes, schedule=ScheduleConfig(), task_config=task_config, ) # 打开编辑对话框 dialog = ScheduleEditDialog(task=task, parent=self) if dialog.exec(): updated_task = dialog.get_task() if updated_task: self.task_manager.schedule_store.add_task(updated_task) self.task_manager._refresh_schedule_table() self.status_bar.showMessage(f"调度任务已创建: {updated_task.name}", 3000) # 切换到任务管理面板的调度选项卡 self._switch_panel(1) def _show_settings(self): """显示设置对话框""" from .widgets.settings_dialog import SettingsDialog dialog = SettingsDialog(self) if dialog.exec(): # 重新加载配置 self._refresh_config() self.status_bar.showMessage("设置已保存", 3000) def _check_config_on_startup(self): """启动时检查配置""" from .utils.app_settings import app_settings if not app_settings.is_configured(): QMessageBox.information( self, "首次配置", "欢迎使用 ETL 管理系统!\n\n" "请先配置 ETL 项目路径,否则无法执行任务。\n\n" "点击 文件 → 设置 进行配置。" ) def _restore_state(self): """从设置恢复窗口状态""" from .utils.app_settings import app_settings # 恢复窗口位置和大小 geometry = app_settings.window_geometry if geometry and len(geometry) == 4: x, y, width, height = geometry # 确保窗口在屏幕范围内 from PySide6.QtWidgets import QApplication screen = QApplication.primaryScreen() if screen: screen_rect = screen.availableGeometry() # 检查位置是否在屏幕范围内 if (x >= screen_rect.x() and y >= screen_rect.y() and x + width <= screen_rect.x() + screen_rect.width() and y + height <= screen_rect.y() + screen_rect.height()): self.setGeometry(x, y, width, height) # 恢复最大化状态 if app_settings.window_maximized: self.showMaximized() # 恢复当前面板 saved_panel = app_settings.current_panel if 0 <= saved_panel < self.nav_list.count(): self.nav_list.setCurrentRow(saved_panel) else: self.nav_list.setCurrentRow(0) # 恢复分割器大小 splitter_sizes = app_settings.splitter_sizes if splitter_sizes and self.splitter: self.splitter.setSizes(splitter_sizes) # 恢复任务管理器状态 if hasattr(self, 'task_manager'): # 恢复选项卡 saved_tab = app_settings.task_manager_tab if hasattr(self.task_manager, 'tab_widget'): if 0 <= saved_tab < self.task_manager.tab_widget.count(): self.task_manager.tab_widget.setCurrentIndex(saved_tab) # 恢复自动执行状态 if app_settings.auto_run_enabled: if hasattr(self.task_manager, 'auto_run_btn'): self.task_manager.auto_run_btn.setChecked(True) # 恢复调度器状态 if app_settings.scheduler_enabled: if hasattr(self.task_manager, 'scheduler_btn'): self.task_manager.scheduler_btn.setChecked(True) # 恢复任务面板状态 if hasattr(self, 'task_panel'): if app_settings.advanced_expanded: if hasattr(self.task_panel, 'advanced_section'): self.task_panel.advanced_section.setExpanded(True) def _save_state(self): """保存窗口状态到设置""" from .utils.app_settings import app_settings # 保存窗口位置和大小(仅在非最大化时保存) if not self.isMaximized(): geo = self.geometry() app_settings.window_geometry = [geo.x(), geo.y(), geo.width(), geo.height()] # 保存最大化状态 app_settings.window_maximized = self.isMaximized() # 保存当前面板 app_settings.current_panel = self.nav_list.currentRow() # 保存分割器大小 if self.splitter: app_settings.splitter_sizes = self.splitter.sizes() # 保存任务管理器状态 if hasattr(self, 'task_manager'): # 保存选项卡 if hasattr(self.task_manager, 'tab_widget'): app_settings.task_manager_tab = self.task_manager.tab_widget.currentIndex() # 保存自动执行状态 if hasattr(self.task_manager, 'auto_run_btn'): app_settings.auto_run_enabled = self.task_manager.auto_run_btn.isChecked() # 保存调度器状态 if hasattr(self.task_manager, 'scheduler_btn'): app_settings.scheduler_enabled = self.task_manager.scheduler_btn.isChecked() # 保存任务面板状态 if hasattr(self, 'task_panel'): if hasattr(self.task_panel, 'advanced_section'): app_settings.advanced_expanded = self.task_panel.advanced_section.isExpanded() def _show_about(self): """显示关于对话框""" QMessageBox.about( self, "关于 飞球 ETL 管理系统", "

飞球 ETL 管理系统

" "

版本: 1.0.0

" "

一个用于管理台球场门店数据 ETL 的图形化工具。

" "

功能包括:

" "" ) def closeEvent(self, event): """关闭事件""" # 检查是否有正在运行的任务 if hasattr(self, 'task_panel') and self.task_panel.is_running(): reply = QMessageBox.question( self, "确认退出", "当前有任务正在执行,确定要退出吗?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.No: event.ignore() return # 检查任务管理器是否有正在运行的任务 if hasattr(self, 'task_manager') and self.task_manager._is_running(): reply = QMessageBox.question( self, "确认退出", "任务管理器中有任务正在执行,确定要退出吗?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.No: event.ignore() return # 保存窗口状态 self._save_state() # 关闭数据库连接 if hasattr(self, 'db_viewer'): self.db_viewer.close_connection() event.accept()