using System; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Media; using Aivfo.OperationLog; namespace ivf_tl_Manage.Helpers { /// /// 全局界面点击轨迹日志(M8 操作日志的「点击层」)。 /// App 启动时调用 一次:注册全局 Button 点击拦截, /// 自动把每次按钮点击记成一条 module="界面点击" 的操作日志,operation = "所在页面 · 按钮文字"。 /// /// 设计要点: /// - 开发阶段全量记录(含导航点击),便于排障; /// - 上线时用 M8 §10 配置把模块「界面点击」整层关闭(OperationLogOptions.IsModuleEnabled),无需改代码; /// - 与逐动作埋点(OperationLogger.Begin,带 输入/输出/结果/报错)互补: /// 本层只记「点了什么」(意图/轨迹),动作层记「结果如何」(定位失败用); /// - 一处注册、全 App 生效,无需各页逐个埋点;全 try 兜底,绝不影响 UI。 /// - 跳过控件内部机件与敏感按键:滚动条 RepeatButton、软键盘按键(避免逐键记录+泄露密码)。 /// internal static class ClickTrailLogger { private static bool _installed; public static void Install() { if (_installed) return; _installed = true; // handledEventsToo=true:即便点击被下游处理也能拿到,确保不漏。 EventManager.RegisterClassHandler(typeof(ButtonBase), ButtonBase.ClickEvent, new RoutedEventHandler(OnAnyClick), true); } private static void OnAnyClick(object sender, RoutedEventArgs e) { try { if (!(sender is ButtonBase b)) return; if (b is RepeatButton) return; // 滚动条/数值微调机件,非用户操作 if (IsInsideSoftKeyboard(b)) return; // 软键盘按键:避免逐键记录 + 泄露密码 string label = ExtractLabel(b); if (string.IsNullOrWhiteSpace(label)) return; // 无可读标签(纯图标机件)不记 string page = FindPageName(b); OperationLogger.Log("界面点击", page + " · " + label, result: "点击"); } catch { /* 轨迹日志绝不影响 UI */ } } private static bool IsInsideSoftKeyboard(DependencyObject d) { for (int i = 0; d != null && i < 60; i++) { if (d.GetType().Name.IndexOf("SoftKeyboard", StringComparison.OrdinalIgnoreCase) >= 0) return true; d = VisualTreeHelper.GetParent(d) ?? LogicalTreeHelper.GetParent(d); } return false; } private static string FindPageName(DependencyObject d) { for (int i = 0; d != null && i < 80; i++) { if (d is UserControl || d is Window) return d.GetType().Name; d = VisualTreeHelper.GetParent(d) ?? LogicalTreeHelper.GetParent(d); } return "未知页面"; } private static string ExtractLabel(ButtonBase b) { if (b.Content is string s && !string.IsNullOrWhiteSpace(s)) return Clip(s); string t = FindFirstText(b); // 搜按钮可视子树第一个文字 if (!string.IsNullOrWhiteSpace(t)) return Clip(t); if (!string.IsNullOrWhiteSpace(b.Name)) return b.Name; if (b.ToolTip is string tip && !string.IsNullOrWhiteSpace(tip)) return Clip(tip); return null; } private static string FindFirstText(DependencyObject d) { if (d is TextBlock tb && !string.IsNullOrWhiteSpace(tb.Text)) return tb.Text; if (d is AccessText at && !string.IsNullOrWhiteSpace(at.Text)) return at.Text; int n = VisualTreeHelper.GetChildrenCount(d); for (int i = 0; i < n; i++) { string r = FindFirstText(VisualTreeHelper.GetChild(d, i)); if (!string.IsNullOrWhiteSpace(r)) return r; } return null; } private static string Clip(string s) { s = s.Trim().Replace("\r", " ").Replace("\n", " "); return s.Length > 40 ? s.Substring(0, 40) : s; } } }