""" PySide6 GUI界面模块 """ from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QSlider, QDialog, QFormLayout, QLineEdit, QSpinBox, QMessageBox, QSystemTrayIcon, QMenu, QTextEdit, QGroupBox, QDialogButtonBox) from PySide6.QtCore import Qt, QTimer, Signal, QPoint from PySide6.QtGui import QFont, QColor, QPainter, QBrush, QPen, QIcon, QAction from typing import Callable, Optional class SentimentIndicator(QWidget): """情感指示灯组件""" def __init__(self, parent=None): super().__init__(parent) self.score = 50 self.label_text = "中性" self.setMinimumSize(100, 100) def set_value(self, score: int, label: str = None): """设置数值和标签""" self.score = max(0, min(100, score)) if label: self.label_text = label self.update() def paintEvent(self, event): """绘制指示灯""" painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing) center = self.rect().center() radius = min(self.width(), self.height()) // 2 - 10 # 根据分数确定颜色 color = self._get_color(self.score) # 绘制外圈 painter.setPen(QPen(QColor(100, 100, 100), 2)) painter.setBrush(QBrush(QColor(30, 30, 30))) painter.drawEllipse(center, radius, radius) # 绘制内圈(渐变效果) gradient_color = QColor(color) painter.setPen(Qt.NoPen) painter.setBrush(QBrush(gradient_color)) painter.drawEllipse(center, radius - 4, radius - 4) # 绘制发光效果 for i in range(3, 0, -1): glow_color = QColor(color) glow_color.setAlpha(50 // i) painter.setBrush(QBrush(glow_color)) painter.drawEllipse(center, radius - i * 5, radius - i * 5) def _get_color(self, score: int) -> QColor: """根据分数获取颜色""" if score < 30: # 冷色系 - 蓝色/青色 ratio = score / 30 return QColor(int(0 + 100 * ratio), int(150 + 50 * ratio), 255) elif score < 70: # 中性 - 灰色/绿色 if score < 50: ratio = (score - 30) / 20 return QColor(int(100 + 50 * ratio), int(200 + 20 * ratio), int(200 - 50 * ratio)) else: ratio = (score - 50) / 20 return QColor(int(150 + 50 * ratio), int(220 - 20 * ratio), int(150 - 50 * ratio)) else: # 暖色系 - 橙色/红色 ratio = (score - 70) / 30 return QColor(255, int(200 - 100 * ratio), int(50 + 50 * ratio)) def get_description(self, score: int) -> str: """获取描述文本""" if score < 20: return "极度看跌" elif score < 40: return "偏悲观" elif score < 60: return "中性" elif score < 80: return "偏乐观" else: return "极度看涨" class ConfigDialog(QDialog): """配置对话框""" def __init__(self, config_manager, parent=None): super().__init__(parent) self.config_manager = config_manager self.setWindowTitle("配置") self.setMinimumWidth(400) self._init_ui() def _init_ui(self): layout = QFormLayout(self) # LLM API 配置 llm_config = self.config_manager.llm_api_config self.base_url_edit = QLineEdit(llm_config.get('base_url', '')) 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.timeout_spin = QSpinBox() self.timeout_spin.setRange(10, 300) self.timeout_spin.setValue(llm_config.get('timeout', 30)) 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) # 爬虫配置 spider_config = self.config_manager.spider_config self.url_edit = QLineEdit(spider_config.get('target_url', '')) self.xpath_edit = QLineEdit(spider_config.get('xpath', '')) self.user_agent_edit = QLineEdit(spider_config.get('user_agent', '')) self.interval_spin = QSpinBox() self.interval_spin.setRange(10, 3600) self.interval_spin.setValue(spider_config.get('fetch_interval', 60)) 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) # UI 配置 ui_config = self.config_manager.ui_config 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)) layout.addRow("透明度:", self.opacity_slider) layout.addRow("窗口置顶:", self.ontop_btn) # 阈值配置 thresholds = ui_config.get('thresholds', {}) self.cold_spin = QSpinBox() self.cold_spin.setRange(0, 50) self.cold_spin.setValue(thresholds.get('cold', 30)) self.warm_spin = QSpinBox() self.warm_spin.setRange(50, 100) self.warm_spin.setValue(thresholds.get('warm', 70)) layout.addRow("寒冷阈值:", self.cold_spin) layout.addRow("温暖阈值:", self.warm_spin) # 按钮 button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) button_box.accepted.connect(self._save_config) button_box.rejected.connect(self.reject) layout.addRow(button_box) def _save_config(self): """保存配置""" # LLM API self.config_manager.update_llm_api( base_url=self.base_url_edit.text(), api_key=self.api_key_edit.text(), model=self.model_edit.text(), timeout=self.timeout_spin.value() ) # 爬虫 self.config_manager.update_spider( target_url=self.url_edit.text(), xpath=self.xpath_edit.text(), user_agent=self.user_agent_edit.text(), fetch_interval=self.interval_spin.value() ) # UI self.config_manager.update_ui( opacity=self.opacity_slider.value() / 100.0, is_on_top=self.ontop_btn.isChecked(), cold_threshold=self.cold_spin.value(), warm_threshold=self.warm_spin.value() ) self.accept() class MainWindow(QWidget): """主窗口""" def __init__(self, config_manager, parent=None): super().__init__(parent) self.config_manager = config_manager self.setWindowTitle("股吧人气指示器") self._init_ui() self._apply_config() # 拖拽相关 self.dragging = False self.drag_position = QPoint() # 系统托盘 self._init_tray_icon() def _init_ui(self): """初始化UI""" layout = QVBoxLayout(self) layout.setContentsMargins(10, 10, 10, 10) # 标题 self.title_label = QLabel("股吧人气") self.title_label.setAlignment(Qt.AlignCenter) title_font = QFont() title_font.setPointSize(14) title_font.setBold(True) self.title_label.setFont(title_font) # 指示灯 self.indicator = SentimentIndicator() self.score_label = QLabel("50 - 中性") self.score_label.setAlignment(Qt.AlignCenter) # 状态信息 self.status_label = QLabel("等待数据...") self.status_label.setAlignment(Qt.AlignCenter) status_font = QFont() status_font.setPointSize(10) self.status_label.setFont(status_font) # 按钮 btn_layout = QHBoxLayout() self.refresh_btn = QPushButton("刷新") self.config_btn = QPushButton("配置") btn_layout.addWidget(self.refresh_btn) btn_layout.addWidget(self.config_btn) # 添加到主布局 layout.addWidget(self.title_label) layout.addWidget(self.indicator) layout.addWidget(self.score_label) layout.addWidget(self.status_label) layout.addLayout(btn_layout) # 设置窗口标志(无边框、可拖拽) self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) self.setAttribute(Qt.WA_TranslucentBackground) def _init_tray_icon(self): """初始化系统托盘""" self.tray_icon = QSystemTrayIcon(self) self.tray_icon.setToolTip("股吧人气指示器") # 创建托盘菜单 tray_menu = QMenu() show_action = QAction("显示", self) hide_action = QAction("隐藏", self) quit_action = QAction("退出", self) show_action.triggered.connect(self.show) hide_action.triggered.connect(self.hide) quit_action.triggered.connect(self.quit_app) tray_menu.addAction(show_action) tray_menu.addAction(hide_action) tray_menu.addAction(quit_action) self.tray_icon.setContextMenu(tray_menu) self.tray_icon.show() def quit_app(self): """退出应用""" self.close() import sys sys.exit(0) def _apply_config(self): """应用配置""" ui_config = self.config_manager.ui_config self.setWindowOpacity(ui_config.get('opacity', 0.9)) if ui_config.get('is_on_top', True): self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint) else: self.setWindowFlags(self.windowFlags() & ~Qt.WindowStaysOnTopHint) thresholds = ui_config.get('thresholds', {}) def mousePressEvent(self, event): """鼠标按下事件""" if event.button() == Qt.LeftButton: self.dragging = True self.drag_position = event.globalPosition().toPoint() - self.frameGeometry().topLeft() def mouseMoveEvent(self, event): """鼠标移动事件""" if self.dragging: self.move(event.globalPosition().toPoint() - self.drag_position) def mouseReleaseEvent(self, event): """鼠标释放事件""" if event.button() == Qt.LeftButton: self.dragging = False def contextMenuEvent(self, event): """右键菜单""" context_menu = QMenu(self) config_action = QAction("配置", self) opacity_action = QAction("透明度", self) quit_action = QAction("退出", self) config_action.triggered.connect(self.show_config) quit_action.triggered.connect(self.quit_app) context_menu.addAction(config_action) context_menu.addAction(quit_action) context_menu.exec(event.globalPosition().toPoint()) def show_config(self): """显示配置对话框""" dialog = ConfigDialog(self.config_manager, self) if dialog.exec() == QDialog.Accepted: self._apply_config() 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}") def update_status(self, text: str): """更新状态""" self.status_label.setText(text) def set_refresh_callback(self, callback: Callable): """设置刷新按钮回调""" self.refresh_btn.clicked.connect(callback) def set_config_callback(self, callback: Callable): """设置配置按钮回调""" self.config_btn.clicked.connect(callback) def show_message(self, title: str, message: str, icon=QMessageBox.Information): """显示消息""" QMessageBox.information(self, title, message) class QCheckBox(QPushButton): """自定义复选框""" def __init__(self, text=""): super().__init__(text) self.setCheckable(True) self.setChecked(False)