feat(ui): 重构配置对话框和主窗口界面
- 将配置对话框改为标签页布局,分为API配置、爬虫配置和界面设置 - 优化主窗口UI,包括指示灯样式、分数显示和截图区域 - 添加窗口圆角和半透明效果 - 改进右键菜单功能,增加刷新操作 - 优化状态显示和分数颜色标识
This commit is contained in:
261
main_window.py
261
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):
|
||||
"""显示消息"""
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 56 KiB |
Reference in New Issue
Block a user