初始化提交

This commit is contained in:
zayac 2024-01-18 21:16:42 +08:00
commit 2bc94fc6b7
39 changed files with 2359 additions and 0 deletions

162
.gitignore vendored Normal file
View File

@ -0,0 +1,162 @@
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/

8
README.md Normal file
View File

@ -0,0 +1,8 @@
# tools-ui
图形化界面版本的小工具
$env:PLAYWRIGHT_BROWSERS_PATH="0"
pyinstaller -F -w -i .\src\ui\icon\icon.ico .\src\app.py --hidden-import plyer.platforms.win.notification --add-data "./src/ui/icon;ui/icon/" --add-data "./src/ui/style.qss;ui/"

13
requirements.txt Normal file
View File

@ -0,0 +1,13 @@
aiohttp==3.9.1
loguru==0.7.2
pika==1.3.2
playwright==1.40.0
pyperclip==1.8.2
PyQt6==6.6.1
PyQt6_sip==13.6.0
python_dateutil==2.8.2
QDarkStyle==3.2.3
Requests==2.31.0
schedule==1.2.1
SQLAlchemy==2.0.25
typing_extensions==4.9.0

13
src/__init__.py Normal file
View File

@ -0,0 +1,13 @@
from loguru import logger
from qtpy import QtCore
from src.core.util import resource_path
# logger.remove()
# logger.add(sys.stderr, level="INFO")
logger.add("{time:YYYY-MM}/{time:YYYY-MM-DD}.log", rotation="00:00", level="DEBUG", retention='1 day',
encoding='utf-8')
QtCore.QDir.addSearchPath('icons', resource_path('ui/icon/'))
__version__ = '0.0.1'

12
src/app.py Normal file
View File

@ -0,0 +1,12 @@
import qdarkstyle
from PyQt6.QtWidgets import QApplication
from src.ui.app import Application
if __name__ == '__main__':
app = QApplication([])
# 应用 qdarkstyle
app.setStyleSheet(qdarkstyle.load_stylesheet(qt_api='pyqt6'))
main_win = Application()
main_win.show()
app.exec()

6
src/config.ini Normal file
View File

@ -0,0 +1,6 @@
[Credentials]
username = zayac
password = 123456
[Minimum]
minimum = True

0
src/core/__init__.py Normal file
View File

93
src/core/api_request.py Normal file
View File

@ -0,0 +1,93 @@
import asyncio
import time
from typing import Any
import aiohttp
import requests
from aiohttp import ClientError
from src import logger
from src.core.constant import BOT_TOKEN, CHAT_ID
from src.core.login import login
from src.core.message_client import send_message
from src.entity.account import Account
from src.entity.api import ApiResponse
def post(url: str, headers: dict, params: dict) -> ApiResponse[Any]:
try:
logger.debug(f"url:{url}")
logger.debug(f"headers:{headers}")
logger.debug(f"params:{params}")
response = requests.post(url=url, headers=headers, json=params)
response.raise_for_status()
logger.debug(f'res:{response.json()}')
return ApiResponse(**response.json())
except requests.RequestException as e:
logger.error(f"HTTP error occurred: {e}")
raise ClientError("HTTP error occurred")
async def async_post(url: str, headers: dict, params: dict) -> ApiResponse[Any]:
try:
async with aiohttp.ClientSession() as session:
response = await session.post(url=url, headers=headers, json=params)
response.raise_for_status()
res = await response.json()
return ApiResponse(**res)
except aiohttp.ClientError as e:
logger.error(f"HTTP error occurred: {e}")
raise ClientError("HTTP error occurred")
def account_post(url: str, account: Account, params: dict) -> ApiResponse[Any]:
for _ in range(3):
try:
if account.headers is None:
account = login(account)
api_res = post(url=account.url + url, headers=account.headers, params=params)
if api_res.status_code == 6000:
return api_res
elif api_res.status_code == 6008:
logger.error(api_res.message)
else:
logger.error(api_res.message)
logger.info('Retrying login')
account = login(account)
except ClientError as e:
logger.error(f"Client error occurred: {e}")
except TimeoutError as e:
logger.error(f"")
send_message(BOT_TOKEN, CHAT_ID, f'{account.url}:加载超时,请检查是否后台更换了链接')
except Exception as e:
logger.error(f"Unknown error: {e}")
time.sleep(10)
send_message(BOT_TOKEN, CHAT_ID, f'{account.url}: Retry limit exceeded, please check the code')
logger.error(f"{account.url}: Retry limit exceeded, please check the code")
async def async_account_post(url: str, account: Account, params: dict) -> ApiResponse[Any]:
for _ in range(3):
try:
api_res = await async_post(url=account.url + url, headers=account.headers, params=params)
if api_res.status_code == 6000:
return api_res
else:
logger.error(api_res.message)
logger.info('Retrying login')
account = await asyncio.get_running_loop().run_in_executor(None, login, account)
except ClientError as e:
logger.error(f"Client error occurred: {e}")
except Exception as e:
logger.error(f"Unknown error: {e}")
# Add a delay before retrying
await asyncio.sleep(10)
send_message(BOT_TOKEN, CHAT_ID, f'{account.url}: Retry limit exceeded, please check the code')
logger.error("Retry limit exceeded, please check the code")
# You can raise a custom exception here or return an error status code

21
src/core/constant.py Normal file
View File

@ -0,0 +1,21 @@
BANNER_URL = '/agent/api/v1/front/banner'
VISUAL_LIST_URL = '/agent/api/v1/front/getAgentDataVisualList'
MEMBER_LIST_URL = '/agent/api/v1/member/list'
MEMBER_DETAIL_URL = '/agent/api/v1/member/detail'
PAY_RECORD_URL = '/agent/api/v1/payRecords/list'
PAY_RECORD_LIST_URL = '/agent/api/v1/member/payRecordList'
FINANCE_URL = '/agent/api/v1/finance/excel/total'
BOT_TOKEN = '6013830443:AAGzq1Tgtr_ZejU7bv0mab14xOwi0_64d0w'
# 工作号id
CHAT_ID = '6054562838'
# 冲群组id
GROUP_ID = '-1002122455730'
# 报数群组id
COUNT_GROUP_ID = '-4062683798'

130
src/core/login.py Normal file
View File

@ -0,0 +1,130 @@
import base64
import json
import re
import time
import requests
from playwright.sync_api import Position, TimeoutError, sync_playwright
from src import logger
from src.entity.account import Account
from src.entity.database import db
from src.ui import global_signals
def on_request(request, account: Account):
if 'banner' in request.url:
headers = request.headers
account.headers = headers
logger.info(f'Headers for account {account.name}: {headers}')
persistence(account, headers)
# 通知app数据更新了
global_signals.user_data_updated.emit()
def login(account: Account) -> Account:
logger.info(f'Starting login for account: {account.name}')
try:
with sync_playwright() as playwright:
account = perform_login(playwright, account)
except Exception as e:
logger.error(f'Error during login for account {account.name}: {e}', exc_info=True)
handle_login_failure(account)
return account
def perform_login(playwright, account: Account) -> Account:
browser = playwright.chromium.launch(headless=False)
context = browser.new_context()
page = context.new_page()
page.goto(account.url)
fill_login_form(page, account)
if handle_captcha(page):
account.headers = capture_request_headers(page, account)
logger.info('登录成功')
else:
logger.error('登录失败或验证码处理失败')
close_resources(page, context, browser)
return account
def fill_login_form(page, account: Account):
username_input = page.get_by_placeholder('用户名')
password_input = page.get_by_placeholder('密码')
username_input.click()
username_input.fill(account.username)
password_input.click()
password_input.fill(account.password)
page.locator("div").filter(has_text=re.compile(r"^登录$")).get_by_role("button").click()
logger.info(f'{account.name}登录ing...........')
def handle_captcha(page) -> bool:
try:
validate_code = page.wait_for_selector('.geetest_box', state='visible')
time.sleep(1)
return process_validate_code(validate_code)
except TimeoutError:
logger.error('超时了')
return False
def process_validate_code(validate_code):
validate_code_buffer = validate_code.screenshot()
img = base64.b64encode(validate_code_buffer).decode('utf-8')
res = base64_api(img=img)
if '|' in res:
click_captcha_positions(validate_code, res)
validate_code.query_selector('.geetest_submit').click()
validate_code.wait_for_element_state('hidden')
logger.debug('验证码点击成功')
return True
else:
logger.error(res)
return False
def click_captcha_positions(validate_code, positions_str):
for part in positions_str.split('|'):
x, y = part.split(',')
validate_code.click(position=Position(x=int(x), y=int(y)))
time.sleep(.5)
def capture_request_headers(page, account: Account):
page.on('request', lambda request: on_request(request, account))
page.wait_for_url(f'{account.url}/app/home?showWelcome=false')
return account.headers
def close_resources(page, context, browser):
page.close()
context.close()
browser.close()
def handle_login_failure(account: Account):
# 处理登录失败的情况
pass
def base64_api(uname='luffy230505', pwd='qwer12345', img='', typeid=20):
logger.info('Calling base64_api')
data = {"username": uname, "password": pwd, "typeid": typeid, "image": img,
'softid': '8d13df0efe074035b54ee9c2bef85106'}
result = json.loads(requests.post("http://api.ttshitu.com/predict", json=data).text)
if result['success']:
return result["data"]["result"]
else:
return result["message"]
def persistence(account: Account, headers: dict):
logger.info(f'Persisting headers for account {account.name}')
with db.Session() as session:
db_account = session.query(Account).filter(Account.username == account.username,
Account.password == account.password).one()
db_account.headers = headers
db_account.x_api_token = headers['x-api-token']
session.commit()
logger.info(f'Headers persisted for account {account.name}')

View File

