Commit 8afe8a66 authored by 段廷瑞's avatar 段廷瑞

feat: 日志查看器优化

parent 41b668f8
......@@ -317,6 +317,9 @@
<Compile Include="UI\wLoginPwd.Designer.cs">
<DependentUpon>wLoginPwd.cs</DependentUpon>
</Compile>
<Compile Include="UI\wLogPathDetector.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="UI\wLogViewer.cs">
<SubType>Form</SubType>
</Compile>
......
using System;
using System.IO;
using System.Linq;
using System.Windows.Forms;
namespace Tools.UI
{
/// <summary>
/// 日志路径探测器 — 选择搜索根目录
/// </summary>
public partial class wLogPathDetector : Form
{
public string SelectedPath { get; private set; }
TextBox txtPath;
Button btnBrowse;
Button btnConfirm;
Button btnCancel;
Label lblInfo;
public wLogPathDetector()
{
InitializeComponent();
SetDefaultPath();
}
void SetDefaultPath()
{
// 优先 D:\, 否则 E:\, 否则第一个可用驱动器
var drives = DriveInfo.GetDrives().Where(d => d.IsReady).ToList();
foreach (var drive in drives)
{
if (drive.Name.StartsWith("D:", StringComparison.OrdinalIgnoreCase))
{
txtPath.Text = drive.RootDirectory.FullName;
return;
}
}
foreach (var drive in drives)
{
if (drive.Name.StartsWith("E:", StringComparison.OrdinalIgnoreCase))
{
txtPath.Text = drive.RootDirectory.FullName;
return;
}
}
var first = drives.FirstOrDefault();
if (first != null)
txtPath.Text = first.RootDirectory.FullName;
}
void InitializeComponent()
{
this.Text = "探测日志路径";
this.Size = new System.Drawing.Size(500, 150);
this.FormBorderStyle = FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.StartPosition = FormStartPosition.CenterParent;
lblInfo = new Label
{
Text = "选择搜索根目录:",
Location = new System.Drawing.Point(12, 15),
AutoSize = true
};
txtPath = new TextBox
{
Location = new System.Drawing.Point(12, 40),
Width = 370
};
btnBrowse = new Button
{
Text = "浏览...",
Location = new System.Drawing.Point(390, 38),
Width = 80
};
btnBrowse.Click += (s, e) =>
{
using (var dlg = new FolderBrowserDialog
{
Description = "选择搜索根目录",
RootFolder = Environment.SpecialFolder.MyComputer,
SelectedPath = txtPath.Text
})
{
if (dlg.ShowDialog() == DialogResult.OK)
txtPath.Text = dlg.SelectedPath;
}
};
btnConfirm = new Button
{
Text = "开始探测",
Location = new System.Drawing.Point(310, 75),
Width = 80
};
btnConfirm.Click += (s, e) =>
{
if (!Directory.Exists(txtPath.Text))
{
MessageBox.Show("目录不存在,请重新选择。", "提示",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
SelectedPath = txtPath.Text;
this.DialogResult = DialogResult.OK;
this.Close();
};
btnCancel = new Button
{
Text = "取消",
Location = new System.Drawing.Point(398, 75),
Width = 72
};
btnCancel.Click += (s, e) =>
{
this.DialogResult = DialogResult.Cancel;
this.Close();
};
this.Controls.AddRange(new Control[] { lblInfo, txtPath, btnBrowse, btnConfirm, btnCancel });
}
}
}
......@@ -34,6 +34,8 @@
this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
this.toolStripLabel1 = new System.Windows.Forms.ToolStripLabel();
this.tsbSelectPath = new System.Windows.Forms.ToolStripButton();
this.tsbDetectPath = new System.Windows.Forms.ToolStripButton();
this.tsbRemovePath = new System.Windows.Forms.ToolStripButton();
this.listBox1 = new System.Windows.Forms.ListBox();
this.textBox1 = new System.Windows.Forms.TextBox();
this.splitContainer1 = new System.Windows.Forms.SplitContainer();
......@@ -54,7 +56,9 @@
this.toolStripSeparator1,
this.toolStripLabel1,
this.tscbLogPath,
this.tsbSelectPath});
this.tsbSelectPath,
this.tsbDetectPath,
this.tsbRemovePath});
this.toolStrip1.Location = new System.Drawing.Point(0, 0);
this.toolStrip1.Name = "toolStrip1";
this.toolStrip1.Size = new System.Drawing.Size(695, 25);
......@@ -85,13 +89,28 @@
this.toolStripLabel1.Text = "日志路径:";
//
// tsbSelectPath
//
//
this.tsbSelectPath.Image = global::Tools.Properties.Resources.文件夹;
this.tsbSelectPath.ImageTransparentColor = System.Drawing.Color.Magenta;
this.tsbSelectPath.Name = "tsbSelectPath";
this.tsbSelectPath.Size = new System.Drawing.Size(76, 22);
this.tsbSelectPath.Text = "选择路径";
//
//
// tsbDetectPath
//
this.tsbDetectPath.Image = global::Tools.Properties.Resources.文件夹;
this.tsbDetectPath.ImageTransparentColor = System.Drawing.Color.Magenta;
this.tsbDetectPath.Name = "tsbDetectPath";
this.tsbDetectPath.Size = new System.Drawing.Size(76, 22);
this.tsbDetectPath.Text = "探测路径";
//
// tsbRemovePath
//
this.tsbRemovePath.Name = "tsbRemovePath";
this.tsbRemovePath.Size = new System.Drawing.Size(28, 22);
this.tsbRemovePath.Text = "×";
this.tsbRemovePath.ToolTipText = "移除当前路径";
//
// listBox1
//
this.listBox1.Dock = System.Windows.Forms.DockStyle.Fill;
......@@ -173,6 +192,8 @@
private System.Windows.Forms.ToolStrip toolStrip1;
private System.Windows.Forms.ToolStripLabel toolStripLabel1;
private System.Windows.Forms.ToolStripButton tsbSelectPath;
private System.Windows.Forms.ToolStripButton tsbDetectPath;
private System.Windows.Forms.ToolStripButton tsbRemovePath;
private System.Windows.Forms.ListBox listBox1;
private System.Windows.Forms.TextBox textBox1;
private System.Windows.Forms.SplitContainer splitContainer1;
......
......@@ -19,7 +19,28 @@ namespace Tools.UI
{
List<string> pathList = new List<string>();
string LogPath { get => this.tscbLogPath.Text; set => this.tscbLogPath.Text = value; }
string LogPath { get => CleanLogPath(this.tscbLogPath.Text); set => this.tscbLogPath.Text = value; }
Dictionary<string, List<string>> _detectedAllPaths = new Dictionary<string, List<string>>();
string _commonPathPrefix = null; // 公共路径前缀(用于"…"缩略显示)
/// <summary>
/// 去除路径中的类型前缀(如 "[PDM] "),还原"…"缩略前缀
/// </summary>
string CleanLogPath(string text)
{
if (string.IsNullOrEmpty(text)) return text;
var pathPart = text;
if (pathPart.StartsWith("["))
{
var idx = pathPart.IndexOf("] ");
if (idx > 0) pathPart = pathPart.Substring(idx + 2);
}
// 还原"…"缩略前缀
if (pathPart.StartsWith("…") && !string.IsNullOrEmpty(_commonPathPrefix))
return _commonPathPrefix + pathPart.Substring(1);
return pathPart;
}
public wLogViewer()
{
......@@ -29,21 +50,33 @@ namespace Tools.UI
//窗体:加载
private void wLogViewer_Load(object sender, EventArgs e)
{
//初始化编码选择下拉框
//初始化编码选择下拉框(常用编码)
var list = new List<string> {
Encoding.Default.HeaderName.ToUpper(),
Encoding.UTF8.HeaderName.ToUpper(),
"GBK",
"GB2312",
Encoding.Default.HeaderName.ToUpper(),
Encoding.Unicode.HeaderName.ToUpper(),
Encoding.BigEndianUnicode.HeaderName.ToUpper(),
};
this.tscbEncoding.Items.AddRange(list.Distinct().ToArray());
this.tscbEncoding.SelectedIndex = 0;
//初始化文件路径下拉框
pathList = Utils.GetJsonConfig<List<string>>();
this.tscbLogPath.Items.AddRange(pathList.Distinct().ToArray());
foreach (var p in pathList)
{
if (!this.tscbLogPath.Items.Contains(p))
this.tscbLogPath.Items.Add(p);
}
//初始化探测路径
LoadDetectedPaths();
}
//窗体:显示完成
private void wLogViewer_Shown(object sender, EventArgs e)
{
this.tsbSelectPath.Click += TsbSelectPath_Click;
this.tsbDetectPath.Click += TsbDetectPath_Click;
this.tsbRemovePath.Click += TsbRemovePath_Click;
this.tscbLogPath.SelectedIndexChanged += TscbLogPath_SelectedIndexChanged;
this.tscbEncoding.SelectedIndexChanged += TscbEncoding_SelectedIndexChanged;
this.listBox1.SelectedIndexChanged += ListBox1_SelectedIndexChanged;
......@@ -62,24 +95,28 @@ namespace Tools.UI
this.tscbLogPath.Items.Add(folder);
//刷新下拉框文本
this.LogPath = folder;
//保存路径记录
Utils.SaveJsonConfig(this.tscbLogPath.Items.Cast<string>().ToList());
//保存路径记录(仅保存原始路径,不含类型前缀)
var rawPaths = this.tscbLogPath.Items.Cast<string>()
.Where(p => !p.StartsWith("["))
.ToList();
Utils.SaveJsonConfig(rawPaths);
}
}
bool _suppressPathChange = false; // 路径变更抑制标志(刷新下拉框时使用)
/// <summary>
/// 路径文本改变时,刷新列表框
/// </summary>
private void TscbLogPath_SelectedIndexChanged(object sender, EventArgs e)
{
if (_suppressPathChange) return;
this.InitListBox();
}
/// <summary>
/// 编码切换:重新解析日志文件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void TscbEncoding_SelectedIndexChanged(object sender, EventArgs e)
{
if (_suppressEncodingChange) return;
this.OpenLogFile();
}
/// <summary>
......@@ -115,8 +152,10 @@ namespace Tools.UI
}
}
int _scanCount = 0; // 探测扫描目录计数
Task _task = null; //当前任务
CancellationTokenSource _tokenSource = new CancellationTokenSource(); //取消令牌
bool _suppressEncodingChange = false; //编码变更抑制标志(自动探测时使用)
/// <summary>
/// 打开日志文件
/// </summary>
......@@ -125,13 +164,27 @@ namespace Tools.UI
var fileName = this.listBox1.SelectedItem?.ToString();
if (string.IsNullOrEmpty(fileName)) return;
//生成文件路径
var logFileName = $@"{this.LogPath}\{fileName}";
if (!File.Exists(logFileName)) return;
//自动探测文件编码
var detectedEnc = DetectFileEncoding(logFileName);
var detectedName = detectedEnc.HeaderName.ToUpper();
//编码映射:将GBK/GB2312等系统编码统一为友好名称
if (detectedEnc.CodePage == Encoding.Default.CodePage)
detectedName = Encoding.Default.HeaderName.ToUpper();
_suppressEncodingChange = true;
if (!this.tscbEncoding.Items.Contains(detectedName))
this.tscbEncoding.Items.Insert(0, detectedName);
this.tscbEncoding.SelectedItem = detectedName;
_suppressEncodingChange = false;
var encode = Encoding.Default;
if (!string.IsNullOrEmpty(this.tscbEncoding.Text))
encode = Encoding.GetEncoding(this.tscbEncoding.Text);
//清空信息
this.textBox1.Text = string.Empty;
//生成文件路径
var logFileName = $@"{this.LogPath}\{fileName}";
//如果上一个任务还未结束,则取消
if (_task != null && !_task.IsCompleted)
{
......@@ -176,5 +229,343 @@ namespace Tools.UI
fs.Close();
}, token);
}
/// <summary>
/// 探测文件编码(BOM检测 + UTF-8合法性验证)
/// </summary>
Encoding DetectFileEncoding(string filePath)
{
var buffer = new byte[4096];
int bytesRead;
using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
bytesRead = fs.Read(buffer, 0, buffer.Length);
}
// 1. BOM检测
if (bytesRead >= 3 && buffer[0] == 0xEF && buffer[1] == 0xBB && buffer[2] == 0xBF)
return new UTF8Encoding(true); // UTF-8 BOM
if (bytesRead >= 2 && buffer[0] == 0xFF && buffer[1] == 0xFE)
return Encoding.Unicode; // UTF-16 LE
if (bytesRead >= 2 && buffer[0] == 0xFE && buffer[1] == 0xFF)
return Encoding.BigEndianUnicode; // UTF-16 BE
if (bytesRead >= 4 && buffer[0] == 0xFF && buffer[1] == 0xFE && buffer[2] == 0x00 && buffer[3] == 0x00)
return Encoding.UTF32;
// 2. 无BOM时,验证是否为合法UTF-8
if (IsValidUtf8(buffer, bytesRead))
return new UTF8Encoding(false); // UTF-8 without BOM
// 3. 默认系统编码(中文Windows为GBK)
return Encoding.Default;
}
/// <summary>
/// 验证字节序列是否为合法UTF-8(无替换字符、无解码异常)
/// </summary>
bool IsValidUtf8(byte[] buffer, int length)
{
try
{
var utf8Strict = Encoding.GetEncoding("UTF-8",
EncoderFallback.ExceptionFallback,
DecoderFallback.ExceptionFallback);
utf8Strict.GetString(buffer, 0, length);
return true;
}
catch (DecoderFallbackException)
{
return false;
}
}
/// <summary>
/// 探测日志路径:弹出目录选择窗口后异步递归搜索 PDM.exe / MES.exe / U6.exe 的 temp 子文件夹
/// </summary>
private async void TsbDetectPath_Click(object sender, EventArgs e)
{
using (var dlg = new wLogPathDetector())
{
if (dlg.ShowDialog() != DialogResult.OK) return;
var rootDir = dlg.SelectedPath;
// 进入扫描状态
tsbDetectPath.Enabled = false;
tsbDetectPath.Text = "扫描中…";
var prevStyle = progressBar1.Style;
progressBar1.Style = ProgressBarStyle.Marquee;
Dictionary<string, List<string>> detected = null;
try
{
var onProgress = new Progress<string>(msg =>
{
this.tsbDetectPath.Text = msg;
});
_scanCount = 0;
detected = await Task.Run(() => DetectLogPaths(rootDir, onProgress));
}
catch (Exception ex)
{
Utils.Tip($"探测过程出错:{ex.Message}");
return;
}
finally
{
// 恢复状态
tsbDetectPath.Enabled = true;
tsbDetectPath.Text = "探测路径";
progressBar1.Style = prevStyle;
}
if (detected == null || detected.Count == 0)
{
Utils.Tip($"在 {rootDir} 下未探测到 PDM/MES/U6 的日志目录。");
return;
}
// 合并到已有结果
foreach (var kvp in detected)
{
if (!_detectedAllPaths.ContainsKey(kvp.Key))
_detectedAllPaths[kvp.Key] = new List<string>();
foreach (var path in kvp.Value)
{
if (!_detectedAllPaths[kvp.Key].Contains(path))
_detectedAllPaths[kvp.Key].Add(path);
}
}
// 刷新下拉框显示
RefreshDetectedDisplay();
// 持久化
SaveDetectedPaths();
var total = detected.Values.Sum(v => v.Count);
Utils.Tip($"已探测到 {total} 个日志目录并加入列表。");
}
}
/// <summary>
/// 移除路径下拉框中当前选中的项
/// </summary>
private void TsbRemovePath_Click(object sender, EventArgs e)
{
var item = this.tscbLogPath.SelectedItem?.ToString();
if (string.IsNullOrEmpty(item))
{
Utils.Tip("请先选择要移除的路径。");
return;
}
// 探测路径:以 "[" 开头,从持久化字典中移除
if (item.StartsWith("["))
{
var path = CleanLogPath(item);
string keyToRemove = null;
foreach (var kvp in _detectedAllPaths)
{
if (kvp.Value.Remove(path))
{
if (kvp.Value.Count == 0)
keyToRemove = kvp.Key;
break;
}
}
if (keyToRemove != null)
_detectedAllPaths.Remove(keyToRemove);
RefreshDetectedDisplay();
SaveDetectedPaths();
}
else
{
// 手动添加的路径:从下拉框和持久化列表中移除
this.tscbLogPath.Items.Remove(item);
pathList.Remove(item);
Utils.SaveJsonConfig(pathList);
}
// 若下拉框还有项则选中第一个
if (this.tscbLogPath.Items.Count > 0)
this.tscbLogPath.SelectedIndex = 0;
}
/// <summary>
/// 在指定根目录下递归搜索 PDM.exe / MES.exe / U6.exe,返回其 temp 目录
/// </summary>
Dictionary<string, List<string>> DetectLogPaths(string rootDir, IProgress<string> onProgress = null)
{
var result = new Dictionary<string, List<string>>();
var targets = new Dictionary<string, string> {
{ "PDM.exe", "PDM" },
{ "MES.exe", "MES" },
{ "U6.exe", "U6" }
};
foreach (var target in targets)
{
var tempDirs = new List<string>();
FindExeTempDirs(rootDir, target.Key, tempDirs, onProgress);
if (tempDirs.Count > 0)
result[target.Value] = tempDirs;
}
return result;
}
/// <summary>
/// 递归搜索指定目录下包含目标 exe 且存在 temp 子目录的路径
/// </summary>
void FindExeTempDirs(string dir, string exeName, List<string> results, IProgress<string> onProgress = null)
{
try
{
// 进度报告(每100个目录报告一次)
var count = Interlocked.Increment(ref _scanCount);
if (count % 100 == 0 && onProgress != null)
onProgress.Report($"扫描中… {count} 个目录");
// 检查当前目录
var exePath = Path.Combine(dir, exeName);
if (File.Exists(exePath))
{
var tempDir = Path.Combine(dir, "temp");
if (Directory.Exists(tempDir) && !results.Contains(tempDir))
results.Add(tempDir);
}
// 递归子目录
foreach (var subDir in Directory.EnumerateDirectories(dir))
{
try
{
// 跳过系统/隐藏目录以提升性能
var attr = File.GetAttributes(subDir);
if ((attr & FileAttributes.System) == FileAttributes.System) continue;
}
catch { }
FindExeTempDirs(subDir, exeName, results, onProgress);
}
}
catch (UnauthorizedAccessException) { }
catch (DirectoryNotFoundException) { }
}
/// <summary>
/// 计算路径列表的最长公共前缀(用于"…"缩略显示)
/// </summary>
string ComputeCommonPrefix(IEnumerable<string> paths)
{
var list = paths.Where(p => !string.IsNullOrEmpty(p)).ToList();
if (list.Count < 2) return null;
var first = list[0];
var prefixLen = 0;
for (int i = 0; i < first.Length; i++)
{
var c = first[i];
if (list.Any(p => p.Length <= i || p[i] != c))
break;
prefixLen = i + 1;
}
if (prefixLen <= 3) return null; // 太短不缩略(如 "D:\")
// 截断到最后一个目录分隔符,保持显示整洁
var prefix = first.Substring(0, prefixLen);
var lastSep = prefix.LastIndexOf(Path.DirectorySeparatorChar);
if (lastSep > 3)
prefix = prefix.Substring(0, lastSep + 1);
return prefix;
}
/// <summary>
/// 刷新下拉框中探测路径的显示(移除旧的,按公共前缀缩略后重新添加)
/// </summary>
void RefreshDetectedDisplay()
{
_suppressPathChange = true;
// 移除旧的探测项
var toRemove = this.tscbLogPath.Items.Cast<string>()
.Where(p => p.StartsWith("["))
.ToList();
foreach (var item in toRemove)
this.tscbLogPath.Items.Remove(item);
// 计算公共前缀
var allPaths = _detectedAllPaths.Values.SelectMany(v => v).ToList();
_commonPathPrefix = ComputeCommonPrefix(allPaths);
// 添加探测路径(缩略显示)
foreach (var kvp in _detectedAllPaths)
{
foreach (var path in kvp.Value.OrderBy(p => p))
{
var displayPath = FormatDetectedPath(kvp.Key, path);
this.tscbLogPath.Items.Add(displayPath);
}
}
_suppressPathChange = false;
}
/// <summary>
/// 格式化探测路径:类型前缀 + 缩略路径
/// </summary>
string FormatDetectedPath(string type, string fullPath)
{
if (!string.IsNullOrEmpty(_commonPathPrefix) && fullPath.StartsWith(_commonPathPrefix))
{
var rest = fullPath.Substring(_commonPathPrefix.Length);
return $"[{type}] …{rest}";
}
return $"[{type}] {fullPath}";
}
/// <summary>
/// 持久化探测到的路径
/// </summary>
void SaveDetectedPaths()
{
var saved = Utils.GetJsonConfig<DetectedLogPaths>();
saved.Paths = _detectedAllPaths.ToDictionary(
kvp => kvp.Key,
kvp => kvp.Value.Distinct().OrderBy(p => p).ToList()
);
Utils.SaveJsonConfig(saved);
}
/// <summary>
/// 加载已持久化的探测路径到下拉框
/// </summary>
void LoadDetectedPaths()
{
var saved = Utils.GetJsonConfig<DetectedLogPaths>();
if (saved.Paths == null || saved.Paths.Count == 0) return;
_detectedAllPaths = saved.Paths.ToDictionary(
kvp => kvp.Key,
kvp => kvp.Value.Where(Directory.Exists).Distinct().ToList()
);
// 清理不存在的条目
foreach (var key in _detectedAllPaths.Keys.ToList())
{
if (_detectedAllPaths[key].Count == 0)
_detectedAllPaths.Remove(key);
}
if (_detectedAllPaths.Count > 0)
RefreshDetectedDisplay();
}
/// <summary>
/// 探测路径持久化容器(支持同类型多实例)
/// </summary>
class DetectedLogPaths
{
public Dictionary<string, List<string>> Paths { get; set; } = new Dictionary<string, List<string>>();
}
}
}
......@@ -400,7 +400,7 @@ namespace Tools.UI
this.Controls.Add(this.tabControl);
this.MinimumSize = new System.Drawing.Size(640, 420);
this.Name = "wTextFormatter";
this.Text = "文本格式化";
this.Text = "格式化工具";
this.tabControl.ResumeLayout(false);
this.tabSql.ResumeLayout(false);
this.tlpSql.ResumeLayout(false);
......
......@@ -45,12 +45,11 @@ namespace Tools
this.CreateModule(Tools_uni, typeof(UI.wTaskBoard), "任务看板", "tr:任务看板的简易化工具,免除翻页困扰"),
this.CreateModule(Tools_uni, typeof(UI.wLoginPwd), "密码解析", "tr:用于登陆密码的解密加密"),
this.CreateModule(Tools_uni, typeof(WindowsSearch.FrmSearch), "文件搜索", "lxj:用于搜索文件内容"),
this.CreateModule(Tools_uni, typeof(UI.wTextFormatter), "文本格式化", "tr:SQL和JSON文本格式化工具"),
//-------------------------------
this.CreateModule(Tools_imp, typeof(UpdateExecuter.frmMain), "更新执行工具", "lxj:用于客户更新和执行SQL脚本"),
this.CreateModule(Tools_imp, typeof(UI.wEmptyTable), "清空表", "tr:输入主表、自动清空关联表的工具"),
this.CreateModule(Tools_imp, typeof(UI.wExcelExportSetting), "Excel导出设置", "tr:Excel导出配置图形化版本"),
this.CreateModule(Tools_imp, typeof(UI.wLogViewer), "日志查看器", "tr:可以方便的查看系统日志"),
this.CreateModule(Tools_imp, typeof(UI.wTextFormatter), "格式化工具", "tr:SQL和JSON文本格式化工具"),
this.CreateModule(Tools_imp, typeof(DotNetVersionChecker.FrmVersion), "系统检测", "lxj:检测操作系统运行环境"),
//-------------------------------
this.CreateModule(Tools_dev, typeof(Ftt.PatchPack.FrmMain), "自动打包工具", "lxj:用于CS后端程序自动打包成压缩包"),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment