tools-pyqt/src/ui/app.py

558 lines
22 KiB
Python

import time
from enum import Enum
from loguru import logger
from PyQt6.QtCore import QDate, QDateTime, Qt, QThreadPool, QTime, QTimer
from PyQt6.QtGui import QAction, QColor, QIcon
from PyQt6.QtWidgets import (QApplication, QCheckBox, QDateEdit, QHBoxLayout,
QHeaderView, QMainWindow, QMenu, QMessageBox,
QPushButton, QSizePolicy, QSystemTrayIcon,
QTableWidget, QTableWidgetItem, QTabWidget,
QTextEdit, QVBoxLayout, QWidget)
from src.ui.config import ConfigManager
from src.core.message_client import send_message
from src.core.util import convert_data, resource_path
from src.entity.member import get_today_new_member_list
from src.entity.pay_record import get_latest_deposit_user
from src.entity.user import get_user_by_username_and_password
from src.ui import global_signals
from src.ui.data_query import ButtonTask, ReportTask
from src.ui.title_bar import CustomTitleBar
class Application(QMainWindow):
def __init__(self):
super().__init__()
self.config_manager = ConfigManager()
self.thread_pool = QThreadPool()
self.report_timer = None
self.date_update_timer = None
self.username = None
self.password = None
self.minimum = False
self.customTitleBar = None
self.tray_icon = None
self.tables = {}
self.is_dragging = False
self.drag_position = None
self.is_resizing = False
self.resize_direction = None
self.toaster_notify_enabled = True
self.telegram_notify_enabled = True
self.initialize_application()
def initialize_application(self):
# 1. 加载配置文件
self.load_config()
# 2. 设置 UI
self.setup_ui()
# 4. 初始化系统托盘图标
self.init_tray_icon()
# 5. 初始化表格数据
self.init_table_data()
self.setup_timers()
self.connect_signals()
def setup_ui(self):
self.apply_stylesheet()
self.set_window_properties()
self.create_central_widget()
self.setup_layouts()
def setup_timers(self):
self.setup_date_update_timer()
self.setup_report_timer()
def connect_signals(self):
self.tray_icon.activated.connect(self.tray_icon_clicked)
global_signals.user_data_updated.connect(self.refresh_user_data)
def set_window_properties(self):
self.resize(600, 400)
self.setWindowTitle("zayac的小工具")
self.setWindowIcon(QIcon("icons:icon.png"))
self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
def create_central_widget(self):
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)
self.main_layout = QVBoxLayout(self.central_widget)
def setup_layouts(self):
self.setup_top_panel()
self.setup_middle_panel()
self.setup_bottom_panel()
self.customTitleBar = CustomTitleBar(self)
self.setMenuWidget(self.customTitleBar)
def init_tray_icon(self):
self.tray_icon = SystemTrayIcon(QIcon("icons:icon.png"), self) # 替换为正确的图标路径
self.tray_icon.show()
def load_config(self):
# 使用
try:
self.username = self.config_manager.get('Credentials', 'username')
self.password = self.config_manager.get('Credentials', 'password')
self.minimum = self.config_manager.get('Minimum', 'minimum')
self.user = get_user_by_username_and_password(self.username, self.password)
except FileNotFoundError as e:
QMessageBox.warning(None, "警告", "配置文件信息加载失败!请检查文件是否存在")
return None, None
def init_table_data(self):
# 初始化表格数据
data = self.query_initial_data(self.user)
for row in data:
time_str = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())
self.update_table(row.agentCode, [time_str, row.registerMembers, row.firstDepositNum, row.netWinLose,
row.effectiveNew, row.activeMembers])
def query_initial_data(self, user):
return ReportTask.get_banner_info_by_user(user)
def setup_report_timer(self):
self.report_timer = QTimer(self)
self.report_timer.timeout.connect(self.update_reports)
self.report_timer.start(60000)
def update_reports(self):
for account in self.user.accounts:
report_task = ReportTask(account)
report_task.signals.table_updated.connect(self.update_table)
self.thread_pool.start(report_task)
def apply_stylesheet(self):
style_sheet = self.load_stylesheet(resource_path('ui/style.qss'))
self.setStyleSheet(style_sheet)
def refresh_user_data(self):
# 刷新用户数据的逻辑
self.user = get_user_by_username_and_password(self.username, self.password) # 重新加载用户
@staticmethod
def load_stylesheet(file_path):
try:
with open(file_path, "r", encoding='utf-8') as file:
return file.read()
except FileNotFoundError:
print(f"无法找到样式文件: {file_path}")
return ""
def get_account_by_account_username(self, username: str):
for account in self.user.accounts:
if account.username == username:
return account
def setup_top_panel(self):
try:
self.top_panel = QHBoxLayout()
self.add_buttons_to_top_panel()
self.add_date_picker_to_top_panel()
self.add_checkboxes_to_top_panel()
self.setup_report_button()
self.main_layout.addLayout(self.top_panel)
except Exception as e:
logger.error(f"Error setting up top panel: {e}")
def add_date_picker_to_top_panel(self):
# 创建日期选择器
self.dateEdit = QDateEdit()
self.dateEdit.setCalendarPopup(True)
self.dateEdit.setDate(QDate.currentDate())
# 设置日期范围
today = QDate.currentDate()
first_day_last_month = QDate(today.year(), today.month(), 1).addMonths(-1)
self.dateEdit.setMinimumDate(first_day_last_month)
self.dateEdit.setMaximumDate(today)
# 将日期选择器添加到顶部面板
self.top_panel.addWidget(self.dateEdit)
def setup_date_update_timer(self):
self.date_update_timer = QTimer(self)
self.date_update_timer.timeout.connect(self.update_date_edit)
self.start_date_update_timer()
def start_date_update_timer(self):
now = QDateTime.currentDateTime()
next_midnight = QDateTime(now.date().addDays(1), QTime(0, 0)) # 设置为次日的午夜 00:00
interval = now.msecsTo(next_midnight)
self.date_update_timer.start(interval if interval > 0 else 86400000) # 86400000ms = 24小时
def update_date_edit(self):
self.dateEdit.setDate(QDate.currentDate()) # 设置 QDateEdit 控件的日期为当前日期
self.update_date_range()
self.start_date_update_timer()
def update_date_range(self):
today = QDate.currentDate()
first_day_last_month = QDate(today.year(), today.month(), 1).addMonths(-1)
self.dateEdit.setMinimumDate(first_day_last_month)
self.dateEdit.setMaximumDate(today)
def setup_report_button(self):
self.report_button = QPushButton("停止喜报")
self.report_button.setCheckable(True)
self.report_button.setChecked(True) # 默认设置为选中状态
self.report_button.setObjectName("Warning")
self.report_button.clicked.connect(self.on_report_clicked)
self.top_panel.addWidget(self.report_button)
def add_buttons_to_top_panel(self):
for name, style in self.get_buttons_info():
self.create_and_add_button(name, style)
def get_buttons_info(self):
return [
("报数", "Primary"),
("存款失败用户", "Danger"),
("负盈利", "Success"),
("薪资", "Light"),
]
def create_and_add_button(self, name, style):
button = QPushButton(name)
button.setObjectName(style)
button.clicked.connect(lambda _, n=name: self.query_data(n))
self.top_panel.addWidget(button)
def add_checkboxes_to_top_panel(self):
# 创建垂直布局来放置复选框
checkbox_layout = QVBoxLayout()
# 添加复选框
self.system_notification_checkbox = QCheckBox("系统通知")
self.telegram_notification_checkbox = QCheckBox("飞机通知")
self.system_notification_checkbox.setChecked(True)
self.telegram_notification_checkbox.setChecked(True)
self.system_notification_checkbox.stateChanged.connect(self.toggle_system_notification)
self.telegram_notification_checkbox.stateChanged.connect(self.toggle_telegram_notification)
checkbox_layout.addWidget(self.system_notification_checkbox)
checkbox_layout.addWidget(self.telegram_notification_checkbox)
# 将复选框布局添加到顶部面板
self.top_panel.addLayout(checkbox_layout)
def toggle_system_notification(self, state):
self.toaster_notify_enabled = state == Qt.CheckState.Checked
def toggle_telegram_notification(self, state):
self.telegram_notify_enabled = state == Qt.CheckState.Checked
def query_data(self, btn_name):
# 获取日期控件的当前值
selected_date = self.dateEdit.date()
# 转换为所需的格式
selected_date_str = selected_date.toString("yyyy-MM-dd")
# 在文本框中显示查询中的消息
self.txt.append(f"正在查询{selected_date_str}{btn_name},请等待...\n")
self.start_data_query(btn_name, selected_date_str)
def start_data_query(self, query_type, selected_date_str):
task = ButtonTask(query_type, selected_date_str, self.user)
task.signals.query_completed.connect(self.display_query_result)
self.thread_pool.start(task)
def display_query_result(self, result, auto_clipboard, need_notify):
if auto_clipboard:
copy_to_clipboard(result)
if need_notify and '' not in result:
self.tray_icon.showMessage("", "自动复制成功", QSystemTrayIcon.MessageIcon.Information, 500)
self.txt.append(result)
def send_notification(self, emoji, account_name, title, count, results, total):
msg = f'{emoji} {account_name} {title}:{count} {results} 总数:*{total}*'
if self.toaster_notify_enabled:
self.tray_icon.showMessage(f"{title}通知", msg, QSystemTrayIcon.MessageIcon.Information, 2000)
if self.telegram_notify_enabled:
send_message(self.user.bot_token, self.user.group_id, msg)
def on_report_clicked(self):
try:
if self.report_button.isChecked():
self.report_timer.start(60000) # 启动定时器
self.report_button.setText("停止喜报") # 更改按钮文本
# 更改按钮样式为 "Warning"
self.report_button.setObjectName("Warning")
else:
self.report_timer.stop() # 停止定时器
self.report_button.setText("启动喜报") # 恢复按钮文本
self.report_button.setObjectName("Success")
# 重新应用样式来更新按钮外观
self.apply_stylesheet()
except Exception as e:
logger.debug(e)
def update_table(self, account_username, data):
try:
table = self.tables.get(account_username)
if not table:
return
self.ensure_table_row_limit(table)
self.insert_data_in_table(table, data, account_username)
except Exception as e:
logger.error(f"Error updating table for {account_username}: {e}")
def ensure_table_row_limit(self, table, row_limit=20):
if table.rowCount() >= row_limit:
table.removeRow(0)
def insert_data_in_table(self, table, data, account_username):
# 获取当前的行数
row_count = table.rowCount()
table.insertRow(row_count)
notifications = []
for col, cell_data in enumerate(data):
self.set_table_item(table, row_count, col, cell_data)
self.check_data_change_for_notifications(table, row_count, col, cell_data, account_username, notifications)
self.send_all_notifications(notifications)
def send_all_notifications(self, notifications):
"""发送所有通知"""
for notification in notifications:
self.send_notification(*notification)
def set_table_item(self, table, row, col, cell_data):
"""在表格指定行列设置单元格数据"""
cell = QTableWidgetItem(str(cell_data))
cell.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
table.setItem(row, col, cell)
def check_data_change_for_notifications(self, table, row, col, new_data, account_username, notifications):
"""检查数据变更并准备通知"""
if row > 0 and col != 0: # 忽略第一列,通常是时间戳
old_data_str = table.item(row - 1, col).text()
old_data = convert_data(old_data_str)
new_data = convert_data(new_data)
if old_data != new_data:
count_change = new_data - old_data
self.update_cell_color(table, row, col, count_change)
# 确保传递 cell_data 和 count_change 参数
self.generate_notifications(account_username, col, new_data, count_change, notifications)
def update_cell_color(self, table, row, col, count_change):
"""更新单元格颜色"""
cell = table.item(row, col)
if count_change > 0:
cell.setForeground(QColor(Qt.GlobalColor.green))
elif count_change < 0:
cell.setForeground(QColor(Qt.GlobalColor.red))
def generate_notifications(self, account_username, col, cell_data, count_change, notifications):
# 生成通知
account = self.get_account_by_account_username(account_username)
if count_change > 0:
if col == 1: # 第1列是注册用户数量
reg_results = ','.join(
[f'`{member.name}`' for member in get_today_new_member_list(account, count_change)])
notifications.append(
('👏', account.name, '注册', count_change, f'用户: {reg_results}', str(cell_data)))
elif col == 2: # 第2列是首存用户数量
deposit_results = '\n'.join(
[f"用户: `{member.name}`, 首存金额: *{member.deposit}*" for member in
get_latest_deposit_user(account, count_change)])
notifications.append(
('🎉', account.name, '首存', count_change, deposit_results, str(cell_data)))
def addToggleButton(self, text, style):
toggleButton = QPushButton(text)
toggleButton.setCheckable(True)
toggleButton.setStyleSheet(style)
self.top_panel.addWidget(toggleButton)
def setup_middle_panel(self):
# 底部面板,包括文本框
self.bottom_panel = QVBoxLayout()
self.txt = QTextEdit()
self.bottom_panel.addWidget(self.txt)
self.main_layout.addLayout(self.bottom_panel)
def setup_bottom_panel(self):
# 中间面板,包括笔记本(标签页)
self.middle_panel = QVBoxLayout()
self.notebook = QTabWidget()
self.middle_panel.addWidget(self.notebook)
self.add_tabs(self.notebook)
self.main_layout.addLayout(self.middle_panel)
def add_tabs(self, notebook):
column_headers = ColumnHeaders.list()
for account in self.user.accounts:
tab = QWidget()
tab_layout = QVBoxLayout(tab)
notebook.addTab(tab, account.name)
table = self.create_table_for_tab(column_headers)
tab_layout.addWidget(table)
self.tables[account.username] = table
def create_table_for_tab(self, column_headers):
table = QTableWidget()
table.setColumnCount(len(column_headers))
table.setHorizontalHeaderLabels(column_headers)
table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
header = table.horizontalHeader()
header.setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
table.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding))
return table
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self.drag_position = event.globalPosition().toPoint()
self.resize_direction = self.get_resize_direction(event.pos())
if self.resize_direction:
self.is_resizing = True
elif self.is_draggable_area(event.pos()):
self.is_dragging = True
else:
return
super().mousePressEvent(event)
def is_draggable_area(self, pos):
# 将点转换为标题栏的局部坐标
title_bar_pos = self.customTitleBar.mapFromParent(pos)
# 检查点是否在标题栏内
return self.customTitleBar.rect().contains(title_bar_pos)
# 重置鼠标样式
def reset_cursor_style(self):
if not (self.is_dragging or self.is_resizing):
self.setCursor(Qt.CursorShape.ArrowCursor)
def mouseMoveEvent(self, event):
direction = self.get_resize_direction(event.pos())
# 更新鼠标样式
if direction == "left" or direction == "right":
self.setCursor(Qt.CursorShape.SizeHorCursor)
elif direction == "top" or direction == "bottom":
self.setCursor(Qt.CursorShape.SizeVerCursor)
elif direction in ["top-left", "bottom-right"]:
self.setCursor(Qt.CursorShape.SizeFDiagCursor)
elif direction in ["top-right", "bottom-left"]:
self.setCursor(Qt.CursorShape.SizeBDiagCursor)
else:
self.reset_cursor_style()
# 处理窗口拖动
if self.is_dragging:
self.move(self.pos() + (event.globalPosition().toPoint() - self.drag_position))
self.drag_position = event.globalPosition().toPoint()
# 处理窗口调整大小
elif self.is_resizing:
self.resize_window(event.globalPosition().toPoint())
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
self.is_dragging = False
self.is_resizing = False
super().mouseReleaseEvent(event)
def get_resize_direction(self, pos):
border_width = 10 # 边缘感应区的宽度
rect = self.rect()
left, right, top, bottom = rect.left(), rect.right(), rect.top(), rect.bottom()
if pos.x() < left + border_width and pos.y() < top + border_width:
return "top-left"
if pos.x() > right - border_width and pos.y() < top + border_width:
return "top-right"
if pos.x() < left + border_width and pos.y() > bottom - border_width:
return "bottom-left"
if pos.x() > right - border_width and pos.y() > bottom - border_width:
return "bottom-right"
if pos.x() < left + border_width:
return "left"
if pos.x() > right - border_width:
return "right"
if pos.y() < top + border_width:
return "top"
if pos.y() > bottom - border_width:
return "bottom"
return None
def resize_window(self, current_pos):
if not self.resize_direction:
return
delta = current_pos - self.drag_position
rect = self.geometry()
if "left" in self.resize_direction:
rect.setLeft(rect.left() + delta.x())
if "right" in self.resize_direction:
rect.setRight(rect.right() + delta.x())
if "top" in self.resize_direction:
rect.setTop(rect.top() + delta.y())
if "bottom" in self.resize_direction:
rect.setBottom(rect.bottom() + delta.y())
self.setGeometry(rect)
self.drag_position = current_pos
def closeEvent(self, event):
if self.minimum:
event.ignore()
self.hide()
else:
super().closeEvent(event)
def exit_application(self):
self.tray_icon.hide() # 隐藏托盘图标
QApplication.quit()
def tray_icon_clicked(self, reason):
if reason == QSystemTrayIcon.ActivationReason.Trigger:
if self.isMinimized() or not self.isVisible():
self.showNormal() # 恢复窗口大小
self.activateWindow() # 激活窗口
else:
self.hide() # 点击托盘图标再次隐藏窗口
def copy_to_clipboard(text):
clipboard = QApplication.clipboard()
clipboard.setText(text)
class SystemTrayIcon(QSystemTrayIcon):
def __init__(self, icon, parent=None):
super().__init__(icon, parent)
self.menu = QMenu(parent)
self.setContextMenu(self.menu)
self.create_actions(parent)
def create_actions(self, parent):
exit_action = QAction("Exit", parent)
exit_action.triggered.connect(parent.exit_application)
self.menu.addAction(exit_action)
class ColumnHeaders(Enum):
TIME = "时间"
REGISTER = "注册"
FIRST_DEPOSIT = "首存"
NET_PROFIT = "负盈利"
EFFECTIVE = "有效"
ACTIVE = "活跃"
@classmethod
def list(cls):
return [cls.TIME.value, cls.REGISTER.value, cls.FIRST_DEPOSIT.value,
cls.NET_PROFIT.value, cls.EFFECTIVE.value, cls.ACTIVE.value]