@ -0,0 +1,34 @@
import pika
class MessageClient:
def __init__(self):
self.credentials = pika.PlainCredentials('bot', 'xiaomi123')
self.connection = pika.BlockingConnection(
pika.ConnectionParameters('164.155.224.131', 5672, '/', credentials=self.credentials))
self.channel = self.connection.channel()
self.channel.queue_declare(queue='message_queue')
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
def send_message(self, bot_token, target_id, message):
self.channel.basic_publish(exchange='', routing_key='message_queue', body=f'{bot_token}|{target_id}|{message}')
def close(self):
self.connection.close()
def send_message(bot_token, target_id, message):
with MessageClient() as client:
client.send_message(bot_token, target_id, escape_markdown(message))
def escape_markdown(text):
escape_chars = ['_', '[', ']', '(', ')', '~', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!']
for char in escape_chars:
text = text.replace(char, '\\' + char)
return text

View File

@ -0,0 +1,58 @@
import time
import pika
import requests
from loguru import logger
def _send_message_to_user(bot_token, target_id, message):
base_url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
params = {
"chat_id": target_id,
"text": message,
"parse_mode": "MarkdownV2"
}
max_retry = 3
retry_count = 0
while retry_count < max_retry:
response = requests.post(base_url, params=params)
if response.status_code == 200:
logger.debug(f'消息发送成功:{message}')
return # 如果发送成功,立即返回
else:
logger.debug('消息发送失败,重试中...')
logger.error(response.text)
time.sleep(10)
retry_count += 1
logger.debug('消息发送失败')
# 你的消息发送逻辑
class MessageServer:
def __init__(self):
self.credentials = pika.PlainCredentials('bot', 'xiaomi123')
self.connection = pika.BlockingConnection(
pika.ConnectionParameters('164.155.224.131', 5672, '/', credentials=self.credentials))
self.channel = self.connection.channel()
self.channel.queue_declare(queue='message_queue')
def start(self):
try:
def callback(ch, method, properties, body):
logger.info(body.decode())
bot_token, target_id, message = body.decode().split('|')
_send_message_to_user(bot_token, target_id, message)
self.channel.basic_consume(queue='message_queue', on_message_callback=callback, auto_ack=True)
self.channel.start_consuming()
except Exception as e:
logger.error(e)
def stop(self):
self.connection.close()
server = MessageServer()
server.start()

62
src/core/salary.py Normal file
View File

@ -0,0 +1,62 @@
from decimal import Decimal
from src.core.util import (get_first_day_by_str, get_first_day_of_last_month,
get_last_day_of_last_month)
from src.entity.database import db
from src.entity.finance import get_finance
from src.entity.user import User
def calculate_commission(profit, employee_type, target_completion):
# 定义提成点位
commission_rates = {
"1": [(0, 0.03), (100001, 0.08), (300001, 0.10), (500001, 0.11), (700001, 0.12), (1000001, 0.13)],
"2": [(0, 0.02), (100001, 0.05), (300001, 0.07), (500001, 0.08), (700001, 0.09), (1000001, 0.10)],
"3": [(0, 0.02), (100001, 0.03), (300001, 0.04), (500001, 0.05), (700001, 0.06), (1000001, 0.07)]
}
# 根据负盈利选择正确的提成点位
rates = commission_rates[employee_type]
rate = 0
for r in rates:
if profit >= r[0]:
rate = r[1]
else:
break
# 计算提成
commission = profit * target_completion * Decimal(str(rate))
return commission
def calculate_salary(employee_type, agent_profit):
if employee_type == "3":
base_salary = 30000
else:
base_salary = 12000
total_salary = 0
for profit in agent_profit:
total_salary += calculate_commission(profit, employee_type, 1)
if total_salary > 70000:
base_salary = 0
return total_salary + base_salary
def get_salary(user: User, date: str):
profits = []
start_date = get_first_day_by_str(date)
for account in user.accounts:
finance = get_finance(account, start_date, date)
print(f'{finance.name}: {finance.netProfit}')
profits.append(int(float(finance.netProfit)))
return f'方式一:{calculate_salary("1", profits)}\n方式二:{calculate_salary("2", profits)}\n方式三:{calculate_salary("3", profits)}'
def get_last_month_salary(user: User):
profits = []
for account in user.accounts:
finance = get_finance(account, get_first_day_of_last_month(), get_last_day_of_last_month())
print(f'{finance.name}: {finance.netProfit}')
profits.append(int(float(finance.netProfit)))
return f'方式一:{calculate_salary("1", profits)}\n方式二:{calculate_salary("2", profits)}\n方式三:{calculate_salary("3", profits)}'

91
src/core/util.py Normal file
View File

@ -0,0 +1,91 @@
import os
import sys
from datetime import datetime, time, timedelta
from dateutil.relativedelta import relativedelta
def get_curr_month():
now = datetime.now()
if now.time() < time(13, 0):
now -= timedelta(days=1)
return now.strftime("%Y-%m")
def get_curr_day():
return datetime.now().strftime("%Y-%m-%d")
def get_one_day_before():
return (datetime.now().date() - timedelta(days=1)).strftime("%Y-%m-%d")
def get_first_day_month():
current_date = datetime.now().date()
first_day_of_month = current_date.replace(day=1)
return first_day_of_month.strftime("%Y-%m-%d")
# 计算两个时间差值 返回值秒
def get_difference(time_str1, time: datetime) -> float:
time1 = datetime.strptime(time_str1, '%Y-%m-%d %H:%M:%S')
time_difference = time - time1
return abs(time_difference.total_seconds())
def resource_path(relative_path):
""" 获取资源的绝对路径。用于访问打包后的资源文件。 """
if hasattr(sys, '_MEIPASS'):
# 如果程序被打包,则使用临时目录
base_path = sys._MEIPASS
else:
# 如果程序未被打包,则使用当前目录
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
def get_last_day_of_last_month():
last_day_of_last_month = datetime.now().replace(day=1) - relativedelta(days=1)
return last_day_of_last_month.strftime("%Y-%m-%d")
def get_first_day_of_last_month():
last_day_of_last_month = datetime.now().replace(day=1) - relativedelta(days=1)
first_day_of_last_month = last_day_of_last_month.replace(day=1)
return first_day_of_last_month.strftime("%Y-%m-%d")
def get_first_day_by_str(date_str):
# 解析日期字符串
date = datetime.strptime(date_str, "%Y-%m-%d")
# 获取该月的第一天
first_day = date.replace(day=1)
return first_day.strftime("%Y-%m-%d")
def convert_data(data):
"""
尝试将数据转换为最合适的类型
"""
# 尝试转换为浮点数
try:
float_data = float(data)
# 如果转换后的数据和原始数据相同,则尝试转换为整数
if float_data.is_integer():
return int(float_data)
return float_data
except ValueError:
pass
# 尝试转换为日期时间
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d", "%H:%M:%S"):
try:
return datetime.strptime(data, fmt)
except ValueError:
pass
# 如果所有转换都失败,保留为字符串
return data

64
src/core/喜报.py Normal file
View File

@ -0,0 +1,64 @@
import time
from concurrent.futures.thread import ThreadPoolExecutor
from typing import List
from loguru import logger
from util import get_curr_day
from src.core.constant import BOT_TOKEN, GROUP_ID
from src.core.message_client import send_message
from src.entity.account import Account
from src.entity.banner_info import BannerInfo, get_banner_info
from src.entity.database import db
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 User
def query_banner_info(account: Account):
last_banner_info = get_banner_info(account)
while True:
try:
date = get_curr_day()
banner_info = get_banner_info(account)
logger.debug(f'{account.name}请求成功:{banner_info}')
logger.info(
f'{time.strftime("%Y-%m-%d %H:%M:%S")} {account.name}:注册:{banner_info.registerMembers},首存:{banner_info.firstDepositNum},负盈利:{banner_info.netWinLose},有效:{banner_info.effectiveNew},活跃:{banner_info.activeMembers}')
if banner_info.registerMembers > last_banner_info.registerMembers:
register_count = banner_info.registerMembers - last_banner_info.registerMembers
logger.debug(f'新注册用户数为 {register_count}')
members = get_today_new_member_list(account, register_count)
if members is not None:
names = ','.join([f'`{member.name}`' for member in members])
else:
names = 'unknown'
msg = f'👏 {account.name} 注册:{register_count} 用户: {names} 总数: {banner_info.registerMembers}'
send_message(BOT_TOKEN, GROUP_ID, msg)
logger.info(f'发送的消息: {msg}')
last_banner_info = banner_info
if banner_info.firstDepositNum > last_banner_info.firstDepositNum:
count = banner_info.firstDepositNum - last_banner_info.firstDepositNum
member_details_list = get_latest_deposit_user(account, count)
msg = '\n'.join(
[f"用户: `{member_detail.name}`, 首存金额: *{member_detail.deposit}*" for member_detail in
member_details_list])
send_message(BOT_TOKEN, GROUP_ID,
f'🎉 {account.name} 首存:{count} {msg} 总数:*{banner_info.firstDepositNum}*')
logger.info(f'发送的消息: {msg}')
last_banner_info = banner_info
time.sleep(60)
except Exception as e:
send_message(BOT_TOKEN, GROUP_ID, str(e))
logger.exception(f'发生未知错误:{e} ')
time.sleep(10)
return query_banner_info(account)
def get_banner_info_by_user(user: User) -> List[BannerInfo]:
with ThreadPoolExecutor(max_workers=len(user.accounts)) as executor:
futures = [executor.submit(get_banner_info, account) for account in user.accounts]
return [future.result() for future in futures]

44
src/core/定时任务.py Normal file
View File

@ -0,0 +1,44 @@
import time
import schedule
from loguru import logger
from 报数 import get_net_win, text_count_by_user
from 查询存款失败用户 import get_pay_failed_by_user
from src.core.constant import BOT_TOKEN, COUNT_GROUP_ID
from src.core.message_client import send_message
from src.entity.user import get_user_by_username_and_password
def job_count(username, password):
logger.info(f'Running job_count for username: {username}')
user = get_user_by_username_and_password(username, password)
send_message(BOT_TOKEN, COUNT_GROUP_ID, text_count_by_user(user))
logger.info(f'Finished job_count for username: {username}')
def query_failed_deposit(username, password):
logger.info(f'Running query_failed_deposit for username: {username}')
user = get_user_by_username_and_password(username, password)
send_message(BOT_TOKEN, COUNT_GROUP_ID, get_pay_failed_by_user(user))
logger.info(f'Finished query_failed_deposit for username: {username}')
def query_net_win(username, password) -> None:
logger.info(f'Running query_net_win for username: {username}')
user = get_user_by_username_and_password(username, password)
send_message(BOT_TOKEN, COUNT_GROUP_ID, get_net_win(user))
logger.info(f'Finished query_net_win for username: {username}')
if __name__ == '__main__':
logger.info('Starting scheduled tasks')
times = ['10:50', '14:40', '17:40', '20:40', '23:59']
for time_str in times:
schedule.every().day.at(time_str).do(job_count, 'zayac', '123456')
schedule.every().day.at(time_str).do(query_net_win, 'zayac', '123456')
schedule.every().day.at('23:59').do(query_failed_deposit, 'zayac', '123456')
while True:
schedule.run_pending()
time.sleep(1)
logger.info('Running scheduled tasks')

53
src/core/报数.py Normal file
View File

@ -0,0 +1,53 @@
from concurrent.futures import ThreadPoolExecutor
from typing import List
import pyperclip
from src import logger
from src.core import util
from src.entity.database import db
from src.entity.finance import Finance, get_finance
from src.entity.user import User
from src.entity.visual_list import VisualInfo, get_curr_data, get_visual_list
def get_statics(account, date=util.get_curr_day()) -> VisualInfo:
params = {"monthDate": util.get_curr_month()}
data = get_visual_list(account, params)
# 合并列表并创建日期到数据的映射
date_map = {item.staticsDate: item for item in data.curData + data.lastData}
# 直接通过日期获取数据
return date_map.get(date)
def count_by_user(user: User, date: str):
accounts = user.accounts
with ThreadPoolExecutor(max_workers=len(accounts)) as t:
futures = [t.submit(get_statics, account, date) for account in accounts]
return [future.result() for future in futures]
def text_count_by_user(user: User, date: str) -> str:
visual_list = count_by_user(user, date)
text = '\n\n'.join(
f'{result.agentName}\n注册:{result.isNew}\n首存:{result.firstCount}\n日活:{int(result.countBets)}\n流水:{int(result.bets)}'
for result in visual_list
)
logger.info(f'Generated text: {text}')
return text
def get_finances_by_user(user: User, date) -> List[Finance]:
accounts = user.accounts
start_date = util.get_first_day_by_str(date)
with ThreadPoolExecutor(max_workers=len(accounts)) as t:
futures = [t.submit(get_finance, account, start_date, date) for account in accounts]
return [future.result() for future in futures]
def get_net_win(user: User, date: str) -> str:
finances = get_finances_by_user(user, date)
finance_strings = [f"{finance.name}: {finance.netProfit}" for finance in finances]
logger.info(f'Finance strings: {finance_strings}')
return '\n'.join(finance_strings)

View File

@ -0,0 +1,82 @@
from concurrent.futures import ThreadPoolExecutor
from typing import Dict, List, Optional
from loguru import logger
from src.core import api_request
from src.core.constant import PAY_RECORD_LIST_URL
from src.core.util import get_first_day_month
from src.entity.account import Account
from src.entity.member import MemberList, get_member_list
from src.entity.user import User
def get_pay_record_list(account: Account, date: str) -> Dict[str, List[str]]:
logger.info(f'Getting pay record list for account: {account.name} and date: {date}')
_names = {'name': account.name, 'names': []}
params = {
"pageNum": 1,
"pageSize": 100,
"registerSort": 1,
"drawSort": -1,
"depositSort": -1,
"lastLoginTimeSort": -1,
"name": "",
"minPay": None,
"maxPay": None,
"startDate": get_first_day_month(),
"registerStartDate": date,
"endDate": date,
"registerEndDate": date,
"firstPayStartTime": "",
"firstPayEndTime": "",
"isBet": "0",
"tagsFlag": "1"
}
member_list = get_member_list(account, params)
if member_list is not None and len(member_list) > 0:
with ThreadPoolExecutor(max_workers=len(member_list)) as executor:
futures = [executor.submit(get_pay_record, account, member, date) for member in member_list]
for future in futures:
result = future.result()
if result:
_names['names'].append(result)
logger.info(f'Finished getting pay record list for account: {account.name} and date: {date}')
return _names
def get_pay_record(account: Account, member: MemberList, date: str) -> Optional[str]:
logger.info(f'Getting pay record for account: {account.name}, member: {member.name}, and date: {date}')
params = {
"pageNum": 1,
"pageSize": 15,
"id": member.id,
"startDate": get_first_day_month(),
"endDate": date
}
res = api_request.account_post(PAY_RECORD_LIST_URL, account=account, params=params)
if int(res.data['orderAmountTotal']) > 0 and int(res.data['scoreAmountTotal']) == 0:
return member.name
return ""
def get_pay_failed_by_user(user: User, date: str) -> Optional[str]:
logger.info(f'Getting pay failed by user: {user.username}')
with ThreadPoolExecutor(max_workers=len(user.accounts)) as executor:
futures = [executor.submit(get_pay_record_list, account, date) for account in user.accounts]
# 使用列表推导式构建结果字符串
text_lines = [
"{}\n{}".format(res['name'], '\n'.join(res['names']))
for future in futures if (res := future.result())['names']
]
text = '\n'.join(text_lines)
if not text:
logger.info('无存款失败用户')
return '无存款失败用户'
logger.info(text)
return text

3
src/entity/__init__.py Normal file
View File

@ -0,0 +1,3 @@
from src import logger
from . import account, user

34
src/entity/account.py Normal file
View File

@ -0,0 +1,34 @@
import json
from dataclasses import dataclass
from enum import Enum
from sqlalchemy import JSON as Sql_JSON
from sqlalchemy import Enum as Sql_Enum
from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from src.entity.database import db
# 账号类型枚举
class AccountType(Enum):
ky = 0
hth = 1
@dataclass
class Account(db.Base):
__tablename__ = 'ky_account'
id: Mapped[db.int_pk]
username: Mapped[db.str_required_unique] = mapped_column(comment='账号')
password: Mapped[db.str_required] = mapped_column(comment='密码')
type: Mapped[AccountType] = mapped_column(Sql_Enum(AccountType), default=AccountType.ky, nullable=False,
comment='类型 ky hth')
name: Mapped[str] = mapped_column(String(64), nullable=True, comment='别名')
url: Mapped[str] = mapped_column(String(128), nullable=False, comment='url')
x_api_token: Mapped[str] = mapped_column(String(64), nullable=True, comment='x-api-token')
headers: Mapped[json] = mapped_column(Sql_JSON, nullable=True, comment='headers')
user_id: Mapped[int] = mapped_column(ForeignKey('ky_user.id'),
nullable=False, comment='关联的用户id')
user: Mapped['user.User'] = relationship('user.User', back_populates='accounts')

12
src/entity/api.py Normal file
View File

@ -0,0 +1,12 @@
from dataclasses import dataclass
from typing import Generic, TypeVar
T = TypeVar('T')
@dataclass
class ApiResponse(Generic[T]):
data: T
message: str
status_code: int

32
src/entity/banner_info.py Normal file
View File

@ -0,0 +1,32 @@
from dataclasses import dataclass
from src.core.api_request import account_post
from src.core.constant import BANNER_URL
from src.entity.account import Account
@dataclass
class BannerInfo:
registerMembers: int
firstDepositNum: int
netWinLose: float
totalMembers: int
activeMembers: int
commissionLevel: float
depositIncrease: float
effectiveNew: int
profit: float
registerIncrease: float
agentCode: str
def __init__(self, **kwargs):
for key, value in kwargs.items():
if value is not None:
setattr(self, key, value)
def get_banner_info(account: Account) -> BannerInfo:
api_response = account_post(url=BANNER_URL, account=account, params={})
banner_info = BannerInfo(**api_response.data)
banner_info.agentCode = account.username
return banner_info

62
src/entity/database.py Normal file
View File

@ -0,0 +1,62 @@
"""
初始化数据的脚本
"""
import sqlalchemy as sa
from sqlalchemy import Integer, String
from sqlalchemy.orm import declarative_base, mapped_column, sessionmaker
from typing_extensions import Annotated
class Database:
int_pk = Annotated[int, mapped_column(Integer, primary_key=True)]
str_required_unique = Annotated[str, mapped_column(String(64), unique=True, nullable=False)]
str_required = Annotated[str, mapped_column(String(64), nullable=False)]
def __init__(self, db_url):
self.engine = sa.create_engine(db_url, echo=False, future=True, pool_size=10, pool_recycle=3600)
self.Session = sessionmaker(bind=self.engine)
self.Base = declarative_base()
def initialize_data(self):
from src.entity.account import Account, AccountType
from src.entity.user import User
session = self.Session()
user = User(username='zayac', password='123456', name='蓝胖', email='stupidzayac@gmail')
account = [Account(username='ky3tg107032', password='tg666888', type=AccountType.ky,
headers={'sec-ch-ua': '"Not_A Brand";v="8", "Chromium";v="120"',
'x-api-token': 'DL_21f490b93c4fc4db0faf5d0458b6bee2',
'x-api-xxx': '7306dfacdddf5be53c9be3588d9669b9a0d65d0e2fc51e4dd61aa551b5eead68',
'x-api-version': '1.0.0', 'sec-ch-ua-mobile': '?0',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'content-type': 'application/json',
'x-api-uuid': '22958E8F-C36B-4C81-AF3F-02A7E6A4DCF2',
'referer': 'https://www.arv5e7.com:6001/app/home?showWelcome=false',
'x-api-client': 'agent_web',
'sec-ch-ua-platform': '"Windows"'},
url='https://www.arv5e7.com:6001',
name='ky32线',
x_api_token='DL_21f490b93c4fc4db0faf5d0458b6bee2',
user=user),
Account(username='htg51120', password='tg666888', type=AccountType.hth,
headers={'sec-ch-ua': '"Not_A Brand";v="8", "Chromium";v="120"',
'x-api-token': 'DL_f55c9588cf34b7e81413bc23f0c9d114',
'x-api-xxx': '40ef612c97c7673f32c7f1367ed5349519350adc2638f2245a53d39527e50f2c',
'x-api-version': '1.0.0', 'sec-ch-ua-mobile': '?0',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'content-type': 'application/json',
'x-api-uuid': '2140DC1C-4343-4AF2-9B20-814457DBC3EC',
'referer': 'https://www.oy9eux.com:9514/app/home?showWelcome=false',
'x-api-client': 'agent_web', 'sec-ch-ua-platform': '"Windows"'},
url='https://www.oy9eux.com:9514',
name='hth20线',
x_api_token='DL_f55c9588cf34b7e81413bc23f0c9d114',
user=user
)]
self.Base.metadata.create_all(self.engine)
session.add(user)
session.add_all(account)
session.commit()
db = Database('mysql+mysqlconnector://ky_tools:hMXWFxRstbkaCDDr@164.155.224.131:13306/ky_tools')

47
src/entity/finance.py Normal file
View File

@ -0,0 +1,47 @@
from dataclasses import dataclass
from decimal import Decimal
from src.core import util
from src.core.api_request import account_post
from src.core.constant import FINANCE_URL
from src.entity.account import Account
'''
财务报表
'''
@dataclass
class Finance(object):
profit: Decimal
promo: Decimal
thirdPartySpend: Decimal
netProfit: Decimal
deposit: Decimal
draw: Decimal
rebate: Decimal
adjust: Decimal
netAmount: Decimal
betAmount: Decimal
handlingFee: Decimal
partnershipProfit: Decimal
name: str
def __init__(self, **kwargs):
for key, value in kwargs.items():
if value is not None:
setattr(self, key, value)
def get_finance(account: Account, start_date=util.get_first_day_month(), end_date=util.get_curr_day()) -> Finance:
"""
:rtype: Finance
"""
api_response = account_post(url=FINANCE_URL, account=account, params={
"startDate": start_date,
"endDate": end_date,
"topId": 0
})
finance = Finance(**api_response.data)
finance.name = account.name
return finance

150
src/entity/member.py Normal file
View File

@ -0,0 +1,150 @@
from dataclasses import dataclass
from datetime import datetime
from typing import List
from src.core import api_request
from src.core.constant import MEMBER_DETAIL_URL, MEMBER_LIST_URL
from src.core.util import get_curr_day, get_first_day_month
from src.entity.account import Account
@dataclass
class BaseMember:
id: int
name: str
realName: str
registerDate: str
deposit: str
draw: str
profit: str
lastLoginTime: str
active: int
promo: str
rebate: str
riskAdjust: str
netAmount: str
betAmount: str
changeLog: str
venueProfitList: list
isChange: int
vipGrade: int
vipGradeStr: str
firstPayAt: str
remark: str
@dataclass
class MemberList(BaseMember):
venueNetAmountList: str
tagsIds: str
tagsInfo: str
@dataclass
class MemberDetail(BaseMember):
venueNetAmountList: list
tags: str
def get_first_pay_datetime(self) -> datetime:
return datetime.strptime(self.firstPayAt, "%Y-%m-%d %H:%M:%S")
def get_member_list(account: Account, params: dict) -> List[MemberList]:
api_response = api_request.account_post(MEMBER_LIST_URL, account=account, params=params)
data_list = api_response.data.get('list', [])
return [MemberList(**item) for item in data_list] if data_list else []
def get_today_new_member_list(account: Account, count: int) -> List[MemberList]:
date = get_curr_day()
params = {
"pageNum": 1,
"pageSize": count,
"registerSort": 1,
"drawSort": -1,
"depositSort": -1,
"lastLoginTimeSort": -1,
"name": "",
"minPay": None,
"maxPay": None,
"startDate": date,
"registerStartDate": date,
"endDate": date,
"registerEndDate": date,
"firstPayStartTime": "",
"firstPayEndTime": "",
"isBet": "",
"tagsFlag": "1"
}
members = get_member_list(account=account, params=params)
return members
async def async_get_member_list(account: Account, params: dict) -> List[MemberList]:
api_res = await api_request.async_account_post(MEMBER_LIST_URL, account=account, params=params)
data_list = api_res.data.get('list', [])
return [MemberList(**item) for item in data_list] if data_list else []
# 根据用户名查询用户详情
def get_member_by_name(account: Account, name: str) -> MemberDetail:
params = {
"pageNum": 1,
"pageSize": 1,
"registerSort": 1,
"drawSort": -1,
"depositSort": -1,
"lastLoginTimeSort": -1,
"name": name,
"minPay": None,
"maxPay": None,
"startDate": get_first_day_month(),
"registerStartDate": '',
"endDate": get_curr_day(),
"registerEndDate": '',
"firstPayStartTime": "",
"firstPayEndTime": "",
"isBet": "",
"tagsFlag": "1"
}
member = get_member_list(account, params)[0]
params = {
'startDate': get_first_day_month(),
'endDate': get_curr_day(),
'id': member.id
}
api_response = api_request.account_post(url=MEMBER_DETAIL_URL, account=account, params=params)
return MemberDetail(**api_response.data)
async def async_get_member_detail_by_name(account: Account, name: str) -> MemberDetail:
params = {
"pageNum": 1,
"pageSize": 1,
"registerSort": 1,
"drawSort": -1,
"depositSort": -1,
"lastLoginTimeSort": -1,
"name": name,
"minPay": None,
"maxPay": None,
"startDate": get_first_day_month(),
"registerStartDate": '',
"endDate": get_curr_day(),
"registerEndDate": '',
"firstPayStartTime": "",
"firstPayEndTime": "",
"isBet": "",
"tagsFlag": "1"
}
member_list = await async_get_member_list(account, params)
member = member_list[0]
params = {
'startDate': get_first_day_month(),
'endDate': get_curr_day(),
'id': member.id
}
api_response = await api_request.async_account_post(url=MEMBER_DETAIL_URL, account=account,
params=params)
return MemberDetail(**api_response.data)

135
src/entity/pay_record.py Normal file
View File

@ -0,0 +1,135 @@
import asyncio
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from datetime import datetime
from src import logger
from src.core import api_request, util
from src.core.constant import PAY_RECORD_URL
from src.core.util import get_curr_day
from src.entity.account import Account
from src.entity.member import (async_get_member_detail_by_name,
get_member_by_name)
@dataclass
class PayRecord(object):
billNo: str
name: str
agentName: str
orderAmount: str
rebateAmount: str
flowRatio: str
payType: int
payName: str
recipientAccount: str
createdAt: str
payStatus: int
payStatusName: str
whetherGetCard: str
topId: int
scoreAmount: str
# 根据用户查询最新存款信息
def get_pay_record_list(account: Account):
logger.info(f'Getting pay record list for account: {account.name}')
# 获取当前成功存款的用户
params = {
"memberName": "",
"payState": 2,
"isRest": False,
"pageNum": 1,
"pageSize": 100,
"startDate": get_curr_day(),
"endDate": get_curr_day()
}
api_response = api_request.account_post(PAY_RECORD_URL, account, params)
logger.info(f'Finished getting pay record list for account: {account.name}')
return [PayRecord(**item) for item in api_response.data['list']]
def get_latest_deposit_user(account: Account, count: int):
logger.info(f'Getting latest deposit user for account: {account.name} and count: {count}')
pay_record_list = get_pay_record_list(account)
# 提取所有用户名
names = []
seen = set()
now = datetime.now()
for item in pay_record_list:
name = item.name
if name not in seen and util.get_difference(item.createdAt, now) < 7200:
names.append(name)
seen.add(name)
logger.debug(f"获取到{len(names)}个成功存款人数")
# 开启多线程 根据用户名查询所有数据
with ThreadPoolExecutor(max_workers=len(names)) as executor:
futures = [executor.submit(get_member_by_name, account, name) for name in names]
try:
results = [future.result() for future in futures]
except Exception as e:
logger.debug(f'查询失败:{e}')
sorted_members = sorted(results, key=lambda member_detail: member_detail.get_first_pay_datetime(), reverse=True)
# 截取前n个
details = sorted_members[:count]
record_dict = {record.createdAt: record.scoreAmount for record in pay_record_list}
member_details = []
for detail in details:
if detail.firstPayAt in record_dict:
detail.deposit = record_dict[detail.firstPayAt]
member_details.append(detail)
logger.info(f'Finished getting latest deposit user for account: {account.name} and count: {count}')
return member_details
# 根据用户查询最新存款信息
async def async_get_pay_record_list(account: Account):
logger.info(f'Async getting pay record list for account: {account.name}')
today = get_curr_day()
params = {
"memberName": "",
"payState": 2,
"isRest": False,
"pageNum": 1,
"pageSize": 100,
"startDate": today,
"endDate": today
}
res = await api_request.async_account_post(PAY_RECORD_URL, account, params)
logger.info(f'Finished async getting pay record list for account: {account.name}')
return [PayRecord(**item) for item in res.data['list']]
# 获取最新存款用户
async def async_get_latest_deposit_user(account: Account, count: int):
logger.info(f'Async getting latest deposit user for account: {account.name} and count: {count}')
pay_record_list = await async_get_pay_record_list(account)
# 提取所有用户名
names = []
seen = set()
now = datetime.now()
for item in pay_record_list:
name = item.name
# 存款订单有效期一般两个小时左右,所以默认只查询两个小时以内的订单即可
if name not in seen and util.get_difference(item.createdAt, now) < 7200:
names.append(name)
seen.add(name)
logger.debug(f"获取到{len(names)}个成功存款人数")
tasks = []
for name in names:
task = asyncio.create_task(async_get_member_detail_by_name(account, name))
tasks.append(task)
results = await asyncio.gather(*tasks)
# logger.info(f'查询成功:{results}')
sorted_members = sorted(results, key=lambda member_detail: member_detail.get_first_pay_datetime(), reverse=True)
# logger.debug(f'首存金额:{[member_detail.deposit for member_detail in sorted_members]}')
# 因为会员列表的存款是统计用户当月的所有金额 所以有延迟 所以需要将订单中的金额赋值给存款金额 然后再返回
details = sorted_members[:count]
record_dict = {record.createdAt: record.scoreAmount for record in pay_record_list}
for detail in details:
if detail.firstPayAt in record_dict:
detail.deposit = record_dict[detail.firstPayAt]
logger.info(f'Finished async getting latest deposit user for account: {account.name} and count: {count}')
return details

34
src/entity/user.py Normal file
View File

@ -0,0 +1,34 @@
from dataclasses import dataclass
from typing import List
from sqlalchemy import String
from sqlalchemy.dialects.mssql import TINYINT
from sqlalchemy.orm import Mapped, mapped_column, relationship
from src.entity import account
from src.entity.database import db
# 用户
@dataclass
class User(db.Base):
__tablename__ = 'ky_user'
id: Mapped[db.int_pk] = mapped_column(comment="唯一键")
username: Mapped[db.str_required_unique] = mapped_column(comment="账号")
password: Mapped[db.str_required] = mapped_column(comment="密码")
name: Mapped[str] = mapped_column(String(32), nullable=True, comment="别名")
email: Mapped[str] = mapped_column(String(32), nullable=False, comment="邮箱")
bot_token: Mapped[str] = mapped_column(String(64), nullable=True, comment="飞机机器人id")
group_id: Mapped[str] = mapped_column(String(32), nullable=True, comment="消息群组id")
chat_id: Mapped[str] = mapped_column(String(32), nullable=True, comment="消息个人id")
telegram_ids: Mapped[str] = mapped_column(String(128), nullable=True, comment="telegram_ids")
status: Mapped[int] = mapped_column(TINYINT, nullable=False, comment='账号状态,0禁用1启用', default=0)
accounts: Mapped[List['account.Account']] = relationship('account.Account', back_populates='user', lazy=False)
def get_user_by_username_and_password(username: str, password: str) -> User:
with db.Session() as session:
user = session.query(User).filter(
User.username == username and User.password == password and User.status == 1).one()
return user

85
src/entity/visual_list.py Normal file
View File

@ -0,0 +1,85 @@
import json
from dataclasses import dataclass
from src.core import api_request
from src.core.constant import VISUAL_LIST_URL
from src.entity.account import Account
# 视图列表对象 对应界面上的图表
@dataclass
class VisualInfo:
staticsDate: str
agentId: int
agentType: int
agentCode: str
agentName: str
isNew: int
firstDeposit: float
deposit: float
depositPromo: float
draw: int
promo: float
promoDividend: float
rebate: float
adjust: float
riskAdjust: float
bets: float
profit: float
allBets: float
firstCount: int
countDeposit: int
countDraw: int
countBets: int
createdAt: str
updatedAt: str
oldDeposit: float
oldDepositCount: int
newDeposit: float
@staticmethod
def from_dict(data):
return VisualInfo(**data)
@dataclass
class VisualList:
curData: list[VisualInfo]
lastData: list[VisualInfo]
@staticmethod
def from_json(json_str):
data = json.loads(json_str)
cur_data = [VisualInfo.from_dict(item) for item in data['curData']]
last_data = [VisualInfo.from_dict(item) for item in data['lastData']]
return VisualList(curData=cur_data, lastData=last_data)
@staticmethod
def dict_to_visual_list(data):
cur_data = [VisualInfo(**item) for item in data['curData']]
last_data = [VisualInfo(**item) for item in data['lastData']]
return VisualList(curData=cur_data, lastData=last_data)
def get_visual_list(account: Account, params: dict) -> VisualList:
res = api_request.account_post(url=VISUAL_LIST_URL, account=account,
params=params)
return VisualList.dict_to_visual_list(res.data)
def get_curr_data(account: Account, params: dict) -> list[VisualInfo]:
res = api_request.account_post(url=VISUAL_LIST_URL, account=account,
params=params)
return [VisualInfo(**item) for item in res.data['curData']]
async def async_get_visual_list(account: Account, params: dict) -> VisualList:
res = await api_request.async_account_post(url=VISUAL_LIST_URL, account=account,
params=params)
return VisualList(**res.data)
async def async_get_curr_data(account: Account, params: dict) -> list[VisualInfo]:
res = await api_request.async_account_post(url=VISUAL_LIST_URL, account=account,
params=params)
return [VisualInfo(**item) for item in res.data['curData']]

8
src/ui/__init__.py Normal file
View File

@ -0,0 +1,8 @@
from PyQt6.QtCore import QObject, pyqtSignal
class GlobalSignals(QObject):
user_data_updated = pyqtSignal()
# 实例化全局信号
global_signals = GlobalSignals()

557
src/ui/app.py Normal file
View File

@ -0,0 +1,557 @@
import configparser
import os
from loguru import logger
from PyQt6.QtCore import QDate, QDateTime, Qt, QThreadPool, QTime, QTimer
from PyQt6.QtGui import QColor, QIcon, QAction
from PyQt6.QtWidgets import (QApplication, QCheckBox, QDateEdit, QHBoxLayout,
QHeaderView, QMainWindow, QPushButton,
QSizePolicy, QTableWidget, QTableWidgetItem,
QTabWidget, QTextEdit, QVBoxLayout, QWidget, QMessageBox, QSystemTrayIcon, QMenu)
from src import resource_path
from src.core.message_client import send_message
from src.core.util import convert_data
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.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. 设置线程池
self.thread_pool = QThreadPool()
# 3. 设置 UI
self.setup_ui()
# 4. 初始化系统托盘图标
self.init_tray_icon()
# 5. 初始化表格数据
self.init_table_data()
# 6. 设置日期更新和报告定时器
self.setup_date_update_timer()
self.setup_report_timer()
# 7. 全局信号处理,更新用户信息
global_signals.user_data_updated.connect(self.refresh_user_data)
# 8. 消息通知对象
self.chat_id = self.user.group_id if self.user.group_id else self.user.chat_id
def setup_ui(self):
self.apply_stylesheet()
self.set_window_properties()
self.create_central_widget()
self.setup_layouts()
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 = QSystemTrayIcon(QIcon("icons:icon.png"), self)
tray_menu = QMenu()
exit_action = QAction("退出", self)
exit_action.triggered.connect(self.exit_application)
tray_menu.addAction(exit_action)
self.tray_icon.setContextMenu(tray_menu)
self.tray_icon.activated.connect(self.tray_icon_clicked)
self.tray_icon.show()
def init_data_and_timers(self):
# 初始化数据
self.init_table_data()
# 设置定时器
self.setup_date_update_timer()
self.setup_report_timer()
def load_config(self):
config = configparser.ConfigParser()
config_file = 'config.ini'
if not os.path.exists(config_file):
QMessageBox.warning(None, "警告", "用户信息获取失败!")
return None, None
config.read(config_file)
username = config.get('Credentials', 'username')
password = config.get('Credentials', 'password')
minimum = config.get('Minimum', 'minimum')
self.username = username
self.password = password
self.user = get_user_by_username_and_password(username, password)
self.minimum = minimum
def init_table_data(self):
# 初始化表格数据
# 例如,加载账户数据并更新表格
for account in self.user.accounts:
data = self.query_initial_data(account)
self.update_table(account.username, data)
def query_initial_data(self, account):
# 实际实现应该根据您的业务逻辑来定义
return ReportTask.query_data_for_account(account)
def setup_report_timer(self):
self.report_timer = QTimer(self)
self.report_timer.timeout.connect(self.update_reports)
self.report_timer.start(60000) # 每60秒触发一次
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))
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())
# 更新日期范围
self.update_date_range()
print(self.dateEdit.date())
# 设置定时器每24小时触发一次
self.date_update_timer.start(86400000)
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)
self.main_layout.addLayout(self.top_panel)
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.chat_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):
cell = self.create_table_cell(cell_data, table, col)
# 注意这里我们使用 row_count 而不是 0
table.setItem(row_count, col, cell)
self.handle_data_change(table, cell_data, col, account_username, notifications)
self.send_all_notifications(notifications)
def create_table_cell(self, cell_data, table, col):
cell = QTableWidgetItem(str(cell_data))
cell.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
return cell
def handle_data_change(self, table, cell_data, col, account_username, notifications):
if table.rowCount() > 1 and col != 0:
old_data_str = table.item(1, col).text()
old_data = convert_data(old_data_str)
new_data = convert_data(cell_data)
if old_data != new_data:
count_change = new_data - old_data
self.update_cell_color(table, col, count_change)
self.generate_notifications(account_username, col, cell_data, count_change, notifications)
def update_cell_color(self, table, col, count_change):
# 更新单元格颜色
cell = table.item(0, 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 send_all_notifications(self, notifications):
for notification in notifications:
self.send_notification(*notification)
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 = ["时间", "注册", "首存", "负盈利", "有效", "活跃"]
for account in self.user.accounts:
# 创建 Tab 和布局
tab = QWidget()
tab_layout = QVBoxLayout(tab)
notebook.addTab(tab, account.name)
# 创建表格并设置列数和列标题
table = QTableWidget()
table.setColumnCount(len(column_headers))
table.setHorizontalHeaderLabels(column_headers)
# 禁用表格的编辑功能
table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
# 设置列宽
header = table.horizontalHeader()
# 设置所有列为自适应宽度
for i in range(len(column_headers)):
header.setSectionResizeMode(i, QHeaderView.ResizeMode.Stretch)
header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
# 将表格的大小调整策略设置为填充整个 Tab
table.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding))
# 将表格添加到布局中
tab_layout.addWidget(table)
# 保存表格引用以便稍后更新
self.tables[account.username] = 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.isVisible():
self.hide()
else:
self.showNormal()
def copy_to_clipboard(text):
clipboard = QApplication.clipboard()
clipboard.setText(text)

