Files
better-genshin-impact/BetterGenshinImpact/GameTask/LogParse/LogParse.cs

1179 lines
50 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using BetterGenshinImpact.Core.Config;
using Wpf.Ui.Violeta.Controls;
using static BetterGenshinImpact.GameTask.LogParse.LogParse.ConfigGroupEntity;
namespace BetterGenshinImpact.GameTask.LogParse
{
public class LogParse
{
private static readonly string _configPath = Global.Absolute(@"log\logparse\config.json");
// 添加一个静态事件用于通知日志的生成状态
public static event Action<string> HtmlGenerationStatusChanged = delegate { };
private static void NotifyHtmlGenerationStatus(string status)
{
HtmlGenerationStatusChanged.Invoke(status);
}
private static List<string> SafeReadAllLines(string filePath)
{
var lines = new List<string>();
try
{
// 使用 FileStream 和 StreamReader允许共享读取
using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var reader = new StreamReader(fileStream);
while (reader.ReadLine() is { } line)
{
lines.Add(line);
}
}
catch (IOException ex)
{
Console.WriteLine($"无法读取文件 {filePath}: {ex.Message}");
}
return lines;
}
public static List<ConfigGroupEntity> ParseFile(List<(string, string)> logFiles)
{
List<(string, string)> logLines = new();
foreach (var logFile in logFiles)
{
string[] logstrs = SafeReadAllLines(logFile.Item1).ToArray();
foreach (var logstr in logstrs)
{
logLines.Add((logstr, logFile.Item2));
}
}
return Parse(logLines);
}
public static List<ConfigGroupEntity> Parse(List<(string, string)> logLines)
{
// var logstrs = log.Item1;
List<ConfigGroupEntity> configGroupEntities = new();
ConfigGroupEntity? configGroupEntity = null;
ConfigTask? configTask = null;
for (int i = 0; i < logLines.Count; i++)
{
var logstr = logLines[i].Item1;
var logrq = logLines[i].Item2;
//if("配置组 \"${}\" 加载完成共25个脚本开始执行")
// 定义正则表达式
var result = ParseBgiLine(@"配置组 ""(.+?)"" 加载完成,共(\d+)个脚本", logstr);
if (result.Item1)
{
configGroupEntity = new()
{
Name = result.Item2[1],
StartDate = ParsePreDataTime(logLines, i - 1, logrq)
};
configGroupEntities.Add(configGroupEntity);
}
if (configGroupEntity != null)
{
//配置组 "战斗" 执行结束
result = ParseBgiLine($"配置组 \"{configGroupEntity.Name}\" 执行结束", logstr);
if (result.Item1)
{
configGroupEntity.EndDate = ParsePreDataTime(logLines, i - 1, logrq);
configGroupEntity = null;
}
}
if (configGroupEntity != null)
{
result = ParseBgiLine(@"→ 开始执行(?:地图追踪任务|JS脚本): ""(.+?)""", logstr);
if (result.Item1)
{
configTask = new();
configTask.Name = result.Item2[1];
configTask.StartDate = ParsePreDataTime(logLines, i - 1, logrq);
configGroupEntity.ConfigTaskList.Add(configTask);
}
if (configTask != null)
{
//前往七天神像复活
if (logstr.EndsWith("前往七天神像复活"))
{
configTask.Fault.ReviveCount++;
}
//传送失败,重试 n 次
result = ParseBgiLine(@"传送失败,重试 (\d+) 次", logstr);
if (result.Item1)
{
configTask.Fault.TeleportFailCount = int.Parse(result.Item2[1]);
}
//战斗超时结束
if (logstr == "战斗超时结束")
{
configTask.Fault.BattleTimeoutCount++;
}
//重试一次路线或放弃此路线!
if (logstr.EndsWith("重试一次路线或放弃此路线!"))
{
configTask.Fault.RetryCount++;
}
//疑似卡死,尝试脱离...
if (logstr == "疑似卡死,尝试脱离...")
{
configTask.Fault.StuckCount++;
}
//One or more errors occurred
result = ParseBgiLine(@"执行脚本时发生异常: ""(.+?)""", logstr);
if (result.Item1)
{
configTask.Fault.ErrCount++;
}
if (logstr.StartsWith("→ 脚本执行结束: \"" + configTask.Name + "\""))
{
configTask.EndDate = ParsePreDataTime(logLines, i - 1, logrq);
configTask = null;
}
result = ParseBgiLine(@"交互或拾取:""(.+?)""", logstr);
if (result.Item1)
{
configTask.AddPick(result.Item2[1]);
}
}
}
}
//无论如何给个结束时间
if (configGroupEntity is { EndDate: null })
{
if (configGroupEntity.ConfigTaskList.Count > 0)
{
ConfigTask ct = configGroupEntity.ConfigTaskList[^1];
if (ct != null)
{
configGroupEntity.EndDate = ct.EndDate ?? ct.StartDate;
}
}
}
return configGroupEntities;
}
private static (bool, List<string>) ParseBgiLine(string pattern, string str)
{
Match match = Regex.Match(str, pattern);
if (match.Success)
{
return (true, match.Groups.Cast<Group>().Select(g => g.Value).ToList());
}
return (false, []);
}
private static DateTime? ParsePreDataTime(List<(string, string)> list, int index, string logrq)
{
if (index < 0)
{
return null;
}
(bool, List<string>) result = ParseBgiLine(@"\[(\d{2}:\d{2}:\d{2})\.\d+\]", list[index].Item1);
if (result.Item1)
{
DateTime dateTime = DateTime.ParseExact(logrq + " " + result.Item2[1], "yyyy-MM-dd HH:mm:ss", null);
return dateTime;
}
return null;
}
public class ConfigGroupEntity
{
//配置组名字
public string Name { get; set; }
//开始日期
public DateTime? StartDate { get; set; }
//结束日期
public DateTime? EndDate { get; set; }
//配置人物列表xxx.json
public List<ConfigTask> ConfigTaskList { get; } = new();
public class ConfigTask
{
public string Name { get; set; }
//开始日期
public DateTime? StartDate { get; set; }
//结束日期
public DateTime? EndDate { get; set; }
//拾取字典
public Dictionary<string, int> Picks { get; } = new();
public void AddPick(string val)
{
if (!Picks.ContainsKey(val))
{
Picks.Add(val, 0);
}
Picks[val] = Picks[val] + 1;
}
public FaultScenario Fault { get; set; } = new();
public class FaultScenario
{
//复活次数
public int ReviveCount { get; set; } = 0;
//传送失败次数
public int TeleportFailCount { get; set; } = 0;
//疑似卡死次数
public int StuckCount { get; set; } = 0;
//重试次数
public int RetryCount { get; set; } = 0;
//战斗超时
public int BattleTimeoutCount { get; set; } = 0;
//异常发生次数
public int ErrCount { get; set; } = 0;
}
}
}
public static List<(string FileName, string Date)> GetLogFiles(string folderPath)
{
// 定义返回的元组列表
var result = new List<(string FileName, string Date)>();
// 确认文件夹是否存在
if (!Directory.Exists(folderPath))
{
Console.WriteLine("指定的文件夹不存在。");
return result;
}
// 定义文件名匹配的正则表达式
string pattern = @"^better-genshin-impact(\d{8})(_\d{3})*\.log$";
Regex regex = new Regex(pattern);
// 遍历文件夹中的所有文件
var files = Directory.GetFiles(folderPath);
foreach (var file in files)
{
string fileName = Path.GetFileName(file);
// 检查文件名是否匹配模式
var match = regex.Match(fileName);
if (match.Success)
{
string dateString = match.Groups[1].Value;
// 尝试将日期字符串格式化为 yyyy-MM-dd
if (DateTime.TryParseExact(dateString, "yyyyMMdd", null, DateTimeStyles.None,
out DateTime parsedDate))
{
result.Add((folderPath + "\\" + fileName, parsedDate.ToString("yyyy-MM-dd")));
}
}
}
// 按日期排序
result = result.OrderBy(r => r.Date).ToList();
return result;
}
public static string ConvertSecondsToTime(double totalSeconds)
{
if (totalSeconds < 0)
throw new ArgumentException("Seconds cannot be negative.");
int hours = (int)(totalSeconds / 3600);
int minutes = (int)((totalSeconds % 3600) / 60);
double seconds = totalSeconds % 60;
string result = "";
if (hours > 0)
{
result += $"{hours}小时";
}
if (minutes > 0 || hours > 0)
{
result += $"{minutes}分钟";
}
if (seconds > 0 || (hours == 0 && minutes == 0))
{
// 根据小数点后是否为0决定是否保留小数
if (seconds % 1 == 0)
{
result += $"{(int)seconds}秒";
}
else
{
result += $"{seconds:F2}秒"; // 保留两位小数
}
}
return result;
}
// 根据时间获取对应的“自定义天”,即以凌晨 4 点为分组的开始
static DateTime GetCustomDay(string timeStr)
{
// 解析字符串为 DateTime 对象
DateTime time = DateTime.ParseExact(timeStr, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
// 获取当天的午夜 00:00 时间
DateTime midnight = time.Date;
// 计算自定义“天”的起始时间(午夜时间 + 4小时
DateTime customDayStart = midnight.AddHours(4);
// 如果当前时间早于自定义天的起始时间,则属于前一天
if (time < customDayStart)
{
customDayStart = customDayStart.AddDays(-1);
}
return customDayStart;
}
public static string FormatNumberWithStyle(int a, int b = 3)
{
if (a == 0)
{
return "";
}
// Determine the style based on the condition
string colorStyle = a >= b ? "color:red;" : string.Empty;
// Return the formatted HTML string
return $"<span style=\"font-weight:bold;{colorStyle}\">{a}</span>";
}
public static string GetNumberOrEmptyString(int number)
{
// 如果数字为0返回空字符串否则返回数字的字符串形式
return number == 0 ? string.Empty : number.ToString();
}
public static string SubtractFiveSeconds(string inputTime, int seconds)
{
try
{
// 将输入的字符串解析为 DateTime
DateTime parsedTime = DateTime.ParseExact(inputTime, "yyyy-MM-dd HH:mm:ss", null);
// 减去 5 秒
DateTime resultTime = parsedTime.AddSeconds(-seconds);
// 转换回指定格式的字符串并返回
return resultTime.ToString("yyyy-MM-dd HH:mm:ss");
}
catch (FormatException)
{
return "Invalid input time format. Please use 'yyyy-MM-dd HH:mm:ss'.";
}
}
public static string GenerHtmlByConfigGroupEntity(
List<ConfigGroupEntity> configGroups,
GameInfo? gameInfo,
LogParseConfig.ScriptGroupLogParseConfig scriptGroupLogParseConfig)
{
(string name, Func<ConfigTask, string> value, string sortType)[] colConfigs =
[
(name: "任务名称", value: task => Path.GetFileNameWithoutExtension(task.Name), sortType: "string"),
(name: "开始时间", value: task => task.StartDate?.ToString("yyyy-MM-dd HH:mm:ss") ?? "", sortType: "date"),
(name: "结束时间", value: task => task.EndDate?.ToString("yyyy-MM-dd HH:mm:ss") ?? "", sortType: "date"),
(name: "任务耗时", value: task => ConvertSecondsToTime((task.EndDate - task.StartDate)?.TotalSeconds ?? 0),
sortType: "number")
];
List<(string name, Func<ConfigTask, string> value, string sortType)>
colConfigList = new();
colConfigList.AddRange(colConfigs);
if (scriptGroupLogParseConfig.FaultStatsSwitch)
{
colConfigList.Add((name: "复活次数", value: task => FormatNumberWithStyle(task.Fault.ReviveCount),
sortType: "number"));
colConfigList.Add((name: "重试次数", value: task => FormatNumberWithStyle(task.Fault.RetryCount),
sortType: "number"));
colConfigList.Add((name: "疑似卡死次数", value: task => FormatNumberWithStyle(task.Fault.StuckCount),
sortType: "number"));
colConfigList.Add((name: "战斗超时次数", value: task => FormatNumberWithStyle(task.Fault.BattleTimeoutCount),
sortType: "number"));
colConfigList.Add((name: "传送失败次数", value: task => FormatNumberWithStyle(task.Fault.TeleportFailCount),
sortType: "number"));
colConfigList.Add((name: "异常发生次数", value: task => FormatNumberWithStyle(task.Fault.ErrCount),
sortType: "number"));
}
var msColConfigs = new (string name, Func<MoraStatistics, string> value, string sortType)[]
{
("日期", ms => ms.Name, "date"),
("小怪数量", ms => GetNumberOrEmptyString(ms.SmallMonsterStatistics), "number"),
("最后小怪时间", ms => ms.LastSmallTime, "date"),
("精英数量", ms => GetNumberOrEmptyString(ms.EliteGameStatistics), "number"),
("精英详细", ms => ms.EliteDetails, "string"),
("最后精英时间", ms => ms.LastEliteTime, "date"),
("总计锄地摩拉", ms => ms.TotalMoraKillingMonstersMora.ToString(), "number"),
("突发事件获取摩拉", ms => ms.EmergencyBonus, "number")
};
//锄地部分新曾字段
var col2Configs = new (string name, Func<MoraStatistics, string> value, string sortType)[]
{
("小怪", ms => GetNumberOrEmptyString(ms.SmallMonsterStatistics), "number"),
("精英", ms => GetNumberOrEmptyString(ms.EliteGameStatistics), "number"),
("精英详细", ms => ms.EliteDetails, "string"),
("锄地摩拉", ms => ms.TotalMoraKillingMonstersMora.ToString(), "number"),
(
name: "摩拉(每秒)",
value: ms => (ms.TotalMoraKillingMonstersMora /
(ms.StatisticsEnd - ms.StatisticsStart)?.TotalSeconds ?? 0)
.ToString("F2"),
sortType: "number"
)
};
StringBuilder html = new StringBuilder();
//从文件解析札记数据
NotifyHtmlGenerationStatus("正在解析札记数据...");
List<ActionItem> actionItems = new();
if (gameInfo != null)
{
actionItems = TravelsDiaryDetailManager.loadAllActionItems(gameInfo, configGroups);
int hoeingDelay;
if (int.TryParse(scriptGroupLogParseConfig.HoeingDelay, out hoeingDelay))
{
foreach (var actionItem in actionItems)
{
actionItem.Time = SubtractFiveSeconds(actionItem.Time, hoeingDelay);
}
}
}
NotifyHtmlGenerationStatus("正在生成日志分析内容...");
string htmlContent = GenerHtmlByConfigGroupEntity(configGroups, "日志分析", colConfigList.ToArray(),
col2Configs, actionItems,
msColConfigs);
// 检查HTML内容大小如果超过阈值则保存为文件
const int maxHtmlSize = 1 * 1024 * 1024; // 1MB 阈值,可以根据实际情况调整
if (htmlContent.Length > maxHtmlSize)
{
NotifyHtmlGenerationStatus($"日志分析较大({htmlContent.Length / 1024}KB),正在保存为文件...");
return SaveHtmlToTempFile(htmlContent);
}
NotifyHtmlGenerationStatus("日志分析生成完成!");
return htmlContent;
}
private static string SaveHtmlToTempFile(string htmlContent)
{
try
{
// 创建临时文件夹(如果不存在)
string tempFolder = Global.Absolute(@"log\logparse\");
if (!Directory.Exists(tempFolder))
{
Directory.CreateDirectory(tempFolder);
}
// 创建唯一的文件名
string fileName = $"LogAnalysis_{DateTime.Now:yyyyMMdd_HHmmss}.html";
string filePath = Path.Combine(tempFolder, fileName);
// 写入HTML内容
File.WriteAllText(filePath, htmlContent, Encoding.UTF8);
NotifyHtmlGenerationStatus($"日志分析文件已保存至: .\\log\\logparse\\{fileName}");
// 返回文件的URI
return new Uri(filePath).AbsoluteUri;
}
catch (Exception ex)
{
NotifyHtmlGenerationStatus($"保存日志分析到临时文件时出错: {ex.Message}");
Console.WriteLine($"保存日志分析到临时文件时出错: {ex.Message}");
return htmlContent; // 如果保存失败返回原始HTML内容
}
}
public static string ConcatenateStrings(string a, string b)
{
if (string.IsNullOrEmpty(b) || b == "0")
{
return "";
}
return a + b;
}
public static string GenerHtmlByConfigGroupEntity(
List<ConfigGroupEntity> configGroups,
string title,
(string name, Func<ConfigTask, string> value, string sortType)[] colConfigs,
(string name, Func<MoraStatistics, string> value, string sortType)[] col2Configs,
List<ActionItem> actionItems,
(string name, Func<MoraStatistics, string> value, string sortType)[] msColConfigs)
{
StringBuilder html = new StringBuilder();
// HTML头部
html.AppendLine("<!DOCTYPE html>");
html.AppendLine("<html lang=\"en\">");
html.AppendLine("<head>");
html.AppendLine(" <meta charset=\"UTF-8\">");
html.AppendLine(" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
html.AppendLine($" <title>{title}</title>");
html.AppendLine(" <style>");
html.AppendLine(" body { font-family: 'Segoe UI', Arial, sans-serif; margin: 0; padding: 16px; background-color: #fbfaef;}");
html.AppendLine(" table { border-collapse: separate; border-spacing: 0; width: 100%; margin-bottom: 20px; }");
html.AppendLine(" th, td { border: 1.5px solid #cce3e5; padding: 8px; text-align: left; }");
html.AppendLine(" th { background-color: #3f51b5; color: white; font-weight: 500; cursor: pointer; position: relative; text-align: center; vertical-align: middle; }");
html.AppendLine(" tr:nth-child(odd) { background-color: #f5fbef; }");
html.AppendLine(" tr:nth-child(even) { background-color: #f2faea; }");
html.AppendLine(" tr:hover { background-color: #cadbb8; transition: background-color 0.2s ease; }");
// 修改排序指示器样式,确保不影响表头文本对齐
html.AppendLine(" th::after { content: ''; display: block; position: absolute; right: 10px; top: 50%; transform: translateY(-50%); width: 0; height: 0; opacity: 0; transition: opacity 0.2s ease; }");
html.AppendLine(" th.sort-asc::after, th.sort-desc::after { opacity: 1; }");
html.AppendLine(" th.sort-asc::after { border-left: 5px solid transparent; border-right: 5px solid transparent; border-bottom: 5px solid white; }");
html.AppendLine(" th.sort-desc::after { border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 5px solid white; }");
// 改进的表格容器和固定表头样式
html.AppendLine(" .table-container { position: relative; max-height: 80vh; overflow-y: auto; border: 1px solid ##cce3e5; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.05); }");
html.AppendLine(" th, td { border: 1.5px solid #cce3e5; padding: 8px; text-align: left; }");
html.AppendLine(" .sticky-header { position: sticky; top: 0; z-index: 100; }");
html.AppendLine(" .sticky-header th { ");
html.AppendLine(" position: sticky; ");
html.AppendLine(" top: 0; ");
html.AppendLine(" background-color: #59a2ab; ");
html.AppendLine(" z-index: 100; ");
html.AppendLine(" border-width: 0; ");
html.AppendLine(" outline: 1.5px solid #cce3e5; ");
html.AppendLine(" text-align: center; ");
html.AppendLine(" vertical-align: middle; ");
html.AppendLine(" }");
html.AppendLine(" .sticky-header th:first-child { ");
html.AppendLine(" border-top-left-radius: 8px; ");
html.AppendLine(" }");
html.AppendLine(" .sticky-header th:last-child { ");
html.AppendLine(" border-top-right-radius: 8px; ");
html.AppendLine(" }");
html.AppendLine(" .sticky-header::after {");
html.AppendLine(" content: '';");
html.AppendLine(" position: absolute;");
html.AppendLine(" left: 0;");
html.AppendLine(" right: 0;");
html.AppendLine(" top: 0;");
html.AppendLine(" height: 100%;");
html.AppendLine(" pointer-events: none;");
html.AppendLine(" z-index: 99;");
html.AppendLine(" }");
html.AppendLine(" tbody tr:first-child td { border-top-color: transparent; }");
html.AppendLine(" tbody tr:last-child td:first-child { border-bottom-left-radius: 8px; }");
html.AppendLine(" tbody tr:last-child td:last-child { border-bottom-right-radius: 8px; }");
html.AppendLine(" .table-container table { margin-bottom: 0; box-shadow: 0 2px 5px rgba(0,0,0,0.05); }");
html.AppendLine(" </style>");
html.AppendLine(" <script>");
html.AppendLine(@"
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('th').forEach(function(th) {
th.removeAttribute('onclick');
th.addEventListener('click', function() {
const table = this.closest('table');
const columnIndex = Array.from(this.parentNode.children).indexOf(this);
const sortType = this.getAttribute('data-sort-type') || 'string';
sortTable(table, columnIndex, sortType);
});
});
});
function getCellValue(row, columnIndex, sortType) {
try {
if (!row || !row.cells || columnIndex >= row.cells.length) {
return sortType === 'number' || sortType === 'date' ? 0 : '';
}
const cell = row.cells[columnIndex];
if (!cell) return sortType === 'number' || sortType === 'date' ? 0 : '';
// 优先使用data-sort属性值
const sortValue = cell.getAttribute('data-sort');
if (sortValue !== null) {
return sortType === 'number' || sortType === 'date' ? parseFloat(sortValue) : sortValue;
}
const value = cell.textContent ? cell.textContent.trim() : '';
// 根据排序类型转换值
if (sortType === 'number') {
// 提取数字部分
const numMatch = value.match(/[\d\.]+/);
return numMatch ? parseFloat(numMatch[0]) : 0;
} else if (sortType === 'date') {
// 修改日期解析逻辑,优先处理 yyyy-MM-dd 格式
if (!value) return 0;
// 尝试解析 yyyy-MM-dd 格式
const dateOnlyMatch = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (dateOnlyMatch) {
const year = parseInt(dateOnlyMatch[1]);
const month = parseInt(dateOnlyMatch[2]) - 1; // 月份从0开始
const day = parseInt(dateOnlyMatch[3]);
return new Date(year, month, day).getTime();
}
// 尝试解析标准日期时间格式 yyyy-MM-dd HH:mm:ss
const dateTimeMatch = value.match(/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/);
if (dateTimeMatch) {
const year = parseInt(dateTimeMatch[1]);
const month = parseInt(dateTimeMatch[2]) - 1; // 月份从0开始
const day = parseInt(dateTimeMatch[3]);
const hour = parseInt(dateTimeMatch[4]);
const minute = parseInt(dateTimeMatch[5]);
const second = parseInt(dateTimeMatch[6]);
return new Date(year, month, day, hour, minute, second).getTime();
}
// 如果无法解析尝试直接使用Date构造函数
return new Date(value).getTime() || 0;
} else if (sortType === 'time') {
// 处理时间格式(小时、分钟、秒)
let seconds = 0;
if (value.includes('小时')) {
const hoursMatch = value.match(/(\d+)小时/);
if (hoursMatch) {
seconds += parseInt(hoursMatch[1]) * 3600;
}
}
if (value.includes('分钟')) {
const minutesMatch = value.match(/(\d+)分钟/);
if (minutesMatch) {
seconds += parseInt(minutesMatch[1]) * 60;
}
}
if (value.includes('秒')) {
const secondsMatch = value.match(/([\d\.]+)秒/);
if (secondsMatch) {
seconds += parseFloat(secondsMatch[1]);
}
}
return seconds;
}
return value;
} catch (e) {
console.error('获取单元格值时出错:', e);
return sortType === 'number' || sortType === 'date' ? 0 : '';
}
}
function sortTable(table, columnIndex, sortType) {
let loadingDiv = null;
let loadingTimer = null;
try {
if (!table) return;
const tbody = table.querySelector('tbody');
if (!tbody) return;
// 创建排序中的提示,但不立即显示
loadingDiv = document.createElement('div');
loadingDiv.style.position = 'fixed';
loadingDiv.style.top = '50%';
loadingDiv.style.left = '50%';
loadingDiv.style.transform = 'translate(-50%, -50%)';
loadingDiv.style.padding = '20px';
loadingDiv.style.background = 'rgba(0,0,0,0.7)';
loadingDiv.style.color = 'white';
loadingDiv.style.borderRadius = '5px';
loadingDiv.style.zIndex = '1000';
loadingDiv.textContent = '排序中,请稍候...';
// 设置延迟显示提示只有排序超过500毫秒才显示
loadingTimer = setTimeout(function() {
document.body.appendChild(loadingDiv);
}, 1000);
// 使用setTimeout让UI有机会更新
setTimeout(function() {
try {
// 保存汇总行
const summaryRows = Array.from(tbody.querySelectorAll('tr.ignore-sort') || []);
// 获取所有行并创建映射
const allRows = Array.from(tbody.querySelectorAll('tr') || []);
if (!allRows.length) {
clearTimeout(loadingTimer);
if (loadingDiv && loadingDiv.parentNode) {
document.body.removeChild(loadingDiv);
}
return;
}
// 首先标记所有行
for (let i = 0; i < allRows.length; i++) {
if (allRows[i]) {
allRows[i].setAttribute('data-original-index', i.toString());
}
}
// 获取需要排序的行(排除汇总行和子行)
const rows = [];
for (let i = 0; i < allRows.length; i++) {
const row = allRows[i];
if (row && row.classList &&
!row.classList.contains('ignore-sort') &&
!row.classList.contains('sub-row')) {
rows.push(row);
}
}
// 创建行和其对应的附属行的映射
const rowPairs = [];
for (let i = 0; i < rows.length; i++) {
try {
const row = rows[i];
if (!row || !row.getAttribute) continue;
const originalIndexStr = row.getAttribute('data-original-index');
if (!originalIndexStr) continue;
const originalIndex = parseInt(originalIndexStr);
if (isNaN(originalIndex)) continue;
// 安全地获取下一行,确保它存在
let nextRow = null;
if (originalIndex + 1 < allRows.length) {
nextRow = allRows[originalIndex + 1];
}
// 安全地检查nextRow是否存在且是否有classList属性
if (nextRow && nextRow.classList &&
typeof nextRow.classList.contains === 'function' &&
nextRow.classList.contains('sub-row')) {
rowPairs.push({main: row, sub: nextRow});
} else {
rowPairs.push({main: row, sub: null});
}
} catch (e) {
console.error('创建行对时出错:', e);
continue;
}
}
// 确定排序方向
let sortDirection = 'asc';
const headerCells = table.querySelectorAll('th');
if (!headerCells || columnIndex >= headerCells.length) {
if (loadingDiv && loadingDiv.parentNode) {
document.body.removeChild(loadingDiv);
}
return;
}
const headerCell = headerCells[columnIndex];
if (!headerCell || !headerCell.classList) {
if (loadingDiv && loadingDiv.parentNode) {
document.body.removeChild(loadingDiv);
}
return;
}
// 如果已经按这列排序,则切换方向
if (headerCell.classList.contains('sort-asc')) {
sortDirection = 'desc';
} else if (headerCell.classList.contains('sort-desc')) {
sortDirection = 'asc';
}
// 清除所有表头的排序指示器
for (let i = 0; i < headerCells.length; i++) {
const th = headerCells[i];
if (th && th.classList) {
th.classList.remove('sort-asc', 'sort-desc');
}
}
// 添加新的排序指示器
headerCell.classList.add('sort-' + sortDirection);
// 特殊处理耗时列
const isTimeColumn = headerCell.textContent && headerCell.textContent.trim() === '任务耗时';
const actualSortType = isTimeColumn ? 'time' : sortType;
// 排序行对 - 使用稳定的排序算法
rowPairs.sort((pairA, pairB) => {
try {
// 确保main对象存在
if (!pairA || !pairA.main || !pairB || !pairB.main) {
return 0;
}
const valueA = getCellValue(pairA.main, columnIndex, actualSortType);
const valueB = getCellValue(pairB.main, columnIndex, actualSortType);
let result;
if (actualSortType === 'number' || actualSortType === 'date' || actualSortType === 'time') {
result = sortDirection === 'asc' ? valueA - valueB : valueB - valueA;
} else {
result = sortDirection === 'asc'
? String(valueA).localeCompare(String(valueB), 'zh-CN')
: String(valueB).localeCompare(String(valueA), 'zh-CN');
}
// 如果值相等,保持原始顺序(稳定排序)
if (result === 0) {
const indexA = parseInt(pairA.main.getAttribute('data-original-index') || '0');
const indexB = parseInt(pairB.main.getAttribute('data-original-index') || '0');
return indexA - indexB;
}
return result;
} catch (e) {
console.error('排序比较时出错:', e);
return 0;
}
});
// 创建文档片段以提高性能
const fragment = document.createDocumentFragment();
// 先添加排序后的数据行和附属行
for (let i = 0; i < rowPairs.length; i++) {
const pair = rowPairs[i];
// 确保main对象存在
if (pair && pair.main) {
fragment.appendChild(pair.main);
// 确保sub对象存在
if (pair.sub) {
fragment.appendChild(pair.sub);
}
}
}
// 最后添加汇总行
for (let i = 0; i < summaryRows.length; i++) {
const row = summaryRows[i];
if (row) {
fragment.appendChild(row);
}
}
// 清空tbody
while (tbody.firstChild) {
tbody.removeChild(tbody.firstChild);
}
// 一次性添加所有行
tbody.appendChild(fragment);
} catch (error) {
console.error('排序过程中发生错误:', error);
alert('排序过程中发生错误: ' + error.message);
} finally {
// 清除定时器并移除加载提示
clearTimeout(loadingTimer);
if (loadingDiv && loadingDiv.parentNode) {
document.body.removeChild(loadingDiv);
}
}
}, 50); // 短暂延迟让UI更新
} catch (error) {
console.error('排序初始化时发生错误:', error);
// 清除定时器并确保加载提示被移除
clearTimeout(loadingTimer);
if (loadingDiv && loadingDiv.parentNode) {
document.body.removeChild(loadingDiv);
}
}
}");
html.AppendLine(" </script>");
html.AppendLine("</head>");
html.AppendLine("<body>");
// 修改 colspan 计算逻辑
int colspan = colConfigs.Length;
if (actionItems.Count > 0)
{
colspan = colspan + col2Configs.Length;
// 按时间分组考虑每天凌晨4点为新的一天
var groupedByCustomDay = actionItems.GroupBy(item => GetCustomDay(item.Time))
.OrderBy(group => group.Key)
.Reverse().ToList();
html.AppendLine("<h2>按日摩拉收益统计</h2>");
html.AppendLine("<div class=\"sticky-table\">");
html.AppendLine("<table>");
html.AppendLine(" <thead>");
html.AppendLine(" <tr class=\"sticky-header\">");
foreach (var item in msColConfigs)
{
html.AppendLine($" <th data-sort-type=\"{item.sortType}\">{item.name}</th>");
}
html.AppendLine(" </tr>");
html.AppendLine(" </tr>");
html.AppendLine("</thead>");
html.AppendLine("<tbody>");
foreach (var group in groupedByCustomDay)
{
//按天统计
MoraStatistics ms = new MoraStatistics();
ms.Name = group.Key.ToString("yyyy-MM-dd");
ms.ActionItems.AddRange(group.ToList());
html.AppendLine(" <tr>");
foreach (var item in msColConfigs)
{
html.AppendLine($" <td >{item.value.Invoke(ms)}</td>");
}
html.AppendLine(" </tr>");
}
html.AppendLine("</tbody>");
html.AppendLine("</table>");
html.AppendLine("</div>");
}
MoraStatistics allms = new MoraStatistics();
allms.ActionItems.AddRange(actionItems);
// 遍历每个配置组生成表格
foreach (var group in configGroups)
{
TimeSpan? timeDiff = group.EndDate - group.StartDate;
double totalSeconds = timeDiff?.TotalSeconds ?? 0;
MoraStatistics groupms = allms.GetFilterMoraStatistics(item =>
{
DateTime dt = DateTime.Parse(item.Time);
if (dt >= group.StartDate && dt <= group.EndDate)
{
return true;
}
return false;
}
);
groupms.StatisticsStart = group.StartDate;
groupms.StatisticsEnd = group.EndDate;
html.AppendLine($"<h2>配置组:{group.Name}</h2>");
html.AppendLine(
$"<h3>{group.StartDate?.ToString("yyyy-MM-dd HH:mm:ss")}-{group.EndDate?.ToString("yyyy-MM-dd HH:mm:ss")}</h3>");
html.AppendLine($"<h3>耗时{ConvertSecondsToTime(totalSeconds)}</h3>");
html.AppendLine("<div class=\"sticky-table\">");
html.AppendLine("<table>");
html.AppendLine(" <thead>");
html.AppendLine(" <tr class=\"sticky-header\">");
foreach (var col in colConfigs)
{
html.AppendLine($" <th data-sort-type=\"{col.sortType}\">{col.name}</th>");
}
if (actionItems.Count > 0)
{
foreach (var col in col2Configs)
{
html.AppendLine($" <th data-sort-type=\"{col.sortType}\">{col.name}</th>");
}
}
html.AppendLine(" </tr>");
html.AppendLine(" </thead>");
html.AppendLine(" <tbody>");
// 合并所有任务的 Picks
Dictionary<string, int> mergedPicks = new Dictionary<string, int>();
foreach (var task in group.ConfigTaskList)
{
foreach (var pick in task.Picks)
{
if (!mergedPicks.ContainsKey(pick.Key))
{
mergedPicks[pick.Key] = 0;
}
mergedPicks[pick.Key] += pick.Value;
}
// 任务行
timeDiff = task.EndDate - task.StartDate;
totalSeconds = timeDiff?.TotalSeconds ?? 0;
html.AppendLine(" <tr>");
// 修改第一列(名称列)的样式
for (int i = 0; i < colConfigs.Length; i++)
{
var item = colConfigs[i];
if (i == 0) // 名称列
{
html.AppendLine(
$" <td class=\"main-row-name\" rowspan=\"2\">{item.value.Invoke(task)}</td>");
}
else
{
html.AppendLine($" <td>{item.value.Invoke(task)}</td>");
}
}
if (actionItems.Count > 0)
{
MoraStatistics configTaskMs = groupms.GetFilterMoraStatistics(item =>
{
DateTime dt = DateTime.Parse(item.Time);
if (dt >= task.StartDate && dt <= task.EndDate)
{
return true;
}
return false;
}
);
configTaskMs.StatisticsStart = task.StartDate;
configTaskMs.StatisticsEnd = task.EndDate;
foreach (var item in col2Configs)
{
html.AppendLine($" <td>{item.value.Invoke(configTaskMs)}</td>");
}
}
html.AppendLine(" </tr>");
// 添加附属行显示该任务的拾取物
var taskSortedPicks = task.Picks.OrderByDescending(p => p.Value)
.Select(p => $"{p.Key} ({p.Value})");
html.AppendLine(" <tr class=\"sub-row\">");
// 跳过第一列因为已经在主行中使用了rowspan="2"
// 计算实际的 colspan 值
int actualColspan = colConfigs.Length - 1;
if (actionItems.Count > 0)
{
actualColspan += col2Configs.Length;
}
html.AppendLine(
$" <td colspan=\"{actualColspan}\">拾取物: {string.Join(", ", taskSortedPicks)}</td>");
html.AppendLine(" </tr>");
}
// 按 Value 倒序排列 Picks
var sortedPicks = mergedPicks.OrderByDescending(p => p.Value)
.Select(p => $"{p.Key} ({p.Value})");
// 修改拾取物行添加 ignore-sort 类
html.AppendLine(" <tr class=\"ignore-sort\">");
html.AppendLine($" <td colspan=\"{colspan}\">拾取物: {string.Join(", ", sortedPicks)}</td>");
html.AppendLine(" </tr>");
if (actionItems.Count > 0)
{
html.AppendLine(" <tr class=\"ignore-sort\">");
html.AppendLine(
$" <td colspan=\"{colspan}\">锄地总计:{ConcatenateStrings("", groupms.SmallMonsterStatistics.ToString()) +
/*ConcatenateStrings(",最后一只小怪挂于", groupms.LastSmallTime) +*/
ConcatenateStrings(",精英怪数量:", groupms.EliteGameStatistics.ToString()) +
ConcatenateStrings(",精英详细:", groupms.EliteDetails) +
/*ConcatenateStrings(",最后一只精英挂于", groupms.LastEliteTime) +*/
ConcatenateStrings(",合计锄地摩拉:", groupms.TotalMoraKillingMonstersMora.ToString()) +
ConcatenateStrings(",每秒摩拉", (groupms.TotalMoraKillingMonstersMora / (groupms.StatisticsEnd - groupms.StatisticsStart)?.TotalSeconds ?? 0).ToString("F2"))}");
html.AppendLine(" </tr>");
}
html.AppendLine("</table>");
html.AppendLine("</div>"); // 关闭 sticky-table div
}
// HTML尾部
html.AppendLine("</body>");
html.AppendLine("</html>");
return html.ToString();
}
public static void WriteConfigFile(LogParseConfig config)
{
var options = new JsonSerializerOptions
{
WriteIndented = true // 启用格式化(缩进)
};
var content = JsonSerializer.Serialize(config, options);
string directoryPath = Path.GetDirectoryName(_configPath);
if (!Directory.Exists(directoryPath))
{
// 如果文件夹不存在,创建文件夹
Directory.CreateDirectory(directoryPath);
}
File.WriteAllText(_configPath, content);
}
public static LogParseConfig LoadConfig()
{
LogParseConfig? config = null;
if (File.Exists(_configPath))
{
try
{
config = JsonSerializer.Deserialize<LogParseConfig>(File.ReadAllText(_configPath)) ??
throw new NullReferenceException();
}
catch (NullReferenceException)
{
Toast.Warning("读取日志分析配置文件失败!");
config = new LogParseConfig();
}
}
else
{
config = new LogParseConfig();
}
return config;
}
}
}