Files
Neo-ZQYY/gui/main_window.py

523 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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 管理系统",
"<h3>飞球 ETL 管理系统</h3>"
"<p>版本: 1.0.0</p>"
"<p>一个用于管理台球场门店数据 ETL 的图形化工具。</p>"
"<p>功能包括:</p>"
"<ul>"
"<li>任务配置与执行</li>"
"<li>环境变量管理</li>"
"<li>数据库查询</li>"
"<li>ETL 状态监控</li>"
"</ul>"
)
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()