62
src/ui/data_query.py Normal file
View File

@ -0,0 +1,62 @@
import time
from PyQt6.QtCore import QObject, QRunnable, QThread, pyqtSignal
from src.core.salary import get_salary
from src.core.报数 import get_net_win, text_count_by_user
from src.core.查询存款失败用户 import get_pay_failed_by_user
from src.entity.banner_info import get_banner_info
class TaskSignals(QObject):
query_completed = pyqtSignal(str, bool, bool)
table_updated = pyqtSignal(str, list)
class ReportTask(QRunnable):
def __init__(self, account):
super().__init__()
self.account = account
self.signals = TaskSignals()
def run(self):
data = self.query_data_for_account(self.account)
self.signals.table_updated.emit(self.account.username, data)
@staticmethod
def query_data_for_account(account):
time_str = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())
banner_info = get_banner_info(account)
# 返回数据列表
return [time_str, banner_info.registerMembers, banner_info.firstDepositNum, banner_info.netWinLose,
banner_info.effectiveNew, banner_info.activeMembers]
class ButtonTask(QRunnable):
def __init__(self, query_type, selected_date_str, user):
super().__init__()
self.query_type = query_type
self.selected_date_str = selected_date_str
self.user = user
self.signals = TaskSignals()
def run(self):
result = ''
auto_clipboard = False
notify = False
if self.query_type == '报数':
result = text_count_by_user(self.user, self.selected_date_str)
auto_clipboard = True
notify = True
elif self.query_type == '存款失败用户':
result = get_pay_failed_by_user(self.user, self.selected_date_str)
if '' not in result:
auto_clipboard = True
notify = True
elif self.query_type == '负盈利':
result = get_net_win(self.user, self.selected_date_str)
elif self.query_type == '薪资':
result = get_salary(self.user, self.selected_date_str)
# 数据查询完成,发出信号
self.signals.query_completed.emit(result, auto_clipboard, notify)

