feat(ui): 初步完成历史记录前端适配
This commit is contained in:
@@ -30,6 +30,9 @@ import sqlite3
|
||||
import json
|
||||
import sys
|
||||
import shutil
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from qfluentwidgets import (
|
||||
QConfig,
|
||||
@@ -42,6 +45,7 @@ from qfluentwidgets import (
|
||||
OptionsValidator,
|
||||
qconfig,
|
||||
)
|
||||
from typing import Union, Dict, List
|
||||
|
||||
|
||||
class AppConfig:
|
||||
@@ -54,7 +58,7 @@ class AppConfig:
|
||||
self.log_path = self.app_path / "debug/AUTO_MAA.log"
|
||||
self.database_path = self.app_path / "data/data.db"
|
||||
self.config_path = self.app_path / "config/config.json"
|
||||
self.history_path = self.app_path / "config/history.json"
|
||||
self.history_path = self.app_path / "history/main.json"
|
||||
self.key_path = self.app_path / "data/key"
|
||||
self.gameid_path = self.app_path / "data/gameid.txt"
|
||||
self.version_path = self.app_path / "resources/version.json"
|
||||
@@ -74,6 +78,7 @@ class AppConfig:
|
||||
(self.app_path / "config").mkdir(parents=True, exist_ok=True)
|
||||
(self.app_path / "data").mkdir(parents=True, exist_ok=True)
|
||||
(self.app_path / "debug").mkdir(parents=True, exist_ok=True)
|
||||
(self.app_path / "history").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 生成版本信息文件
|
||||
if not self.version_path.exists():
|
||||
@@ -461,6 +466,206 @@ class AppConfig:
|
||||
cur.close()
|
||||
db.close()
|
||||
|
||||
def save_maa_log(self, log_path: Path, logs: list, maa_result: str) -> None:
|
||||
"""保存MAA日志"""
|
||||
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
data: Dict[str, Union[str, Dict[str, Union[int, dict]]]] = {
|
||||
"recruit_statistics": defaultdict(int),
|
||||
"drop_statistics": defaultdict(dict),
|
||||
"maa_result": maa_result,
|
||||
}
|
||||
|
||||
# 公招统计(仅统计招募到的)
|
||||
confirmed_recruit = False
|
||||
current_star_level = None
|
||||
i = 0
|
||||
while i < len(logs):
|
||||
if "公招识别结果:" in logs[i]:
|
||||
current_star_level = None # 每次识别公招时清空之前的星级
|
||||
i += 1
|
||||
while i < len(logs) and "Tags" not in logs[i]: # 读取所有公招标签
|
||||
i += 1
|
||||
|
||||
if i < len(logs) and "Tags" in logs[i]: # 识别星级
|
||||
star_match = re.search(r"(\d+)\s*★ Tags", logs[i])
|
||||
if star_match:
|
||||
current_star_level = f"{star_match.group(1)}★"
|
||||
|
||||
if "已确认招募" in logs[i]: # 只有确认招募后才统计
|
||||
confirmed_recruit = True
|
||||
|
||||
if confirmed_recruit and current_star_level:
|
||||
data["recruit_statistics"][current_star_level] += 1
|
||||
confirmed_recruit = False # 重置,等待下一次公招
|
||||
current_star_level = None # 清空已处理的星级
|
||||
|
||||
i += 1
|
||||
|
||||
# 掉落统计
|
||||
current_stage = None
|
||||
stage_drops = {}
|
||||
|
||||
for i, line in enumerate(logs):
|
||||
drop_match = re.search(r"([A-Za-z0-9\-]+) 掉落统计:", line)
|
||||
if drop_match:
|
||||
# 发现新关卡,保存前一个关卡数据
|
||||
if current_stage and stage_drops:
|
||||
data["drop_statistics"][current_stage] = stage_drops
|
||||
|
||||
current_stage = drop_match.group(1)
|
||||
stage_drops = {}
|
||||
continue
|
||||
|
||||
if current_stage:
|
||||
item_match: List[str] = re.findall(
|
||||
r"([\u4e00-\u9fa5]+)\s*:\s*([\d,]+)(?:\s*\(\+[\d,]+\))?", line
|
||||
)
|
||||
for item, total in item_match:
|
||||
# 解析数值时去掉逗号 (如 2,160 -> 2160)
|
||||
total = int(total.replace(",", ""))
|
||||
|
||||
# 黑名单
|
||||
if item not in [
|
||||
"当前次数",
|
||||
"理智",
|
||||
"最快截图耗时",
|
||||
"专精等级",
|
||||
"剩余时间",
|
||||
]:
|
||||
stage_drops[item] = total
|
||||
|
||||
# 处理最后一个关卡的掉落数据
|
||||
if current_stage and stage_drops:
|
||||
data["drop_statistics"][current_stage] = stage_drops
|
||||
|
||||
# 保存日志
|
||||
with log_path.open("w", encoding="utf-8") as f:
|
||||
f.writelines(logs)
|
||||
with log_path.with_suffix(".json").open("w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=4)
|
||||
|
||||
logger.info(f"处理完成:{log_path}")
|
||||
|
||||
self.merge_maa_logs(log_path.parent)
|
||||
|
||||
def merge_maa_logs(self, logs_path: Path) -> None:
|
||||
"""合并所有 .log 文件"""
|
||||
|
||||
data = {
|
||||
"recruit_statistics": defaultdict(int),
|
||||
"drop_statistics": defaultdict(dict),
|
||||
"maa_result": defaultdict(str),
|
||||
}
|
||||
|
||||
for json_file in logs_path.glob("*.json"):
|
||||
|
||||
with json_file.open("r", encoding="utf-8") as f:
|
||||
single_data: Dict[str, Union[str, Dict[str, Union[int, dict]]]] = (
|
||||
json.load(f)
|
||||
)
|
||||
|
||||
# 合并公招统计
|
||||
for star_level, count in single_data["recruit_statistics"].items():
|
||||
data["recruit_statistics"][star_level] += count
|
||||
|
||||
# 合并掉落统计
|
||||
for stage, drops in single_data["drop_statistics"].items():
|
||||
if stage not in data["drop_statistics"]:
|
||||
data["drop_statistics"][stage] = {} # 初始化关卡
|
||||
|
||||
for item, count in drops.items():
|
||||
|
||||
if item in data["drop_statistics"][stage]:
|
||||
data["drop_statistics"][stage][item] += count
|
||||
else:
|
||||
data["drop_statistics"][stage][item] = count
|
||||
|
||||
# 合并MAA结果
|
||||
data["maa_result"][
|
||||
json_file.name.replace(".json", "").replace("-", ":")
|
||||
] = single_data["maa_result"]
|
||||
|
||||
# 生成汇总 JSON 文件
|
||||
with logs_path.with_suffix(".json").open("w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=4)
|
||||
|
||||
logger.info(f"统计完成:{logs_path.with_suffix(".json")}")
|
||||
|
||||
def load_maa_logs(
|
||||
self, mode: str, json_path: Path
|
||||
) -> Dict[str, Union[str, list, Dict[str, list]]]:
|
||||
"""加载MAA日志统计信息"""
|
||||
|
||||
if mode == "总览":
|
||||
|
||||
with json_path.open("r", encoding="utf-8") as f:
|
||||
info: Dict[str, Dict[str, Union[int, dict]]] = json.load(f)
|
||||
|
||||
data = {}
|
||||
data["条目索引"] = [
|
||||
[k, "完成" if v == "Success!" else "异常"]
|
||||
for k, v in info["maa_result"].items()
|
||||
]
|
||||
data["条目索引"].insert(0, ["数据总览", "运行"])
|
||||
data["统计数据"] = {"公招统计": list(info["recruit_statistics"].items())}
|
||||
|
||||
for game_id, drops in info["drop_statistics"].items():
|
||||
data["统计数据"][f"掉落统计:{game_id}"] = list(drops.items())
|
||||
|
||||
data["统计数据"]["报错汇总"] = [
|
||||
[k, v] for k, v in info["maa_result"].items() if v != "Success!"
|
||||
]
|
||||
|
||||
elif mode == "单项":
|
||||
|
||||
with json_path.open("r", encoding="utf-8") as f:
|
||||
info: Dict[str, Union[str, Dict[str, Union[int, dict]]]] = json.load(f)
|
||||
|
||||
data = {}
|
||||
|
||||
data["统计数据"] = {"公招统计": list(info["recruit_statistics"].items())}
|
||||
|
||||
for game_id, drops in info["drop_statistics"].items():
|
||||
data["统计数据"][f"掉落统计:{game_id}"] = list(drops.items())
|
||||
|
||||
with json_path.with_suffix(".log").open("r", encoding="utf-8") as f:
|
||||
log = f.read()
|
||||
|
||||
data["日志信息"] = log
|
||||
|
||||
return data
|
||||
|
||||
def search_history(self) -> dict:
|
||||
"""搜索所有历史记录"""
|
||||
|
||||
history_dict = {}
|
||||
|
||||
for date_folder in (Config.app_path / "history").iterdir():
|
||||
if not date_folder.is_dir():
|
||||
continue # 只处理日期文件夹
|
||||
|
||||
try:
|
||||
|
||||
date = datetime.strptime(date_folder.name, "%Y-%m-%d")
|
||||
|
||||
history_dict[date.strftime("%Y年 %m月 %d日")] = list(
|
||||
date_folder.glob("*.json")
|
||||
)
|
||||
|
||||
except ValueError:
|
||||
logger.warning(f"非日期格式的目录: {date_folder}")
|
||||
|
||||
return {
|
||||
k: v
|
||||
for k, v in sorted(
|
||||
history_dict.items(),
|
||||
key=lambda x: datetime.strptime(x[0], "%Y年 %m月 %d日"),
|
||||
reverse=True,
|
||||
)
|
||||
}
|
||||
|
||||
def save_history(self, key: str, content: dict) -> None:
|
||||
"""保存历史记录"""
|
||||
|
||||
@@ -540,6 +745,9 @@ class AppConfig:
|
||||
class GlobalConfig(QConfig):
|
||||
"""全局配置"""
|
||||
|
||||
function_HistoryRetentionTime = OptionsConfigItem(
|
||||
"Function", "HistoryRetentionTime", 7, OptionsValidator([7, 15, 30, 60, 0])
|
||||
)
|
||||
function_IfAllowSleep = ConfigItem(
|
||||
"Function", "IfAllowSleep", False, BoolValidator()
|
||||
)
|
||||
@@ -582,12 +790,6 @@ class GlobalConfig(QConfig):
|
||||
update_UpdateType = OptionsConfigItem(
|
||||
"Update", "UpdateType", "main", OptionsValidator(["main", "dev"])
|
||||
)
|
||||
# 日志管理
|
||||
function_IfEnableLog = ConfigItem("Function", "IfEnableLog", False, BoolValidator())
|
||||
function_LogRetentionDays = OptionsConfigItem(
|
||||
"Function", "LogRetentionDays", "7 天",
|
||||
OptionsValidator(["7 天", "15 天", "30 天", "60 天", "永不清理"])
|
||||
)
|
||||
|
||||
|
||||
class QueueConfig(QConfig):
|
||||
|
||||
@@ -38,7 +38,6 @@ from typing import List
|
||||
|
||||
from app.core import Config
|
||||
from app.services import Notify, System
|
||||
from app.services.maa_log_analyzer import analyze_maa_logs
|
||||
|
||||
|
||||
class MaaManager(QObject):
|
||||
@@ -288,6 +287,14 @@ class MaaManager(QObject):
|
||||
# 移除静默进程标记
|
||||
Config.silence_list.remove(self.emulator_path)
|
||||
|
||||
# 保存运行日志
|
||||
Config.save_maa_log(
|
||||
Config.app_path
|
||||
/ f"history/{curdate}/{self.data[user[2]][0]}/{start_time.strftime("%H-%M-%S")}.log",
|
||||
self.check_maa_log(start_time, mode_book[j]),
|
||||
self.maa_result,
|
||||
)
|
||||
|
||||
# 成功完成代理的用户修改相关参数
|
||||
if run_book[0] and run_book[1]:
|
||||
if self.data[user[2]][14] == 0 and self.data[user[2]][3] != -1:
|
||||
@@ -531,8 +538,8 @@ class MaaManager(QObject):
|
||||
with self.maa_log_path.open(mode="r", encoding="utf-8") as f:
|
||||
pass
|
||||
|
||||
def check_maa_log(self, start_time: datetime, mode: str) -> None:
|
||||
"""检查MAA日志以判断MAA程序运行状态"""
|
||||
def check_maa_log(self, start_time: datetime, mode: str) -> list:
|
||||
"""获取MAA日志并检查以判断MAA程序运行状态"""
|
||||
|
||||
# 获取日志
|
||||
logs = []
|
||||
@@ -616,6 +623,8 @@ class MaaManager(QObject):
|
||||
|
||||
self.quit_monitor()
|
||||
|
||||
return logs
|
||||
|
||||
def start_monitor(self, start_time: datetime, mode: str) -> None:
|
||||
"""开始监视MAA日志"""
|
||||
|
||||
@@ -638,70 +647,6 @@ class MaaManager(QObject):
|
||||
self.log_monitor_timer.stop()
|
||||
self.monitor_loop.quit()
|
||||
|
||||
# 检查用户是否开启日志保存功能
|
||||
if not Config.global_config.get(Config.global_config.function_IfEnableLog):
|
||||
logger.info(f"{self.name} | 用户未启用日志保存功能,跳过保存")
|
||||
return
|
||||
|
||||
# 获取当前运行的用户
|
||||
current_user = next((user[0].split(" - ")[0] for user in self.user_list if user[1] == "运行"), "UnknownUser")
|
||||
|
||||
# 新增日志保存功能
|
||||
try:
|
||||
# 获取当前日期和时间
|
||||
now = datetime.now()
|
||||
date_str = now.strftime("%Y-%m-%d")
|
||||
time_str = now.strftime("%H-%M-%S")
|
||||
|
||||
# 停三秒,保证日志完全写入
|
||||
time.sleep(3)
|
||||
|
||||
# 设定日志保存路径:/maa_run_history/{date}/{实例}/{用户}/{time}.log
|
||||
base_path = Path(f"./maa_run_history/{date_str}/{self.name}/{current_user}")
|
||||
base_path.mkdir(parents=True, exist_ok=True) # 确保目录存在
|
||||
|
||||
log_file_path = base_path / f"{time_str}.log"
|
||||
|
||||
# 读取 MAA 运行日志
|
||||
with self.maa_log_path.open(mode="r", encoding="utf-8") as f:
|
||||
logs = f.readlines()
|
||||
|
||||
# **只获取最后一次 MAA 运行日志**
|
||||
last_start_idx = None
|
||||
last_exit_idx = None
|
||||
|
||||
# 反向查找最后一个 "MaaAssistantArknights GUI exited"
|
||||
for i in range(len(logs) - 1, -1, -1):
|
||||
if "MaaAssistantArknights GUI exited" in logs[i]:
|
||||
last_exit_idx = i
|
||||
break
|
||||
|
||||
# 反向查找最近的 "MaaAssistantArknights GUI started"
|
||||
if last_exit_idx is not None:
|
||||
for i in range(last_exit_idx, -1, -1):
|
||||
if "MaaAssistantArknights GUI started" in logs[i]:
|
||||
last_start_idx = i
|
||||
break
|
||||
|
||||
# 确保找到了完整的日志片段
|
||||
if last_start_idx is not None and last_exit_idx is not None:
|
||||
relevant_logs = logs[last_start_idx: last_exit_idx + 1]
|
||||
|
||||
# 只保存最后一次的完整日志
|
||||
with log_file_path.open(mode="w", encoding="utf-8") as f:
|
||||
f.writelines(relevant_logs)
|
||||
|
||||
logger.info(f"{self.name} | 运行日志已保存: {log_file_path}")
|
||||
|
||||
# ========== **调用分析函数** ==========
|
||||
analyze_maa_logs(base_path)
|
||||
|
||||
else:
|
||||
logger.warning(f"{self.name} | 未找到完整的 MAA 运行日志片段,跳过保存")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"{self.name} | 日志保存失败: {str(e)}")
|
||||
|
||||
def set_maa(self, mode, index):
|
||||
"""配置MAA运行参数"""
|
||||
logger.info(f"{self.name} | 配置MAA运行参数: {mode}/{index}")
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from collections import defaultdict
|
||||
from loguru import logger
|
||||
|
||||
from app.core import Config
|
||||
|
||||
|
||||
def analyze_maa_logs(logs_directory: Path):
|
||||
"""
|
||||
遍历 logs_directory 下所有 .log 文件,解析公招和掉落信息,并保存为 JSON 文件
|
||||
"""
|
||||
if not logs_directory.exists():
|
||||
logger.error(f"目录不存在: {logs_directory}")
|
||||
return
|
||||
|
||||
# **检查并删除超期日志**
|
||||
clean_old_logs(logs_directory)
|
||||
|
||||
# 设定 JSON 输出路径
|
||||
json_output_path = logs_directory / f"{logs_directory.name}.json" if logs_directory.parent.name == "maa_run_history" else logs_directory.parent / f"{logs_directory.name}.json"
|
||||
|
||||
aggregated_data = {
|
||||
# "recruit_statistics": defaultdict(int),
|
||||
"drop_statistics": defaultdict(lambda: defaultdict(int)),
|
||||
}
|
||||
|
||||
log_files = list(logs_directory.rglob("*.log"))
|
||||
if not log_files:
|
||||
logger.error(f"没有找到 .log 文件: {logs_directory}")
|
||||
return
|
||||
|
||||
for log_file in log_files:
|
||||
analyze_single_log(log_file, aggregated_data)
|
||||
|
||||
# 生成 JSON 文件
|
||||
with open(json_output_path, "w", encoding="utf-8") as json_file:
|
||||
json.dump(aggregated_data, json_file, ensure_ascii=False, indent=4)
|
||||
|
||||
logger.info(f"统计完成:{json_output_path}")
|
||||
|
||||
def analyze_single_log(log_file_path: Path, aggregated_data):
|
||||
"""
|
||||
解析单个 .log 文件,提取公招结果 & 关卡掉落数据
|
||||
"""
|
||||
# recruit_data = aggregated_data["recruit_statistics"]
|
||||
drop_data = aggregated_data["drop_statistics"]
|
||||
|
||||
with open(log_file_path, "r", encoding="utf-8") as f:
|
||||
logs = f.readlines()
|
||||
|
||||
# # **公招统计**
|
||||
# i = 0
|
||||
# while i < len(logs):
|
||||
# if "公招识别结果:" in logs[i]:
|
||||
# tags = []
|
||||
# i += 1
|
||||
# while i < len(logs) and "Tags" not in logs[i]: # 读取所有公招标签
|
||||
# tags.append(logs[i].strip())
|
||||
# i += 1
|
||||
#
|
||||
# if i < len(logs) and "Tags" in logs[i]: # 确保 Tags 行存在
|
||||
# star_match = re.search(r"(\d+)\s*Tags", logs[i]) # 提取 3,4,5,6 星
|
||||
# if star_match:
|
||||
# star_level = f"{star_match.group(1)}★"
|
||||
# recruit_data[star_level] += 1
|
||||
# i += 1
|
||||
|
||||
# **掉落统计**
|
||||
current_stage = None
|
||||
for i, line in enumerate(logs):
|
||||
drop_match = re.search(r"(\d+-\d+) 掉落统计:", line)
|
||||
if drop_match:
|
||||
current_stage = drop_match.group(1)
|
||||
continue
|
||||
|
||||
if current_stage and re.search(r"(\S+)\s*:\s*(\d+)\s*\(\+\d+\)", line):
|
||||
item_match = re.findall(r"(\S+)\s*:\s*(\d+)\s*\(\+(\d+)\)", line)
|
||||
for item, total, increment in item_match:
|
||||
drop_data[current_stage][item] += int(increment)
|
||||
|
||||
logger.info(f"处理完成:{log_file_path}")
|
||||
|
||||
|
||||
def clean_old_logs(logs_directory: Path):
|
||||
"""
|
||||
删除超过用户设定天数的日志文件
|
||||
"""
|
||||
retention_setting = Config.global_config.get(Config.global_config.function_LogRetentionDays)
|
||||
retention_days_mapping = {
|
||||
"7 天": 7,
|
||||
"15 天": 15,
|
||||
"30 天": 30,
|
||||
"60 天": 60,
|
||||
"永不清理": None
|
||||
}
|
||||
|
||||
retention_days = retention_days_mapping.get(retention_setting, None)
|
||||
if retention_days is None:
|
||||
logger.info("🔵 用户设置日志保留时间为【永不清理】,跳过清理")
|
||||
return
|
||||
|
||||
cutoff_time = datetime.now() - timedelta(days=retention_days)
|
||||
|
||||
deleted_count = 0
|
||||
for log_file in logs_directory.rglob("*.log"):
|
||||
file_time = datetime.fromtimestamp(log_file.stat().st_mtime) # 获取文件的修改时间
|
||||
if file_time < cutoff_time:
|
||||
try:
|
||||
os.remove(log_file)
|
||||
deleted_count += 1
|
||||
logger.info(f"🗑️ 已删除超期日志: {log_file}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 删除日志失败: {log_file}, 错误: {e}")
|
||||
|
||||
logger.info(f"✅ 清理完成: {deleted_count} 个日志文件")
|
||||
|
||||
# # 运行代码
|
||||
# logs_directory = Path("")
|
||||
# analyze_maa_logs(logs_directory)
|
||||
@@ -39,6 +39,10 @@ from qfluentwidgets import (
|
||||
Signal,
|
||||
ComboBox,
|
||||
CheckBox,
|
||||
IconWidget,
|
||||
FluentIcon,
|
||||
CardWidget,
|
||||
BodyLabel,
|
||||
qconfig,
|
||||
ConfigItem,
|
||||
TimeEdit,
|
||||
@@ -319,3 +323,53 @@ class TimeEditSettingCard(SettingCard):
|
||||
qconfig.set(self.configItem_time, value)
|
||||
|
||||
self.TimeEdit.setTime(QTime.fromString(value, "HH:mm"))
|
||||
|
||||
|
||||
class StatefulItemCard(CardWidget):
|
||||
|
||||
def __init__(self, item: list, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.Layout = QHBoxLayout(self)
|
||||
|
||||
self.Label = BodyLabel(item[0], self)
|
||||
self.icon = IconWidget(FluentIcon.MORE, self)
|
||||
self.icon.setFixedSize(16, 16)
|
||||
self.update_status(item[1])
|
||||
|
||||
self.Layout.addWidget(self.icon)
|
||||
self.Layout.addWidget(self.Label)
|
||||
self.Layout.addStretch(1)
|
||||
|
||||
def update_status(self, status: str):
|
||||
|
||||
if status == "完成":
|
||||
self.icon.setIcon(FluentIcon.ACCEPT)
|
||||
self.Label.setTextColor("#0eb840", "#0eb840")
|
||||
elif status == "等待":
|
||||
self.icon.setIcon(FluentIcon.MORE)
|
||||
self.Label.setTextColor("#161823", "#e3f9fd")
|
||||
elif status == "运行":
|
||||
self.icon.setIcon(FluentIcon.PLAY)
|
||||
self.Label.setTextColor("#177cb0", "#70f3ff")
|
||||
elif status == "跳过":
|
||||
self.icon.setIcon(FluentIcon.REMOVE)
|
||||
self.Label.setTextColor("#75878a", "#7397ab")
|
||||
elif status == "异常":
|
||||
self.icon.setIcon(FluentIcon.CLOSE)
|
||||
self.Label.setTextColor("#ff2121", "#ff2121")
|
||||
|
||||
|
||||
class QuantifiedItemCard(CardWidget):
|
||||
|
||||
def __init__(self, item: list, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.Layout = QHBoxLayout(self)
|
||||
|
||||
self.Name = BodyLabel(item[0], self)
|
||||
self.Numb = BodyLabel(str(item[1]), self)
|
||||
|
||||
self.Layout.addWidget(self.Name)
|
||||
self.Layout.addStretch(1)
|
||||
self.Layout.addWidget(self.Numb)
|
||||
|
||||
@@ -34,8 +34,6 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
from qfluentwidgets import (
|
||||
CardWidget,
|
||||
IconWidget,
|
||||
BodyLabel,
|
||||
Pivot,
|
||||
ScrollArea,
|
||||
FluentIcon,
|
||||
@@ -53,6 +51,7 @@ import json
|
||||
|
||||
|
||||
from app.core import Config, TaskManager, Task, MainInfoBar
|
||||
from .Widget import StatefulItemCard
|
||||
|
||||
|
||||
class DispatchCenter(QWidget):
|
||||
@@ -335,7 +334,7 @@ class DispatchBox(QWidget):
|
||||
self.viewLayout.addLayout(self.Layout)
|
||||
self.viewLayout.setContentsMargins(3, 0, 3, 3)
|
||||
|
||||
self.task_cards: List[ItemCard] = []
|
||||
self.task_cards: List[StatefulItemCard] = []
|
||||
|
||||
def create_task(self, task_list: list):
|
||||
"""创建任务队列"""
|
||||
@@ -351,7 +350,7 @@ class DispatchBox(QWidget):
|
||||
|
||||
for task in task_list:
|
||||
|
||||
self.task_cards.append(ItemCard(task))
|
||||
self.task_cards.append(StatefulItemCard(task))
|
||||
self.Layout.addWidget(self.task_cards[-1])
|
||||
|
||||
self.Layout.addStretch(1)
|
||||
@@ -373,7 +372,7 @@ class DispatchBox(QWidget):
|
||||
self.viewLayout.addLayout(self.Layout)
|
||||
self.viewLayout.setContentsMargins(3, 0, 3, 3)
|
||||
|
||||
self.user_cards: List[ItemCard] = []
|
||||
self.user_cards: List[StatefulItemCard] = []
|
||||
|
||||
def create_user(self, user_list: list):
|
||||
"""创建用户队列"""
|
||||
@@ -389,7 +388,7 @@ class DispatchBox(QWidget):
|
||||
|
||||
for user in user_list:
|
||||
|
||||
self.user_cards.append(ItemCard(user))
|
||||
self.user_cards.append(StatefulItemCard(user))
|
||||
self.Layout.addWidget(self.user_cards[-1])
|
||||
|
||||
self.Layout.addStretch(1)
|
||||
@@ -419,38 +418,3 @@ class DispatchBox(QWidget):
|
||||
|
||||
self.text.moveCursor(QTextCursor.End)
|
||||
self.text.ensureCursorVisible()
|
||||
|
||||
|
||||
class ItemCard(CardWidget):
|
||||
|
||||
def __init__(self, task_item: list, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.Layout = QHBoxLayout(self)
|
||||
|
||||
self.Label = BodyLabel(task_item[0], self)
|
||||
self.icon = IconWidget(FluentIcon.MORE, self)
|
||||
self.icon.setFixedSize(16, 16)
|
||||
self.update_status(task_item[1])
|
||||
|
||||
self.Layout.addWidget(self.icon)
|
||||
self.Layout.addWidget(self.Label)
|
||||
self.Layout.addStretch(1)
|
||||
|
||||
def update_status(self, status: str):
|
||||
|
||||
if status == "完成":
|
||||
self.icon.setIcon(FluentIcon.ACCEPT)
|
||||
self.Label.setTextColor("#0eb840", "#0eb840")
|
||||
elif status == "等待":
|
||||
self.icon.setIcon(FluentIcon.MORE)
|
||||
self.Label.setTextColor("#7397ab", "#7397ab")
|
||||
elif status == "运行":
|
||||
self.icon.setIcon(FluentIcon.PLAY)
|
||||
self.Label.setTextColor("#2e4e7e", "#2e4e7e")
|
||||
elif status == "跳过":
|
||||
self.icon.setIcon(FluentIcon.REMOVE)
|
||||
self.Label.setTextColor("#606060", "#d2d2d2")
|
||||
elif status == "异常":
|
||||
self.icon.setIcon(FluentIcon.CLOSE)
|
||||
self.Label.setTextColor("#ff2121", "#ff2121")
|
||||
|
||||
264
app/ui/history.py
Normal file
264
app/ui/history.py
Normal file
@@ -0,0 +1,264 @@
|
||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
||||
# Copyright © <2024> <DLmaster361>
|
||||
|
||||
# This file is part of AUTO_MAA.
|
||||
|
||||
# AUTO_MAA is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO_MAA is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# DLmaster_361@163.com
|
||||
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA历史记录界面
|
||||
v4.2
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
from loguru import logger
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
QVBoxLayout,
|
||||
QHBoxLayout,
|
||||
)
|
||||
from qfluentwidgets import (
|
||||
ScrollArea,
|
||||
FluentIcon,
|
||||
HeaderCardWidget,
|
||||
PushButton,
|
||||
ExpandGroupSettingCard,
|
||||
TextBrowser,
|
||||
)
|
||||
from PySide6.QtCore import Signal
|
||||
import os
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
|
||||
from app.core import Config
|
||||
from .Widget import StatefulItemCard, QuantifiedItemCard
|
||||
|
||||
|
||||
class History(QWidget):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("历史记录")
|
||||
|
||||
content_widget = QWidget()
|
||||
self.content_layout = QVBoxLayout(content_widget)
|
||||
|
||||
scrollArea = ScrollArea()
|
||||
scrollArea.setWidgetResizable(True)
|
||||
scrollArea.setWidget(content_widget)
|
||||
layout = QVBoxLayout()
|
||||
layout.addWidget(scrollArea)
|
||||
self.setLayout(layout)
|
||||
|
||||
self.history_card_list = []
|
||||
|
||||
self.refresh()
|
||||
|
||||
def refresh(self):
|
||||
"""刷新脚本实例界面"""
|
||||
|
||||
while self.content_layout.count() > 0:
|
||||
item = self.content_layout.takeAt(0)
|
||||
if item.spacerItem():
|
||||
self.content_layout.removeItem(item.spacerItem())
|
||||
elif item.widget():
|
||||
item.widget().deleteLater()
|
||||
|
||||
self.history_card_list = []
|
||||
|
||||
history_dict = Config.search_history()
|
||||
|
||||
for date, user_list in history_dict.items():
|
||||
|
||||
self.history_card_list.append(HistoryCard(date, user_list, self))
|
||||
self.content_layout.addWidget(self.history_card_list[-1])
|
||||
|
||||
self.content_layout.addStretch(1)
|
||||
|
||||
|
||||
class HistoryCard(ExpandGroupSettingCard):
|
||||
|
||||
def __init__(self, date: str, user_list: List[Path], parent=None):
|
||||
super().__init__(
|
||||
FluentIcon.HISTORY,
|
||||
date,
|
||||
f"{date}的历史运行记录与统计信息",
|
||||
parent,
|
||||
)
|
||||
|
||||
widget = QWidget()
|
||||
Layout = QVBoxLayout(widget)
|
||||
self.viewLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.viewLayout.setSpacing(0)
|
||||
self.addGroupWidget(widget)
|
||||
|
||||
self.user_history_card_list = []
|
||||
|
||||
for user_path in user_list:
|
||||
|
||||
self.user_history_card_list.append(self.UserHistoryCard(user_path, self))
|
||||
Layout.addWidget(self.user_history_card_list[-1])
|
||||
|
||||
class UserHistoryCard(HeaderCardWidget):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
user_history_path: Path,
|
||||
parent=None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setTitle(user_history_path.name.replace(".json", ""))
|
||||
|
||||
self.user_history_path = user_history_path
|
||||
self.main_history = Config.load_maa_logs("总览", user_history_path)
|
||||
|
||||
self.index_card = self.IndexCard(self.main_history["条目索引"], self)
|
||||
self.statistics_card = QHBoxLayout()
|
||||
self.log_card = self.LogCard(self)
|
||||
|
||||
self.index_card.index_changed.connect(self.update_info)
|
||||
|
||||
self.viewLayout.addWidget(self.index_card)
|
||||
self.viewLayout.addLayout(self.statistics_card)
|
||||
self.viewLayout.addWidget(self.log_card)
|
||||
self.viewLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.viewLayout.setSpacing(0)
|
||||
self.viewLayout.setStretch(0, 1)
|
||||
self.viewLayout.setStretch(2, 4)
|
||||
|
||||
self.update_info("数据总览")
|
||||
|
||||
def update_info(self, index: str) -> None:
|
||||
"""更新信息"""
|
||||
|
||||
if index == "数据总览":
|
||||
|
||||
while self.statistics_card.count() > 0:
|
||||
item = self.statistics_card.takeAt(0)
|
||||
if item.spacerItem():
|
||||
self.statistics_card.removeItem(item.spacerItem())
|
||||
elif item.widget():
|
||||
item.widget().deleteLater()
|
||||
|
||||
for name, item_list in self.main_history["统计数据"].items():
|
||||
|
||||
statistics_card = self.StatisticsCard(name, item_list, self)
|
||||
self.statistics_card.addWidget(statistics_card)
|
||||
|
||||
self.log_card.hide()
|
||||
|
||||
else:
|
||||
|
||||
single_history = Config.load_maa_logs(
|
||||
"单项",
|
||||
self.user_history_path.with_suffix("")
|
||||
/ f"{index.replace(":","-")}.json",
|
||||
)
|
||||
|
||||
while self.statistics_card.count() > 0:
|
||||
item = self.statistics_card.takeAt(0)
|
||||
if item.spacerItem():
|
||||
self.statistics_card.removeItem(item.spacerItem())
|
||||
elif item.widget():
|
||||
item.widget().deleteLater()
|
||||
|
||||
for name, item_list in single_history["统计数据"].items():
|
||||
|
||||
statistics_card = self.StatisticsCard(name, item_list, self)
|
||||
self.statistics_card.addWidget(statistics_card)
|
||||
|
||||
self.log_card.text.setText(single_history["日志信息"])
|
||||
self.log_card.button.clicked.disconnect()
|
||||
self.log_card.button.clicked.connect(
|
||||
lambda: os.startfile(
|
||||
self.user_history_path.with_suffix("")
|
||||
/ f"{index.replace(":","-")}.log"
|
||||
)
|
||||
)
|
||||
self.log_card.show()
|
||||
|
||||
self.viewLayout.setStretch(1, self.statistics_card.count())
|
||||
|
||||
self.setMinimumHeight(300)
|
||||
|
||||
class IndexCard(HeaderCardWidget):
|
||||
|
||||
index_changed = Signal(str)
|
||||
|
||||
def __init__(self, index_list: list, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setTitle("记录条目")
|
||||
|
||||
self.Layout = QVBoxLayout()
|
||||
self.viewLayout.addLayout(self.Layout)
|
||||
self.viewLayout.setContentsMargins(3, 0, 3, 3)
|
||||
|
||||
self.index_cards: List[StatefulItemCard] = []
|
||||
|
||||
for index in index_list:
|
||||
|
||||
self.index_cards.append(StatefulItemCard(index))
|
||||
self.index_cards[-1].clicked.connect(
|
||||
partial(self.index_changed.emit, index[0])
|
||||
)
|
||||
self.Layout.addWidget(self.index_cards[-1])
|
||||
|
||||
self.Layout.addStretch(1)
|
||||
|
||||
class StatisticsCard(HeaderCardWidget):
|
||||
|
||||
def __init__(self, name: str, item_list: list, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setTitle(name)
|
||||
|
||||
self.Layout = QVBoxLayout()
|
||||
self.viewLayout.addLayout(self.Layout)
|
||||
self.viewLayout.setContentsMargins(3, 0, 3, 3)
|
||||
|
||||
self.item_cards: List[QuantifiedItemCard] = []
|
||||
|
||||
for item in item_list:
|
||||
|
||||
self.item_cards.append(QuantifiedItemCard(item))
|
||||
self.Layout.addWidget(self.item_cards[-1])
|
||||
|
||||
if len(item_list) == 0:
|
||||
self.Layout.addWidget(QuantifiedItemCard(["暂无记录", ""]))
|
||||
|
||||
self.Layout.addStretch(1)
|
||||
|
||||
class LogCard(HeaderCardWidget):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setTitle("日志")
|
||||
|
||||
self.text = TextBrowser(self)
|
||||
self.button = PushButton("打开日志文件", self)
|
||||
self.button.clicked.connect(lambda: print("打开日志文件"))
|
||||
|
||||
Layout = QVBoxLayout()
|
||||
Layout.addWidget(self.text)
|
||||
Layout.addWidget(self.button)
|
||||
self.viewLayout.setContentsMargins(3, 0, 3, 3)
|
||||
self.viewLayout.addLayout(Layout)
|
||||
@@ -46,6 +46,8 @@ from qfluentwidgets import (
|
||||
from PySide6.QtGui import QIcon, QCloseEvent
|
||||
from PySide6.QtCore import Qt, QTimer
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
import shutil
|
||||
|
||||
from app.core import Config, TaskManager, MainTimer, MainInfoBar
|
||||
from app.services import Notify, Crypto, System
|
||||
@@ -53,6 +55,7 @@ from .setting import Setting
|
||||
from .member_manager import MemberManager
|
||||
from .queue_manager import QueueManager
|
||||
from .dispatch_center import DispatchCenter
|
||||
from .history import History
|
||||
|
||||
|
||||
class AUTO_MAA(MSFluentWindow):
|
||||
@@ -76,6 +79,7 @@ class AUTO_MAA(MSFluentWindow):
|
||||
self.member_manager = MemberManager(self)
|
||||
self.queue_manager = QueueManager(self)
|
||||
self.dispatch_center = DispatchCenter(self)
|
||||
self.history = History(self)
|
||||
|
||||
self.addSubInterface(
|
||||
self.setting,
|
||||
@@ -105,6 +109,13 @@ class AUTO_MAA(MSFluentWindow):
|
||||
FluentIcon.IOT,
|
||||
NavigationItemPosition.TOP,
|
||||
)
|
||||
self.addSubInterface(
|
||||
self.history,
|
||||
FluentIcon.HISTORY,
|
||||
"历史记录",
|
||||
FluentIcon.HISTORY,
|
||||
NavigationItemPosition.BOTTOM,
|
||||
)
|
||||
self.stackedWidget.currentChanged.connect(
|
||||
lambda index: (self.member_manager.refresh() if index == 1 else None)
|
||||
)
|
||||
@@ -123,6 +134,9 @@ class AUTO_MAA(MSFluentWindow):
|
||||
self.dispatch_center.update_top_bar() if index == 3 else None
|
||||
)
|
||||
)
|
||||
self.stackedWidget.currentChanged.connect(
|
||||
lambda index: (self.history.refresh() if index == 4 else None)
|
||||
)
|
||||
|
||||
# 创建系统托盘及其菜单
|
||||
self.tray = QSystemTrayIcon(
|
||||
@@ -197,6 +211,9 @@ class AUTO_MAA(MSFluentWindow):
|
||||
qconfig.load(Config.config_path, Config.global_config)
|
||||
Config.global_config.save()
|
||||
|
||||
# 清理旧日志
|
||||
self.clean_old_logs()
|
||||
|
||||
# 检查密码
|
||||
self.setting.check_PASSWORD()
|
||||
|
||||
@@ -247,6 +264,40 @@ class AUTO_MAA(MSFluentWindow):
|
||||
if reason == QSystemTrayIcon.DoubleClick:
|
||||
self.show_ui("显示主窗口")
|
||||
|
||||
def clean_old_logs(self):
|
||||
"""
|
||||
删除超过用户设定天数的日志文件(基于目录日期)
|
||||
"""
|
||||
|
||||
if (
|
||||
Config.global_config.get(Config.global_config.function_HistoryRetentionTime)
|
||||
== 0
|
||||
):
|
||||
logger.info("由于用户设置日志永久保留,跳过日志清理")
|
||||
return
|
||||
|
||||
deleted_count = 0
|
||||
|
||||
for date_folder in (Config.app_path / "history").iterdir():
|
||||
if not date_folder.is_dir():
|
||||
continue # 只处理日期文件夹
|
||||
|
||||
try:
|
||||
# 只检查 `YYYY-MM-DD` 格式的文件夹
|
||||
folder_date = datetime.strptime(date_folder.name, "%Y-%m-%d")
|
||||
if datetime.now() - folder_date > timedelta(
|
||||
days=Config.global_config.get(
|
||||
Config.global_config.function_HistoryRetentionTime
|
||||
)
|
||||
):
|
||||
shutil.rmtree(date_folder, ignore_errors=True)
|
||||
deleted_count += 1
|
||||
logger.info(f"已删除超期日志目录: {date_folder}")
|
||||
except ValueError:
|
||||
logger.warning(f"非日期格式的目录: {date_folder}")
|
||||
|
||||
logger.info(f"清理完成: {deleted_count} 个日期目录")
|
||||
|
||||
def start_main_task(self) -> None:
|
||||
"""启动主任务"""
|
||||
|
||||
|
||||
@@ -77,7 +77,6 @@ class Setting(QWidget):
|
||||
self.function = FunctionSettingCard(self)
|
||||
self.start = StartSettingCard(self)
|
||||
self.ui = UiSettingCard(self)
|
||||
self.log_settings = LogSettingCard(self)
|
||||
self.notification = NotifySettingCard(self)
|
||||
self.security = SecuritySettingCard(self)
|
||||
self.updater = UpdaterSettingCard(self)
|
||||
@@ -93,7 +92,6 @@ class Setting(QWidget):
|
||||
content_layout.addWidget(self.function)
|
||||
content_layout.addWidget(self.start)
|
||||
content_layout.addWidget(self.ui)
|
||||
content_layout.addWidget(self.log_settings)
|
||||
content_layout.addWidget(self.notification)
|
||||
content_layout.addWidget(self.security)
|
||||
content_layout.addWidget(self.updater)
|
||||
@@ -419,6 +417,13 @@ class FunctionSettingCard(HeaderCardWidget):
|
||||
super().__init__(parent)
|
||||
self.setTitle("功能")
|
||||
|
||||
self.card_HistoryRetentionTime = ComboBoxSettingCard(
|
||||
configItem=Config.global_config.function_HistoryRetentionTime,
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="历史记录保留时间",
|
||||
content="选择历史记录的保留时间,超期自动清理",
|
||||
texts=["7 天", "15 天", "30 天", "60 天", "永久"],
|
||||
)
|
||||
self.card_IfAllowSleep = SwitchSettingCard(
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="启动时阻止系统休眠",
|
||||
@@ -434,6 +439,7 @@ class FunctionSettingCard(HeaderCardWidget):
|
||||
)
|
||||
|
||||
Layout = QVBoxLayout()
|
||||
Layout.addWidget(self.card_HistoryRetentionTime)
|
||||
Layout.addWidget(self.card_IfAllowSleep)
|
||||
Layout.addWidget(self.card_IfSilence)
|
||||
Layout.addWidget(self.card_IfAgreeBilibili)
|
||||
@@ -799,35 +805,6 @@ class OtherSettingCard(HeaderCardWidget):
|
||||
self.viewLayout.setSpacing(0)
|
||||
self.addGroupWidget(widget)
|
||||
|
||||
class LogSettingCard(HeaderCardWidget):
|
||||
"""日志管理设置"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setTitle("日志管理")
|
||||
|
||||
# 日志存储开关
|
||||
self.card_IfEnableLog = SwitchSettingCard(
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="启用日志存储",
|
||||
content="记录并存储每次运行的日志,用于多账号日常掉落统计",
|
||||
configItem=Config.global_config.function_IfEnableLog,
|
||||
)
|
||||
|
||||
# 日志保留天数设置
|
||||
self.card_LogRetentionDays = ComboBoxSettingCard(
|
||||
configItem=Config.global_config.function_LogRetentionDays,
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="日志保留天数",
|
||||
content="选择日志的保留时间,超期自动清理",
|
||||
texts=["7 天", "15 天", "30 天", "60 天", "永不清理"],
|
||||
)
|
||||
|
||||
Layout = QVBoxLayout()
|
||||
Layout.addWidget(self.card_IfEnableLog)
|
||||
Layout.addWidget(self.card_LogRetentionDays)
|
||||
self.viewLayout.addLayout(Layout)
|
||||
|
||||
|
||||
def version_text(version_numb: list) -> str:
|
||||
"""将版本号列表转为可读的文本信息"""
|
||||
|
||||
Reference in New Issue
Block a user