| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100 |
- 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
- {
- /// <summary>
- /// 全局界面点击轨迹日志(M8 操作日志的「点击层」)。
- /// App 启动时调用 <see cref="Install"/> 一次:注册全局 Button 点击拦截,
- /// 自动把每次按钮点击记成一条 module="界面点击" 的操作日志,operation = "所在页面 · 按钮文字"。
- ///
- /// 设计要点:
- /// - 开发阶段全量记录(含导航点击),便于排障;
- /// - 上线时用 M8 §10 配置把模块「界面点击」整层关闭(OperationLogOptions.IsModuleEnabled),无需改代码;
- /// - 与逐动作埋点(OperationLogger.Begin,带 输入/输出/结果/报错)互补:
- /// 本层只记「点了什么」(意图/轨迹),动作层记「结果如何」(定位失败用);
- /// - 一处注册、全 App 生效,无需各页逐个埋点;全 try 兜底,绝不影响 UI。
- /// - 跳过控件内部机件与敏感按键:滚动条 RepeatButton、软键盘按键(避免逐键记录+泄露密码)。
- /// </summary>
- 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;
- }
- }
- }
|