2026-01-07 17:32:58 +08:00
|
|
|
|
"""
|
|
|
|
|
|
PySide6 GUI界面模块
|
|
|
|
|
|
"""
|
|
|
|
|
|
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
|
|
|
|
|
QPushButton, QSlider, QDialog, QFormLayout,
|
|
|
|
|
|
QLineEdit, QSpinBox, QMessageBox, QSystemTrayIcon,
|
2026-01-28 17:30:30 +08:00
|
|
|
|
QMenu, QTextEdit, QGroupBox, QDialogButtonBox, QCheckBox, QScrollArea, QFileDialog,
|
|
|
|
|
|
QTabWidget)
|
2026-01-07 17:32:58 +08:00
|
|
|
|
from PySide6.QtCore import Qt, QTimer, Signal, QPoint
|
2026-01-13 17:06:18 +08:00
|
|
|
|
from PySide6.QtGui import QFont, QColor, QPainter, QBrush, QPen, QIcon, QAction, QPixmap
|
2026-01-07 17:32:58 +08:00
|
|
|
|
from typing import Callable, Optional
|
2026-01-12 09:19:38 +08:00
|
|
|
|
from loguru import logger
|
|
|
|
|
|
|
2026-01-07 17:32:58 +08:00
|
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
|
"""根据分数获取颜色"""
|
2026-01-12 09:19:38 +08:00
|
|
|
|
# 45-55分区间:每1分一个颜色(从绿色到黄色渐变)
|
|
|
|
|
|
if 45 <= score <= 55:
|
|
|
|
|
|
# 在45-55分之间,每1分一个颜色
|
|
|
|
|
|
ratio = (score - 45) / 10 # 0到1之间的比例
|
|
|
|
|
|
# 从绿色(0, 200, 0)渐变到黄色(255, 255, 0)
|
|
|
|
|
|
r = int(0 + 255 * ratio)
|
|
|
|
|
|
g = 200 if ratio < 0.5 else int(200 + 55 * (ratio - 0.5) * 2)
|
|
|
|
|
|
b = int(0 + 0 * ratio)
|
|
|
|
|
|
return QColor(r, g, b)
|
|
|
|
|
|
|
|
|
|
|
|
# 45分以下:每5分一个颜色(从深蓝到绿色渐变)
|
|
|
|
|
|
elif score < 45:
|
|
|
|
|
|
# 将分数映射到0-8的区间(0-40分,每5分一个级别)
|
|
|
|
|
|
level = score // 5
|
|
|
|
|
|
# 从深蓝色(0, 100, 255)渐变到绿色(0, 200, 0)
|
|
|
|
|
|
ratio = level / 8 # 0-8对应0-40分
|
|
|
|
|
|
r = int(0 + 0 * ratio)
|
|
|
|
|
|
g = int(100 + 100 * ratio)
|
|
|
|
|
|
b = int(255 - 255 * ratio)
|
|
|
|
|
|
return QColor(r, g, b)
|
|
|
|
|
|
|
|
|
|
|
|
# 55分以上:每5分一个颜色(从黄色到红色渐变)
|
2026-01-07 17:32:58 +08:00
|
|
|
|
else:
|
2026-01-12 09:19:38 +08:00
|
|
|
|
# 将分数映射到0-8的区间(60-100分,每5分一个级别)
|
|
|
|
|
|
level = (score - 60) // 5
|
|
|
|
|
|
level = max(0, min(8, level)) # 限制在0-8范围内
|
|
|
|
|
|
# 从黄色(255, 255, 0)渐变到红色(255, 0, 0)
|
|
|
|
|
|
ratio = level / 8
|
|
|
|
|
|
r = 255
|
|
|
|
|
|
g = int(255 - 255 * ratio)
|
|
|
|
|
|
b = 0
|
|
|
|
|
|
return QColor(r, g, b)
|
2026-01-07 17:32:58 +08:00
|
|
|
|
|
|
|
|
|
|
def get_description(self, score: int) -> str:
|
|
|
|
|
|
"""获取描述文本"""
|
2026-01-13 17:06:18 +08:00
|
|
|
|
if score < 30:
|
|
|
|
|
|
return "极度悲观"
|
|
|
|
|
|
elif score < 39:
|
|
|
|
|
|
return "悲观"
|
|
|
|
|
|
elif score < 45:
|
2026-01-07 17:32:58 +08:00
|
|
|
|
return "偏悲观"
|
2026-01-13 17:06:18 +08:00
|
|
|
|
elif score < 55:
|
|
|
|
|
|
return "中立"
|
|
|
|
|
|
elif score < 65:
|
2026-01-07 17:32:58 +08:00
|
|
|
|
return "偏乐观"
|
2026-01-13 17:06:18 +08:00
|
|
|
|
elif score < 70:
|
|
|
|
|
|
return "乐观"
|
2026-01-07 17:32:58 +08:00
|
|
|
|
else:
|
2026-01-13 17:06:18 +08:00
|
|
|
|
return "极度乐观"
|
2026-01-07 17:32:58 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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):
|
2026-01-28 17:30:30 +08:00
|
|
|
|
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)
|
2026-01-07 17:32:58 +08:00
|
|
|
|
|
|
|
|
|
|
llm_config = self.config_manager.llm_api_config
|
|
|
|
|
|
|
|
|
|
|
|
self.base_url_edit = QLineEdit(llm_config.get('base_url', ''))
|
2026-01-28 17:30:30 +08:00
|
|
|
|
self.base_url_edit.setPlaceholderText("https://api.openai.com/v1")
|
2026-01-07 17:32:58 +08:00
|
|
|
|
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', ''))
|
2026-01-28 17:30:30 +08:00
|
|
|
|
self.model_edit.setPlaceholderText("gpt-3.5-turbo")
|
2026-01-07 17:32:58 +08:00
|
|
|
|
self.timeout_spin = QSpinBox()
|
|
|
|
|
|
self.timeout_spin.setRange(10, 300)
|
|
|
|
|
|
self.timeout_spin.setValue(llm_config.get('timeout', 30))
|
2026-01-28 17:30:30 +08:00
|
|
|
|
self.timeout_spin.setSuffix(" 秒")
|
2026-01-07 17:32:58 +08:00
|
|
|
|
|
2026-01-28 17:30:30 +08:00
|
|
|
|
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)
|
2026-01-07 17:32:58 +08:00
|
|
|
|
|
|
|
|
|
|
spider_config = self.config_manager.spider_config
|
|
|
|
|
|
|
|
|
|
|
|
self.url_edit = QLineEdit(spider_config.get('target_url', ''))
|
2026-01-28 17:30:30 +08:00
|
|
|
|
self.url_edit.setPlaceholderText("https://example.com")
|
2026-01-07 17:32:58 +08:00
|
|
|
|
self.xpath_edit = QLineEdit(spider_config.get('xpath', ''))
|
2026-01-28 17:30:30 +08:00
|
|
|
|
self.xpath_edit.setPlaceholderText("//div[@class='content']")
|
2026-01-07 17:32:58 +08:00
|
|
|
|
self.user_agent_edit = QLineEdit(spider_config.get('user_agent', ''))
|
2026-01-28 17:30:30 +08:00
|
|
|
|
self.user_agent_edit.setPlaceholderText("Mozilla/5.0...")
|
2026-01-07 17:32:58 +08:00
|
|
|
|
self.interval_spin = QSpinBox()
|
|
|
|
|
|
self.interval_spin.setRange(10, 3600)
|
2026-01-12 09:19:38 +08:00
|
|
|
|
self.interval_spin.setValue(spider_config.get('fetch_interval', 15))
|
2026-01-28 17:30:30 +08:00
|
|
|
|
self.interval_spin.setSuffix(" 秒")
|
2026-01-07 17:32:58 +08:00
|
|
|
|
|
2026-01-28 17:30:30 +08:00
|
|
|
|
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)
|
2026-01-07 17:32:58 +08:00
|
|
|
|
|
2026-01-28 17:30:30 +08:00
|
|
|
|
# Chrome浏览器路径
|
2026-01-23 14:12:10 +08:00
|
|
|
|
chrome_path_layout = QHBoxLayout()
|
2026-01-28 17:30:30 +08:00
|
|
|
|
chrome_path_layout.setSpacing(8)
|
2026-01-23 14:12:10 +08:00
|
|
|
|
self.chrome_path_edit = QLineEdit(spider_config.get('chrome_path', ''))
|
|
|
|
|
|
self.chrome_path_edit.setPlaceholderText("留空则自动查找Chrome浏览器")
|
|
|
|
|
|
self.chrome_browse_btn = QPushButton("浏览...")
|
2026-01-28 17:30:30 +08:00
|
|
|
|
self.chrome_browse_btn.setFixedWidth(60)
|
2026-01-23 14:12:10 +08:00
|
|
|
|
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)
|
2026-01-28 17:30:30 +08:00
|
|
|
|
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)
|
2026-01-22 16:19:57 +08:00
|
|
|
|
|
2026-01-07 17:32:58 +08:00
|
|
|
|
ui_config = self.config_manager.ui_config
|
|
|
|
|
|
|
2026-01-28 17:30:30 +08:00
|
|
|
|
# 透明度
|
|
|
|
|
|
opacity_layout = QHBoxLayout()
|
|
|
|
|
|
opacity_layout.setSpacing(8)
|
2026-01-07 17:32:58 +08:00
|
|
|
|
self.opacity_slider = QSlider(Qt.Horizontal)
|
|
|
|
|
|
self.opacity_slider.setRange(30, 100)
|
|
|
|
|
|
self.opacity_slider.setValue(int(ui_config.get('opacity', 0.9) * 100))
|
2026-01-28 17:30:30 +08:00
|
|
|
|
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()
|
|
|
|
|
|
|
|
|
|
|
|
# 窗口置顶
|
|
|
|
|
|
self.ontop_btn = QPushButton("窗口置顶")
|
2026-01-07 17:32:58 +08:00
|
|
|
|
self.ontop_btn.setCheckable(True)
|
2026-01-28 17:30:30 +08:00
|
|
|
|
self.ontop_btn.setFixedWidth(100)
|
2026-01-07 17:32:58 +08:00
|
|
|
|
self.ontop_btn.setChecked(ui_config.get('is_on_top', True))
|
2026-01-28 17:30:30 +08:00
|
|
|
|
self.ontop_btn.setStyleSheet(
|
|
|
|
|
|
"QPushButton { background-color: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; padding: 6px; }"
|
|
|
|
|
|
"QPushButton:checked { background-color: #2196F3; color: white; }"
|
|
|
|
|
|
)
|
2026-01-07 17:32:58 +08:00
|
|
|
|
|
2026-01-28 17:30:30 +08:00
|
|
|
|
ui_layout.addRow("透明度:", opacity_layout)
|
|
|
|
|
|
ui_layout.addRow("窗口行为:", self.ontop_btn)
|
2026-01-07 17:32:58 +08:00
|
|
|
|
|
|
|
|
|
|
# 阈值配置
|
|
|
|
|
|
thresholds = ui_config.get('thresholds', {})
|
2026-01-28 17:30:30 +08:00
|
|
|
|
|
|
|
|
|
|
threshold_layout = QHBoxLayout()
|
|
|
|
|
|
threshold_layout.setSpacing(12)
|
|
|
|
|
|
|
2026-01-07 17:32:58 +08:00
|
|
|
|
self.cold_spin = QSpinBox()
|
|
|
|
|
|
self.cold_spin.setRange(0, 50)
|
|
|
|
|
|
self.cold_spin.setValue(thresholds.get('cold', 30))
|
2026-01-28 17:30:30 +08:00
|
|
|
|
self.cold_spin.setSuffix(" 分")
|
|
|
|
|
|
self.cold_spin.setFixedWidth(80)
|
|
|
|
|
|
|
2026-01-07 17:32:58 +08:00
|
|
|
|
self.warm_spin = QSpinBox()
|
|
|
|
|
|
self.warm_spin.setRange(50, 100)
|
|
|
|
|
|
self.warm_spin.setValue(thresholds.get('warm', 70))
|
2026-01-28 17:30:30 +08:00
|
|
|
|
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)
|
2026-01-07 17:32:58 +08:00
|
|
|
|
|
2026-01-28 17:30:30 +08:00
|
|
|
|
# 添加标签页
|
|
|
|
|
|
tab_widget.addTab(api_tab, "API 配置")
|
|
|
|
|
|
tab_widget.addTab(spider_tab, "爬虫配置")
|
|
|
|
|
|
tab_widget.addTab(ui_tab, "界面设置")
|
2026-01-07 17:32:58 +08:00
|
|
|
|
|
2026-01-28 17:30:30 +08:00
|
|
|
|
layout.addWidget(tab_widget)
|
|
|
|
|
|
|
|
|
|
|
|
# 底部按钮
|
|
|
|
|
|
button_layout = QHBoxLayout()
|
|
|
|
|
|
button_layout.setContentsMargins(20, 0, 20, 20)
|
|
|
|
|
|
button_layout.setSpacing(8)
|
|
|
|
|
|
|
2026-01-07 17:32:58 +08:00
|
|
|
|
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
|
|
|
|
button_box.accepted.connect(self._save_config)
|
|
|
|
|
|
button_box.rejected.connect(self.reject)
|
2026-01-28 17:30:30 +08:00
|
|
|
|
|
|
|
|
|
|
# 设置按钮样式
|
|
|
|
|
|
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)
|
2026-01-07 17:32:58 +08:00
|
|
|
|
|
2026-01-23 14:12:10 +08:00
|
|
|
|
def _browse_chrome_path(self):
|
|
|
|
|
|
"""浏览Chrome路径"""
|
|
|
|
|
|
file_path, _ = QFileDialog.getOpenFileName(
|
2026-01-22 16:19:57 +08:00
|
|
|
|
self,
|
2026-01-23 14:12:10 +08:00
|
|
|
|
"选择Chrome浏览器可执行文件",
|
2026-01-22 16:19:57 +08:00
|
|
|
|
"",
|
2026-01-23 14:12:10 +08:00
|
|
|
|
"Chrome浏览器 (*.exe);;所有文件 (*.*)"
|
2026-01-22 16:19:57 +08:00
|
|
|
|
)
|
2026-01-23 14:12:10 +08:00
|
|
|
|
if file_path:
|
|
|
|
|
|
self.chrome_path_edit.setText(file_path)
|
2026-01-22 16:19:57 +08:00
|
|
|
|
|
2026-01-07 17:32:58 +08:00
|
|
|
|
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(),
|
2026-01-22 16:19:57 +08:00
|
|
|
|
fetch_interval=self.interval_spin.value(),
|
2026-01-23 14:12:10 +08:00
|
|
|
|
chrome_path=self.chrome_path_edit.text()
|
2026-01-07 17:32:58 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 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):
|
|
|
|
|
|
"""主窗口"""
|
|
|
|
|
|
|
2026-01-12 09:19:38 +08:00
|
|
|
|
def __init__(self, config_manager, spider_manager=None, parent=None):
|
2026-01-07 17:32:58 +08:00
|
|
|
|
super().__init__(parent)
|
|
|
|
|
|
self.config_manager = config_manager
|
2026-01-12 09:19:38 +08:00
|
|
|
|
self.spider_manager = spider_manager
|
|
|
|
|
|
|
|
|
|
|
|
# 获取页面标题并设置窗口标题
|
|
|
|
|
|
self._set_window_title()
|
|
|
|
|
|
|
2026-01-07 17:32:58 +08:00
|
|
|
|
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)
|
2026-01-28 17:30:30 +08:00
|
|
|
|
layout.setContentsMargins(16, 16, 16, 16)
|
|
|
|
|
|
layout.setSpacing(12)
|
2026-01-07 17:32:58 +08:00
|
|
|
|
|
|
|
|
|
|
# 标题
|
2026-01-28 17:30:30 +08:00
|
|
|
|
self.title_label = QLabel("上证指数 sh000001")
|
2026-01-07 17:32:58 +08:00
|
|
|
|
self.title_label.setAlignment(Qt.AlignCenter)
|
|
|
|
|
|
title_font = QFont()
|
2026-01-28 17:30:30 +08:00
|
|
|
|
title_font.setPointSize(16)
|
2026-01-07 17:32:58 +08:00
|
|
|
|
title_font.setBold(True)
|
|
|
|
|
|
self.title_label.setFont(title_font)
|
2026-01-28 17:30:30 +08:00
|
|
|
|
self.title_label.setStyleSheet("color: #333;")
|
2026-01-07 17:32:58 +08:00
|
|
|
|
|
|
|
|
|
|
# 指示灯
|
|
|
|
|
|
self.indicator = SentimentIndicator()
|
2026-01-28 17:30:30 +08:00
|
|
|
|
self.indicator.setMinimumSize(120, 120)
|
|
|
|
|
|
|
|
|
|
|
|
# 分数和标签
|
|
|
|
|
|
self.score_label = QLabel("50")
|
2026-01-07 17:32:58 +08:00
|
|
|
|
self.score_label.setAlignment(Qt.AlignCenter)
|
2026-01-28 17:30:30 +08:00
|
|
|
|
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;")
|
2026-01-07 17:32:58 +08:00
|
|
|
|
|
|
|
|
|
|
# 状态信息
|
|
|
|
|
|
self.status_label = QLabel("等待数据...")
|
|
|
|
|
|
self.status_label.setAlignment(Qt.AlignCenter)
|
2026-01-28 17:30:30 +08:00
|
|
|
|
self.status_label.setStyleSheet("color: #999; font-size: 11px;")
|
2026-01-07 17:32:58 +08:00
|
|
|
|
|
2026-01-13 17:06:18 +08:00
|
|
|
|
# 上证所截图显示
|
|
|
|
|
|
self.screenshot_label = QLabel("等待截图...")
|
|
|
|
|
|
self.screenshot_label.setAlignment(Qt.AlignCenter)
|
2026-01-28 17:30:30 +08:00
|
|
|
|
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;"
|
|
|
|
|
|
"}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-07 17:32:58 +08:00
|
|
|
|
|
|
|
|
|
|
# 添加到主布局
|
|
|
|
|
|
layout.addWidget(self.title_label)
|
2026-01-28 17:30:30 +08:00
|
|
|
|
layout.addWidget(self.indicator, alignment=Qt.AlignCenter)
|
2026-01-07 17:32:58 +08:00
|
|
|
|
layout.addWidget(self.score_label)
|
2026-01-28 17:30:30 +08:00
|
|
|
|
layout.addWidget(self.sentiment_label)
|
|
|
|
|
|
layout.addWidget(self.screenshot_label)
|
2026-01-07 17:32:58 +08:00
|
|
|
|
layout.addWidget(self.status_label)
|
|
|
|
|
|
|
|
|
|
|
|
# 设置窗口标志(无边框、可拖拽)
|
|
|
|
|
|
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
|
|
|
|
|
|
self.setAttribute(Qt.WA_TranslucentBackground)
|
2026-01-28 17:30:30 +08:00
|
|
|
|
self.setStyleSheet(
|
|
|
|
|
|
"QWidget {"
|
|
|
|
|
|
" background-color: rgba(255, 255, 255, 0.95);"
|
|
|
|
|
|
" border-radius: 12px;"
|
|
|
|
|
|
"}"
|
|
|
|
|
|
)
|
2026-01-07 17:32:58 +08:00
|
|
|
|
|
2026-01-12 09:19:38 +08:00
|
|
|
|
def _set_window_title(self):
|
|
|
|
|
|
"""设置窗口标题"""
|
|
|
|
|
|
logger.debug("设置窗口标题")
|
|
|
|
|
|
|
|
|
|
|
|
# 尝试从爬虫获取页面标题
|
|
|
|
|
|
if hasattr(self, 'spider_manager') and self.spider_manager:
|
|
|
|
|
|
try:
|
|
|
|
|
|
page_title = self.spider_manager.get_page_title()
|
|
|
|
|
|
if page_title:
|
|
|
|
|
|
# 从页面标题中提取股票名称
|
|
|
|
|
|
import re
|
|
|
|
|
|
match = re.search(r'(上证指数sh\d+)', page_title)
|
|
|
|
|
|
if match:
|
|
|
|
|
|
stock_name = match.group(1)
|
|
|
|
|
|
window_title = f"冷暖值 - {stock_name}"
|
|
|
|
|
|
self.setWindowTitle(window_title)
|
|
|
|
|
|
|
|
|
|
|
|
# 同时更新主标题标签(如果已初始化)
|
|
|
|
|
|
if hasattr(self, 'title_label'):
|
|
|
|
|
|
self.title_label.setText("上证指数sh000001")
|
|
|
|
|
|
logger.info(f"设置窗口标题: {window_title}")
|
|
|
|
|
|
return
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"获取页面标题失败: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
# 如果获取失败,使用默认标题
|
|
|
|
|
|
self.setWindowTitle("冷暖值 - 股吧人气")
|
|
|
|
|
|
logger.info("使用默认窗口标题")
|
|
|
|
|
|
|
2026-01-07 17:32:58 +08:00
|
|
|
|
def _init_tray_icon(self):
|
|
|
|
|
|
"""初始化系统托盘"""
|
2026-01-12 09:19:38 +08:00
|
|
|
|
logger.debug("初始化系统托盘")
|
2026-01-07 17:32:58 +08:00
|
|
|
|
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()
|
2026-01-12 09:19:38 +08:00
|
|
|
|
logger.info("系统托盘初始化完成")
|
2026-01-07 17:32:58 +08:00
|
|
|
|
|
|
|
|
|
|
def quit_app(self):
|
|
|
|
|
|
"""退出应用"""
|
2026-01-12 09:19:38 +08:00
|
|
|
|
logger.info("退出应用")
|
2026-01-07 17:32:58 +08:00
|
|
|
|
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)
|
2026-01-28 17:30:30 +08:00
|
|
|
|
refresh_action = QAction("刷新", self)
|
2026-01-07 17:32:58 +08:00
|
|
|
|
config_action = QAction("配置", self)
|
|
|
|
|
|
quit_action = QAction("退出", self)
|
|
|
|
|
|
|
2026-01-28 17:30:30 +08:00
|
|
|
|
refresh_action.triggered.connect(self._on_refresh)
|
2026-01-07 17:32:58 +08:00
|
|
|
|
config_action.triggered.connect(self.show_config)
|
|
|
|
|
|
quit_action.triggered.connect(self.quit_app)
|
|
|
|
|
|
|
2026-01-28 17:30:30 +08:00
|
|
|
|
context_menu.addAction(refresh_action)
|
2026-01-07 17:32:58 +08:00
|
|
|
|
context_menu.addAction(config_action)
|
2026-01-28 17:30:30 +08:00
|
|
|
|
context_menu.addSeparator()
|
2026-01-07 17:32:58 +08:00
|
|
|
|
context_menu.addAction(quit_action)
|
2026-01-12 09:19:38 +08:00
|
|
|
|
context_menu.exec(event.globalPos())
|
2026-01-07 17:32:58 +08:00
|
|
|
|
|
|
|
|
|
|
def show_config(self):
|
|
|
|
|
|
"""显示配置对话框"""
|
|
|
|
|
|
dialog = ConfigDialog(self.config_manager, self)
|
|
|
|
|
|
if dialog.exec() == QDialog.Accepted:
|
|
|
|
|
|
self._apply_config()
|
|
|
|
|
|
|
2026-01-28 17:30:30 +08:00
|
|
|
|
def _on_refresh(self):
|
|
|
|
|
|
"""刷新回调"""
|
|
|
|
|
|
if hasattr(self, '_refresh_callback') and self._refresh_callback:
|
|
|
|
|
|
self._refresh_callback()
|
|
|
|
|
|
logger.info("执行刷新操作")
|
|
|
|
|
|
|
2026-01-07 17:32:58 +08:00
|
|
|
|
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)
|
2026-01-28 17:30:30 +08:00
|
|
|
|
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;")
|
|
|
|
|
|
|
2026-01-12 09:19:38 +08:00
|
|
|
|
logger.debug(f"更新指示灯: {score}分 - {label}")
|
2026-01-07 17:32:58 +08:00
|
|
|
|
|
2026-01-28 17:30:30 +08:00
|
|
|
|
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"
|
|
|
|
|
|
|
2026-01-07 17:32:58 +08:00
|
|
|
|
def update_status(self, text: str):
|
|
|
|
|
|
"""更新状态"""
|
|
|
|
|
|
self.status_label.setText(text)
|
2026-01-12 09:19:38 +08:00
|
|
|
|
logger.debug(f"更新状态: {text}")
|
2026-01-07 17:32:58 +08:00
|
|
|
|
|
|
|
|
|
|
def set_refresh_callback(self, callback: Callable):
|
2026-01-28 17:30:30 +08:00
|
|
|
|
"""设置刷新回调"""
|
|
|
|
|
|
self._refresh_callback = callback
|
|
|
|
|
|
logger.debug("设置刷新回调")
|
2026-01-07 17:32:58 +08:00
|
|
|
|
|
|
|
|
|
|
def set_config_callback(self, callback: Callable):
|
2026-01-28 17:30:30 +08:00
|
|
|
|
"""设置配置回调(已废弃,配置直接通过右键菜单调用)"""
|
|
|
|
|
|
logger.debug("配置回调已废弃,配置直接通过右键菜单调用")
|
2026-01-07 17:32:58 +08:00
|
|
|
|
|
2026-01-12 09:19:38 +08:00
|
|
|
|
def show_message(self, title: str, message: str):
|
2026-01-07 17:32:58 +08:00
|
|
|
|
"""显示消息"""
|
2026-01-12 09:19:38 +08:00
|
|
|
|
logger.info(f"显示消息: {title} - {message}")
|
2026-01-07 17:32:58 +08:00
|
|
|
|
QMessageBox.information(self, title, message)
|
|
|
|
|
|
|
2026-01-13 17:06:18 +08:00
|
|
|
|
def update_sse_screenshot(self, screenshot_path: str):
|
|
|
|
|
|
"""更新上证所截图显示"""
|
|
|
|
|
|
logger.info(f"更新截图显示: {screenshot_path}")
|
|
|
|
|
|
pixmap = QPixmap(screenshot_path)
|
|
|
|
|
|
if not pixmap.isNull():
|
|
|
|
|
|
self.screenshot_label.setPixmap(pixmap.scaled(
|
|
|
|
|
|
self.screenshot_label.size(),
|
|
|
|
|
|
Qt.KeepAspectRatio,
|
|
|
|
|
|
Qt.SmoothTransformation
|
|
|
|
|
|
))
|
|
|
|
|
|
self.screenshot_label.setText("")
|
|
|
|
|
|
logger.info("截图显示更新成功")
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.screenshot_label.setText("截图加载失败")
|
|
|
|
|
|
logger.warning("截图加载失败")
|
|
|
|
|
|
|
2026-01-14 14:39:44 +08:00
|
|
|
|
def add_waveform_data(self, time_str: str, value: float):
|
|
|
|
|
|
"""添加波形图数据点(暂未实现波形图功能)"""
|
|
|
|
|
|
logger.debug(f"波形图数据: 时间={time_str}, 值={value}")
|
|
|
|
|
|
|
2026-01-07 17:32:58 +08:00
|
|
|
|
|
|
|
|
|
|
class QCheckBox(QPushButton):
|
|
|
|
|
|
"""自定义复选框"""
|
|
|
|
|
|
def __init__(self, text=""):
|
|
|
|
|
|
super().__init__(text)
|
|
|
|
|
|
self.setCheckable(True)
|
|
|
|
|
|
self.setChecked(False)
|