1
src/ui/icon/close.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1705040984076" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6674" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M568.277333 512l283.008-327.637333c4.736-5.418667 0.725333-13.696-6.570666-13.696H758.613333a17.792 17.792 0 0 0-13.226666 5.973333L512 446.890667 278.613333 176.64A17.450667 17.450667 0 0 0 265.344 170.666667H179.285333c-7.296 0-11.306667 8.277333-6.570666 13.696L455.722667 512l-283.008 327.594667c-4.736 5.461333-0.725333 13.738667 6.570666 13.738666H265.386667a17.792 17.792 0 0 0 13.226666-5.973333L512 577.109333l233.386667 270.250667c3.242667 3.754667 8.106667 5.973333 13.269333 5.973333h86.058667c7.338667 0 11.306667-8.277333 6.570666-13.738666L568.277333 512z" fill="#dfe1e2" p-id="6675"></path></svg>

After

Width:  |  Height:  |  Size: 945 B

BIN
src/ui/icon/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
src/ui/icon/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 546 KiB

1
src/ui/icon/max.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1705041340268" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7442" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M358.058667 128H156.970667A28.970667 28.970667 0 0 0 128 157.013333v202.837334c0 7.978667 6.528 14.506667 14.506667 14.506666h43.434666a14.506667 14.506667 0 0 0 14.506667-14.506666V200.448h157.610667a14.506667 14.506667 0 0 0 14.506666-14.506667V142.506667a14.506667 14.506667 0 0 0-14.506666-14.506667zM881.493333 649.642667h-43.434666a14.506667 14.506667 0 0 0-14.506667 14.506666v159.402667h-157.610667a14.506667 14.506667 0 0 0-14.506666 14.506667v43.434666c0 7.978667 6.570667 14.506667 14.506666 14.506667h201.088c16 0 28.970667-12.928 28.970667-29.013333v-202.837334a14.506667 14.506667 0 0 0-14.506667-14.506666zM358.058667 823.552H200.448v-159.402667a14.506667 14.506667 0 0 0-14.506667-14.506666H142.506667a14.506667 14.506667 0 0 0-14.506667 14.506666v202.88c0 16 12.970667 28.970667 29.013333 28.970667h201.045334a14.506667 14.506667 0 0 0 14.506666-14.506667v-43.434666a14.506667 14.506667 0 0 0-14.506666-14.506667zM866.986667 128h-201.088a14.506667 14.506667 0 0 0-14.506667 14.506667v43.434666c0 7.978667 6.570667 14.506667 14.506667 14.506667h157.610666v159.402667c0 7.978667 6.528 14.506667 14.506667 14.506666h43.434667a14.506667 14.506667 0 0 0 14.506666-14.506666V156.970667A28.928 28.928 0 0 0 866.986667 128z" fill="#dfe1e2" p-id="7443"></path></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

