diff --git a/main_window.py b/main_window.py index 095d9cc..dcc9a5b 100644 --- a/main_window.py +++ b/main_window.py @@ -4,7 +4,8 @@ PySide6 GUI界面模块 from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QSlider, QDialog, QFormLayout, QLineEdit, QSpinBox, QMessageBox, QSystemTrayIcon, - QMenu, QTextEdit, QGroupBox, QDialogButtonBox, QCheckBox, QScrollArea, QFileDialog) + QMenu, QTextEdit, QGroupBox, QDialogButtonBox, QCheckBox, QScrollArea, QFileDialog, + QTabWidget) from PySide6.QtCore import Qt, QTimer, Signal, QPoint from PySide6.QtGui import QFont, QColor, QPainter, QBrush, QPen, QIcon, QAction, QPixmap from typing import Callable, Optional @@ -120,81 +121,171 @@ class ConfigDialog(QDialog): self._init_ui() def _init_ui(self): - layout = QFormLayout(self) + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + + # 创建标签页 + tab_widget = QTabWidget() + tab_widget.setStyleSheet( + "QTabWidget::pane { border: none; }" + "QTabBar::tab { padding: 12px 20px; font-size: 13px; }" + "QTabBar::tab:selected { background-color: #2196F3; color: white; }" + ) + + # API配置标签页 + api_tab = QWidget() + api_layout = QFormLayout(api_tab) + api_layout.setContentsMargins(20, 20, 20, 20) + api_layout.setSpacing(12) - # LLM API 配置 llm_config = self.config_manager.llm_api_config self.base_url_edit = QLineEdit(llm_config.get('base_url', '')) + self.base_url_edit.setPlaceholderText("https://api.openai.com/v1") self.api_key_edit = QLineEdit(llm_config.get('api_key', '')) self.api_key_edit.setEchoMode(QLineEdit.Password) self.model_edit = QLineEdit(llm_config.get('model', '')) + self.model_edit.setPlaceholderText("gpt-3.5-turbo") self.timeout_spin = QSpinBox() self.timeout_spin.setRange(10, 300) self.timeout_spin.setValue(llm_config.get('timeout', 30)) + self.timeout_spin.setSuffix(" 秒") - layout.addRow("API Base URL:", self.base_url_edit) - layout.addRow("API Key:", self.api_key_edit) - layout.addRow("Model:", self.model_edit) - layout.addRow("Timeout (s):", self.timeout_spin) + api_layout.addRow("API Base URL:", self.base_url_edit) + api_layout.addRow("API Key:", self.api_key_edit) + api_layout.addRow("Model:", self.model_edit) + api_layout.addRow("超时时间:", self.timeout_spin) + + # 爬虫配置标签页 + spider_tab = QWidget() + spider_layout = QFormLayout(spider_tab) + spider_layout.setContentsMargins(20, 20, 20, 20) + spider_layout.setSpacing(12) - # 爬虫配置 spider_config = self.config_manager.spider_config self.url_edit = QLineEdit(spider_config.get('target_url', '')) + self.url_edit.setPlaceholderText("https://example.com") self.xpath_edit = QLineEdit(spider_config.get('xpath', '')) + self.xpath_edit.setPlaceholderText("//div[@class='content']") self.user_agent_edit = QLineEdit(spider_config.get('user_agent', '')) + self.user_agent_edit.setPlaceholderText("Mozilla/5.0...") self.interval_spin = QSpinBox() self.interval_spin.setRange(10, 3600) self.interval_spin.setValue(spider_config.get('fetch_interval', 15)) + self.interval_spin.setSuffix(" 秒") - layout.addRow("目标URL:", self.url_edit) - layout.addRow("XPath:", self.xpath_edit) - layout.addRow("User Agent:", self.user_agent_edit) - layout.addRow("刷新间隔(s):", self.interval_spin) + spider_layout.addRow("目标 URL:", self.url_edit) + spider_layout.addRow("XPath 表达式:", self.xpath_edit) + spider_layout.addRow("User Agent:", self.user_agent_edit) + spider_layout.addRow("刷新间隔:", self.interval_spin) - # Chrome浏览器路径配置 + # Chrome浏览器路径 chrome_path_layout = QHBoxLayout() + chrome_path_layout.setSpacing(8) self.chrome_path_edit = QLineEdit(spider_config.get('chrome_path', '')) self.chrome_path_edit.setPlaceholderText("留空则自动查找Chrome浏览器") self.chrome_browse_btn = QPushButton("浏览...") + self.chrome_browse_btn.setFixedWidth(60) self.chrome_browse_btn.clicked.connect(self._browse_chrome_path) chrome_path_layout.addWidget(self.chrome_path_edit) chrome_path_layout.addWidget(self.chrome_browse_btn) - layout.addRow("Chrome路径:", chrome_path_layout) + spider_layout.addRow("Chrome 路径:", chrome_path_layout) + + # 界面配置标签页 + ui_tab = QWidget() + ui_layout = QFormLayout(ui_tab) + ui_layout.setContentsMargins(20, 20, 20, 20) + ui_layout.setSpacing(12) - # UI 配置 ui_config = self.config_manager.ui_config + # 透明度 + opacity_layout = QHBoxLayout() + opacity_layout.setSpacing(8) self.opacity_slider = QSlider(Qt.Horizontal) self.opacity_slider.setRange(30, 100) self.opacity_slider.setValue(int(ui_config.get('opacity', 0.9) * 100)) - self.ontop_check = QCheckBox() if hasattr(self, 'QCheckBox') else None - # 使用 QPushButton 替代 QCheckBox - self.ontop_btn = QPushButton("置顶") - self.ontop_btn.setCheckable(True) - self.ontop_btn.setChecked(ui_config.get('is_on_top', True)) + self.opacity_slider.setFixedWidth(120) + self.opacity_label = QLabel(f"{int(ui_config.get('opacity', 0.9) * 100)}%") + self.opacity_label.setFixedWidth(40) + self.opacity_slider.valueChanged.connect( + lambda v: self.opacity_label.setText(f"{v}%") + ) + opacity_layout.addWidget(self.opacity_slider) + opacity_layout.addWidget(self.opacity_label) + opacity_layout.addStretch() - layout.addRow("透明度:", self.opacity_slider) - layout.addRow("窗口置顶:", self.ontop_btn) + # 窗口置顶 + self.ontop_btn = QPushButton("窗口置顶") + self.ontop_btn.setCheckable(True) + self.ontop_btn.setFixedWidth(100) + self.ontop_btn.setChecked(ui_config.get('is_on_top', True)) + self.ontop_btn.setStyleSheet( + "QPushButton { background-color: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; padding: 6px; }" + "QPushButton:checked { background-color: #2196F3; color: white; }" + ) + + ui_layout.addRow("透明度:", opacity_layout) + ui_layout.addRow("窗口行为:", self.ontop_btn) # 阈值配置 thresholds = ui_config.get('thresholds', {}) + + threshold_layout = QHBoxLayout() + threshold_layout.setSpacing(12) + self.cold_spin = QSpinBox() self.cold_spin.setRange(0, 50) self.cold_spin.setValue(thresholds.get('cold', 30)) + self.cold_spin.setSuffix(" 分") + self.cold_spin.setFixedWidth(80) + self.warm_spin = QSpinBox() self.warm_spin.setRange(50, 100) self.warm_spin.setValue(thresholds.get('warm', 70)) + self.warm_spin.setSuffix(" 分") + self.warm_spin.setFixedWidth(80) + + threshold_layout.addWidget(QLabel("寒冷:")) + threshold_layout.addWidget(self.cold_spin) + threshold_layout.addWidget(QLabel("温暖:")) + threshold_layout.addWidget(self.warm_spin) + threshold_layout.addStretch() + + ui_layout.addRow("阈值设置:", threshold_layout) - layout.addRow("寒冷阈值:", self.cold_spin) - layout.addRow("温暖阈值:", self.warm_spin) + # 添加标签页 + tab_widget.addTab(api_tab, "API 配置") + tab_widget.addTab(spider_tab, "爬虫配置") + tab_widget.addTab(ui_tab, "界面设置") - # 按钮 + layout.addWidget(tab_widget) + + # 底部按钮 + button_layout = QHBoxLayout() + button_layout.setContentsMargins(20, 0, 20, 20) + button_layout.setSpacing(8) + button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) button_box.accepted.connect(self._save_config) button_box.rejected.connect(self.reject) - layout.addRow(button_box) + + # 设置按钮样式 + ok_btn = button_box.button(QDialogButtonBox.Ok) + cancel_btn = button_box.button(QDialogButtonBox.Cancel) + ok_btn.setStyleSheet( + "QPushButton { background-color: #2196F3; color: white; border: none; border-radius: 4px; padding: 8px 16px; }" + "QPushButton:hover { background-color: #1976D2; }" + ) + cancel_btn.setStyleSheet( + "QPushButton { background-color: #f5f5f5; color: #333; border: 1px solid #ddd; border-radius: 4px; padding: 8px 16px; }" + "QPushButton:hover { background-color: #e0e0e0; }" + ) + + button_layout.addStretch() + button_layout.addWidget(button_box) + layout.addLayout(button_layout) def _browse_chrome_path(self): """浏览Chrome路径""" @@ -261,65 +352,75 @@ class MainWindow(QWidget): def _init_ui(self): """初始化UI""" layout = QVBoxLayout(self) - layout.setContentsMargins(10, 10, 10, 10) + layout.setContentsMargins(16, 16, 16, 16) + layout.setSpacing(12) # 标题 - self.title_label = QLabel("上证指数sh000001") + self.title_label = QLabel("上证指数 sh000001") self.title_label.setAlignment(Qt.AlignCenter) title_font = QFont() - title_font.setPointSize(14) + title_font.setPointSize(16) title_font.setBold(True) self.title_label.setFont(title_font) + self.title_label.setStyleSheet("color: #333;") # 指示灯 self.indicator = SentimentIndicator() - self.score_label = QLabel("50 - 中性") + self.indicator.setMinimumSize(120, 120) + + # 分数和标签 + self.score_label = QLabel("50") self.score_label.setAlignment(Qt.AlignCenter) + score_font = QFont() + score_font.setPointSize(24) + score_font.setBold(True) + self.score_label.setFont(score_font) + self.score_label.setStyleSheet("color: #2196F3;") + + self.sentiment_label = QLabel("中性") + self.sentiment_label.setAlignment(Qt.AlignCenter) + sentiment_font = QFont() + sentiment_font.setPointSize(12) + self.sentiment_label.setFont(sentiment_font) + self.sentiment_label.setStyleSheet("color: #666;") # 状态信息 self.status_label = QLabel("等待数据...") self.status_label.setAlignment(Qt.AlignCenter) - status_font = QFont() - status_font.setPointSize(10) - self.status_label.setFont(status_font) + self.status_label.setStyleSheet("color: #999; font-size: 11px;") # 上证所截图显示 - screenshot_group = QGroupBox("上证所行情") - screenshot_layout = QVBoxLayout(screenshot_group) - self.screenshot_label = QLabel("等待截图...") self.screenshot_label.setAlignment(Qt.AlignCenter) - self.screenshot_label.setMinimumSize(400, 200) - self.screenshot_label.setStyleSheet("border: 1px solid #ccc; background-color: #f0f0f0;") - - screenshot_scroll = QScrollArea() - screenshot_scroll.setWidget(self.screenshot_label) - screenshot_scroll.setWidgetResizable(True) - screenshot_scroll.setMinimumHeight(150) - - screenshot_layout.addWidget(screenshot_scroll) + self.screenshot_label.setMinimumSize(380, 180) + self.screenshot_label.setMaximumHeight(200) + self.screenshot_label.setStyleSheet( + "QLabel {" + " border: 1px solid #e0e0e0;" + " border-radius: 8px;" + " background-color: #f5f5f5;" + "}" + ) + - # 按钮 - btn_layout = QHBoxLayout() - self.refresh_btn = QPushButton("刷新") - self.config_btn = QPushButton("配置") - self.quit_btn = QPushButton("退出") - self.quit_btn.clicked.connect(self.quit_app) - btn_layout.addWidget(self.refresh_btn) - btn_layout.addWidget(self.config_btn) - btn_layout.addWidget(self.quit_btn) # 添加到主布局 layout.addWidget(self.title_label) - layout.addWidget(self.indicator) + layout.addWidget(self.indicator, alignment=Qt.AlignCenter) layout.addWidget(self.score_label) + layout.addWidget(self.sentiment_label) + layout.addWidget(self.screenshot_label) layout.addWidget(self.status_label) - layout.addWidget(screenshot_group) - layout.addLayout(btn_layout) # 设置窗口标志(无边框、可拖拽) self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) self.setAttribute(Qt.WA_TranslucentBackground) + self.setStyleSheet( + "QWidget {" + " background-color: rgba(255, 255, 255, 0.95);" + " border-radius: 12px;" + "}" + ) def _set_window_title(self): """设置窗口标题""" @@ -412,14 +513,17 @@ class MainWindow(QWidget): def contextMenuEvent(self, event): """右键菜单""" context_menu = QMenu(self) + refresh_action = QAction("刷新", self) config_action = QAction("配置", self) - opacity_action = QAction("透明度", self) quit_action = QAction("退出", self) + refresh_action.triggered.connect(self._on_refresh) config_action.triggered.connect(self.show_config) quit_action.triggered.connect(self.quit_app) + context_menu.addAction(refresh_action) context_menu.addAction(config_action) + context_menu.addSeparator() context_menu.addAction(quit_action) context_menu.exec(event.globalPos()) @@ -429,28 +533,55 @@ class MainWindow(QWidget): if dialog.exec() == QDialog.Accepted: self._apply_config() + def _on_refresh(self): + """刷新回调""" + if hasattr(self, '_refresh_callback') and self._refresh_callback: + self._refresh_callback() + logger.info("执行刷新操作") + def update_indicator(self, score: int, label: str = None): """更新指示灯""" if label is None: label = self.indicator.get_description(score) self.indicator.set_value(score, label) - self.score_label.setText(f"{score} - {label}") + self.score_label.setText(str(score)) + self.sentiment_label.setText(label) + + color = self._get_score_color(score) + self.score_label.setStyleSheet(f"color: {color}; font-size: 24px; font-weight: bold;") + logger.debug(f"更新指示灯: {score}分 - {label}") + def _get_score_color(self, score: int) -> str: + """根据分数获取颜色值""" + if score < 30: + return "#1565C0" + elif score < 39: + return "#1976D2" + elif score < 45: + return "#42A5F5" + elif score < 55: + return "#66BB6A" + elif score < 65: + return "#FFA726" + elif score < 70: + return "#FB8C00" + else: + return "#E53935" + def update_status(self, text: str): """更新状态""" self.status_label.setText(text) logger.debug(f"更新状态: {text}") def set_refresh_callback(self, callback: Callable): - """设置刷新按钮回调""" - self.refresh_btn.clicked.connect(callback) - logger.debug("设置刷新按钮回调") + """设置刷新回调""" + self._refresh_callback = callback + logger.debug("设置刷新回调") def set_config_callback(self, callback: Callable): - """设置配置按钮回调""" - self.config_btn.clicked.connect(callback) - logger.debug("设置配置按钮回调") + """设置配置回调(已废弃,配置直接通过右键菜单调用)""" + logger.debug("配置回调已废弃,配置直接通过右键菜单调用") def show_message(self, title: str, message: str): """显示消息""" diff --git a/sse_screenshot.png b/sse_screenshot.png index ddd8a60..8801745 100644 Binary files a/sse_screenshot.png and b/sse_screenshot.png differ