feat(ui): 重构配置对话框和主窗口界面

- 将配置对话框改为标签页布局,分为API配置、爬虫配置和界面设置
- 优化主窗口UI,包括指示灯样式、分数显示和截图区域
- 添加窗口圆角和半透明效果
- 改进右键菜单功能,增加刷新操作
- 优化状态显示和分数颜色标识
This commit is contained in:
2026-01-28 17:30:30 +08:00
parent f256bd0852
commit ee721e9abe
2 changed files with 196 additions and 65 deletions

View File

@@ -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):
"""显示消息"""

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 56 KiB