1
src/ui/icon/min.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1705041096742" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7186" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M851.2 214.186667l-41.386667-41.386667a7.381333 7.381333 0 0 0-10.368 0L654.933333 317.397333l-50.176-50.176a7.253333 7.253333 0 0 0-12.373333 4.266667l-21.589333 173.525333a7.338667 7.338667 0 0 0 8.192 8.149334l173.568-21.546667c6.058667-0.725333 8.533333-8.106667 4.309333-12.373333L706.688 369.066667l144.597333-144.64a7.338667 7.338667 0 0 0-0.085333-10.24z m-406.186667 356.608l-173.568 21.589333a7.338667 7.338667 0 0 0-4.309333 12.373333l50.176 50.176-144.512 144.512a7.381333 7.381333 0 0 0 0 10.368l41.386667 41.386667a7.381333 7.381333 0 0 0 10.368 0l144.597333-144.64 50.176 50.218667a7.253333 7.253333 0 0 0 12.373333-4.309334l21.461334-173.482666a7.253333 7.253333 0 0 0-8.106667-8.192z" fill="#dfe1e2" p-id="7187"></path></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

1
src/ui/icon/top.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1705040893836" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6460" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M927.701333 375.978667l-279.637333-279.637334A37.546667 37.546667 0 0 0 621.397333 85.333333a37.546667 37.546667 0 0 0-26.666666 11.008L411.904 279.296a366.506667 366.506667 0 0 0-41.770667-2.304 374.016 374.016 0 0 0-234.325333 82.048 37.717333 37.717333 0 0 0-3.072 56.064l206.208 206.208-244.48 244.224a17.92 17.92 0 0 0-5.205333 11.093333l-3.84 42.24a18.090667 18.090667 0 0 0 18.048 19.754667c0.554667 0 1.109333 0 1.706666-0.128l42.24-3.84a17.92 17.92 0 0 0 11.093334-5.248l244.437333-244.437333 206.208 206.208a37.546667 37.546667 0 0 0 26.666667 11.008 37.546667 37.546667 0 0 0 29.397333-14.08 374.826667 374.826667 0 0 0 79.658667-276.224l182.826666-182.826667a37.589333 37.589333 0 0 0 0-53.077333z m-240.725333 178.346666l-27.776 27.818667 4.266667 39.04a294.997333 294.997333 0 0 1-34.474667 174.677333L228.309333 394.922667a293.546667 293.546667 0 0 1 174.634667-34.517334l39.04 4.309334 27.818667-27.776 151.722666-151.722667 217.301334 217.301333-151.850667 151.850667z" fill="#dfe1e2" p-id="6461"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

94
src/ui/style.qss Normal file
View File

@ -0,0 +1,94 @@
QPushButton {
border: 2px solid transparent;
border-radius: 4px;
padding: 6px 12px;
font-size: 14px;
color: white;
transition: background-color 0.3s, border-color 0.3s;
}
/* 采用更柔和的颜色 */
/* Primary 风格的按钮 */
QPushButton#Primary {
background-color: #0056b3; /* 暗淡蓝色 */
}
QPushButton#Primary:hover {
background-color: #0044a3;
}
/* Secondary 风格的按钮 */
QPushButton#Secondary {
background-color: #383d41; /* 暗灰色 */
}
QPushButton#Secondary:hover {
background-color: #303438;
}
/* Success 风格的按钮 */
QPushButton#Success {
background-color: #155724; /* 暗绿色 */
}
QPushButton#Success:hover {
background-color: #0f4b1e;
}
/* Info 风格的按钮 */
QPushButton#Info {
background-color: #0c5460; /* 暗青色 */
}
QPushButton#Info:hover {
background-color: #0a4a56;
}
/* Warning 风格的按钮 */
QPushButton#Warning {
background-color: #856404; /* 暗黄色 */
color: black;
}
QPushButton#Warning:hover {
background-color: #755c03;
}
/* Danger 风格的按钮 */
QPushButton#Danger {
background-color: #721c24; /* 暗红色 */
}
QPushButton#Danger:hover {
background-color: #621b21;
}
/* Light 风格的按钮 */
QPushButton#Light {
background-color: #818182; /* 浅灰色 */
color: black;
}
QPushButton#Light:hover {
background-color: #737475;
}
/* Dark 风格的按钮 */
QPushButton#Dark {
background-color: #1b1c1e; /* 接近黑色 */
}
QPushButton#Dark:hover {
background-color: #1a1b1d;
}
QToolButton {
background-color: transparent !important;
border: none !important;
}
QToolButton:hover {
background-color: rgba(255, 255, 255, 20) !important;
border: 1px solid #555 !important; /* 鼠标悬停时的边框 */
}
QToolButton:pressed, QToolButton:checked {
background-color: rgba(255, 255, 255, 40) !important;
}
QToolButton#stayOnTopButton:checked {
border: 1px solid #555 !important; /* 置顶时的边框 */
}
QToolButton#closeButton:hover {
background-color: red !important; /* 鼠标悬停在关闭按钮上时的背景色 */
}

94
src/ui/title_bar.py Normal file
View File

@ -0,0 +1,94 @@
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QFont, QIcon, QPixmap
from PyQt6.QtWidgets import QHBoxLayout, QLabel, QToolButton, QWidget
class CustomTitleBar(QWidget):
def __init__(self, parent):
super().__init__(parent)
self.parent = parent
self.setup_ui()
self.mousePressed = False
def setup_ui(self):
self.layout = QHBoxLayout()
self.layout.setContentsMargins(0, 0, 0, 0) # 设置布局的边距为0
self.layout.setSpacing(0) # 设置按钮之间的间距为0
# 创建图标 QLabel
self.iconLabel = QLabel(self)
self.iconLabel.setFixedSize(32, 32) # 设置图标大小
iconPixmap = QPixmap('icons:icon.png').scaled(24, 24, Qt.AspectRatioMode.KeepAspectRatio)
self.iconLabel.setPixmap(iconPixmap)
self.layout.addWidget(self.iconLabel) # 添加图标到布局
# 创建标题 QLabel
font = QFont()
font.setPointSize(11)
self.titleLabel = QLabel("zayac的小工具")
self.titleLabel.setFont(font)
self.titleLabel.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
self.layout.addWidget(self.titleLabel, 1)
# 使用图标按钮并应用样式
self.stayOnTopButton = QToolButton()
self.stayOnTopButton.setIcon(QIcon('icons:top.svg'))
self.stayOnTopButton.setFixedSize(32, 32) # 设置按钮的固定大小
self.stayOnTopButton.setCheckable(True)
self.stayOnTopButton.clicked.connect(self.toggle_stay_on_top)
self.layout.addWidget(self.stayOnTopButton)
# 设置其他按钮
self.minimizeButton = QToolButton()
self.minimizeButton.setIcon(QIcon('icons:min.svg'))
self.minimizeButton.setFixedSize(32, 32)
self.minimizeButton.clicked.connect(self.parent.showMinimized)
self.layout.addWidget(self.minimizeButton)
self.maximizeButton = QToolButton()
self.maximizeButton.setIcon(QIcon('icons:max.svg'))
self.maximizeButton.setFixedSize(32, 32)
self.maximizeButton.clicked.connect(self.toggle_maximize)
self.layout.addWidget(self.maximizeButton)
self.closeButton = QToolButton()
self.closeButton.setIcon(QIcon('icons:close.svg'))
self.closeButton.setFixedSize(32, 32)
self.closeButton.clicked.connect(self.parent.close)
self.closeButton.setObjectName("closeButton")
self.layout.addWidget(self.closeButton)
# 在创建按钮后立即设置样式表
self.setLayout(self.layout)
def mousePressEvent(self, event):
self.mousePressed = True
self.mousePos = event.globalPosition().toPoint()
def mouseMoveEvent(self, event):
if self.mousePressed:
self.parent.move(self.parent.pos() + (event.globalPosition().toPoint() - self.mousePos))
self.mousePos = event.globalPosition().toPoint()
def mouseReleaseEvent(self, event):
self.mousePressed = False
def toggle_stay_on_top(self):
if self.stayOnTopButton.isChecked():
self.parent.setWindowFlags(self.parent.windowFlags() | Qt.WindowType.WindowStaysOnTopHint)
else:
self.parent.setWindowFlags(self.parent.windowFlags() & ~Qt.WindowType.WindowStaysOnTopHint)
self.parent.show() # 重新显示窗口以应用新的窗口标志
def enterEvent(self, event):
self.parent.reset_cursor_style() # 通知主窗口重置鼠标样式
def leaveEvent(self, event):
self.parent.reset_cursor_style() # 通知主窗口重置鼠标样式
def toggle_maximize(self):
if self.parent.isMaximized():
self.parent.showNormal()
else:
self.parent.showMaximized()