928 lines
30 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using Aisu.SIM1.Face.Core;
using Aisu.SIM1.Face.Protocol;
using UnityEngine;
using UnityEngine.UI;
namespace Aisu.SIM1.Face.UI.Common
{
/// <summary>
/// FaceModule UI 公共状态。
/// </summary>
public sealed class FaceModuleUiState
{
// 公开数据字段。
public bool IsBusy;
// 公开数据字段。
public string CurrentPage;
// 公开数据字段。
public string LastResult;
// 公开数据字段。
public float Progress;
}
/// <summary>
/// 所有功能页面的基类。
/// </summary>
public abstract class FaceModulePageBase : MonoBehaviour
{
// 内部状态字段。
protected FaceModuleManager m_manager;
// 内部状态字段。
protected FaceModuleLogPanel m_logPanel;
// 内部状态字段。
protected FaceModuleLoadingView m_loadingView;
// 内部状态字段。
protected Text m_resultText;
/// <summary>
/// 初始化页面。
/// </summary>
public virtual void Init(FaceModuleManager manager, FaceModuleLogPanel logPanel, FaceModuleLoadingView loadingView)
{
m_manager = manager;
m_logPanel = logPanel;
m_loadingView = loadingView;
}
/// <summary>
/// 页面显示。
/// </summary>
public virtual void Show()
{
gameObject.SetActive(true);
}
/// <summary>
/// 页面隐藏。
/// </summary>
public virtual void Hide()
{
gameObject.SetActive(false);
}
/// <summary>
/// 由代码生成页面内容。
/// </summary>
public virtual void Build(FaceModuleUiBuilder builder)
{
}
/// <summary>
/// 执行 RequireConnected 方法。
/// </summary>
protected bool RequireConnected()
{
if (m_manager != null && m_manager.IsOpen)
{
return true;
}
SetResult("模组未连接");
return false;
}
/// <summary>
/// 执行 RequireReady 方法。
/// </summary>
protected bool RequireReady()
{
if (RequireConnected() && m_manager.IsReady)
{
return true;
}
SetResult("模组未 READY");
return false;
}
/// <summary>
/// 执行 SetResult 方法。
/// </summary>
protected void SetResult(string message)
{
if (m_resultText != null)
{
m_resultText.text = message;
}
m_logPanel?.Info(message);
}
/// <summary>
/// 执行 SetError 方法。
/// </summary>
protected void SetError(Exception exception)
{
SetResult(exception.Message);
m_logPanel?.Error(exception.Message);
Debug.LogException(exception);
}
/// <summary>
/// 执行 ReadByte 方法。
/// </summary>
protected byte ReadByte(InputField input, byte fallback)
{
return input != null && byte.TryParse(input.text, out var value) ? value : fallback;
}
/// <summary>
/// 执行 ReadInt 方法。
/// </summary>
protected int ReadInt(InputField input, int fallback)
{
return input != null && int.TryParse(input.text, out var value) ? value : fallback;
}
/// <summary>
/// 执行 ReadText 方法。
/// </summary>
protected string ReadText(InputField input)
{
return input != null ? input.text : string.Empty;
}
/// <summary>
/// 执行 RunWithButton 方法。
/// </summary>
protected void RunWithButton(Button button, Func<System.Threading.Tasks.Task> action)
{
if (button != null)
{
button.interactable = false;
}
RunAsync(async () =>
{
try
{
await action();
}
finally
{
if (button != null)
{
button.interactable = true;
}
}
});
}
/// <summary>
/// 执行 RunAsync 方法。
/// </summary>
protected async void RunAsync(Func<System.Threading.Tasks.Task> action)
{
try
{
m_loadingView?.Show("处理中...");
await action();
}
catch (Exception ex)
{
SetError(ex);
}
finally
{
m_loadingView?.Hide();
}
}
}
/// <summary>
/// 顶部状态栏。
/// </summary>
public sealed class FaceModuleStatusBar : MonoBehaviour
{
// Inspector 绑定字段。
[SerializeField] private Text m_connectionText;
// Inspector 绑定字段。
[SerializeField] private Text m_readyText;
// Inspector 绑定字段。
[SerializeField] private Text m_statusText;
// Inspector 绑定字段。
[SerializeField] private Text m_commandText;
// Inspector 绑定字段。
[SerializeField] private Text m_errorText;
// 内部状态字段。
private FaceModuleManager m_manager;
private Action<FaceResultCode, string> m_errorHandler; // Manager 错误事件回调。
#region Unity API
/// <summary>
/// 执行 Update 方法。
/// </summary>
private void Update()
{
Refresh();
}
#endregion
/// <summary>
/// 初始化状态栏。
/// </summary>
public void Init(FaceModuleManager manager)
{
if (m_manager != null && m_errorHandler != null)
{
m_manager.OnError -= m_errorHandler;
}
m_manager = manager;
if (m_manager != null)
{
m_errorHandler = (_, message) =>
{
if (m_errorText != null)
{
m_errorText.text = "错误: " + message;
}
};
m_manager.OnError += m_errorHandler;
}
Refresh();
}
private void OnDestroy()
{
if (m_manager != null && m_errorHandler != null)
{
m_manager.OnError -= m_errorHandler;
}
}
/// <summary>
/// 绑定状态栏文本。
/// </summary>
public void Bind(Text connectionText, Text readyText, Text statusText, Text commandText, Text errorText)
{
m_connectionText = connectionText;
m_readyText = readyText;
m_statusText = statusText;
m_commandText = commandText;
m_errorText = errorText;
}
/// <summary>
/// 刷新显示。
/// </summary>
public void Refresh()
{
if (m_manager == null)
{
return;
}
if (m_connectionText != null) m_connectionText.text = m_manager.IsReady ? "硬件已连接" : (m_manager.IsOpen ? "端口已打开" : "未连接");
if (m_readyText != null) m_readyText.text = m_manager.IsReady ? "READY" : "未 READY";
if (m_statusText != null) m_statusText.text = "状态: " + m_manager.CurrentStatus;
if (m_commandText != null) m_commandText.text = "命令: " + m_manager.CurrentCommand;
if (m_errorText != null && !string.IsNullOrEmpty(m_manager.LastError)) m_errorText.text = "错误: " + m_manager.LastError;
}
}
/// <summary>
/// 左侧导航栏。
/// </summary>
public sealed class FaceModuleNavigationPanel : MonoBehaviour
{
private readonly List<Button> m_buttons = new List<Button>();
// 内部状态字段。
private FaceModuleMainPanel m_mainPanel;
/// <summary>
/// 初始化导航栏。
/// </summary>
public void Init(FaceModuleMainPanel mainPanel)
{
m_mainPanel = mainPanel;
}
/// <summary>
/// 添加页面导航按钮。
/// </summary>
public void AddNavigation(string pageName, string label, FaceModuleUiBuilder builder)
{
var button = builder.Button(transform, label, () => m_mainPanel.ShowPage(pageName));
m_buttons.Add(button);
}
}
/// <summary>
/// 底部日志面板。
/// </summary>
public sealed class FaceModuleLogPanel : MonoBehaviour
{
// Inspector 绑定字段。
[SerializeField] private Text m_logText;
// Inspector 绑定字段。
[SerializeField] private ScrollRect m_scrollRect;
// Inspector 绑定字段。
[SerializeField] private Toggle m_autoScrollToggle;
private readonly List<string> m_lines = new List<string>();
private FaceModuleManager m_manager; // 当前绑定的 Manager。
private Action<FaceResultCode, string> m_errorHandler; // Manager 错误事件回调。
private Action m_readyHandler; // Manager READY 事件回调。
/// <summary>
/// 绑定日志 UI。
/// </summary>
public void Bind(Text logText, ScrollRect scrollRect, Toggle autoScrollToggle)
{
m_logText = logText;
m_scrollRect = scrollRect;
m_autoScrollToggle = autoScrollToggle;
}
/// <summary>
/// 连接 Manager 日志事件。
/// </summary>
public void Init(FaceModuleManager manager)
{
UnbindManagerEvents();
m_manager = manager;
if (manager == null)
{
return;
}
m_errorHandler = (_, message) => Error(message);
m_readyHandler = () => Info("模组 READY");
manager.OnRawLog += Raw;
manager.OnError += m_errorHandler;
manager.OnReady += m_readyHandler;
}
private void OnDestroy()
{
UnbindManagerEvents();
}
/// <summary>
/// 添加业务日志。
/// </summary>
public void Info(string message)
{
Append("INFO", message);
}
/// <summary>
/// 添加错误日志。
/// </summary>
public void Error(string message)
{
Append("ERROR", message);
}
/// <summary>
/// 添加原始收发日志。
/// </summary>
public void Raw(string message)
{
var tag = message.StartsWith("TX", StringComparison.OrdinalIgnoreCase) ? "TX" : "RX";
var body = message.Length > 3 ? message.Substring(3) : message;
Append(tag, body);
}
/// <summary>
/// 清空日志。
/// </summary>
public void Clear()
{
m_lines.Clear();
RefreshText();
}
/// <summary>
/// 保存日志到本地。
/// </summary>
public void Save()
{
var dir = Path.Combine(Application.persistentDataPath, "FaceModuleLogs");
Directory.CreateDirectory(dir);
File.WriteAllLines(Path.Combine(dir, DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".log"), m_lines);
Info("日志已保存: " + dir);
}
/// <summary>
/// 执行 Append 方法。
/// </summary>
private void Append(string tag, string message)
{
m_lines.Add($"[{DateTime.Now:HH:mm:ss.fff}][{tag}] {message}");
if (m_lines.Count > 1000)
{
m_lines.RemoveAt(0);
}
RefreshText();
}
/// <summary>
/// 执行 RefreshText 方法。
/// </summary>
private void RefreshText()
{
if (m_logText != null)
{
m_logText.text = string.Join("\n", m_lines);
}
if (m_scrollRect != null && (m_autoScrollToggle == null || m_autoScrollToggle.isOn))
{
Canvas.ForceUpdateCanvases();
m_scrollRect.verticalNormalizedPosition = 0f;
}
}
private void UnbindManagerEvents()
{
if (m_manager == null)
{
return;
}
m_manager.OnRawLog -= Raw;
if (m_errorHandler != null)
{
m_manager.OnError -= m_errorHandler;
}
if (m_readyHandler != null)
{
m_manager.OnReady -= m_readyHandler;
}
}
}
/// <summary>
/// Loading / 进度提示。
/// </summary>
public sealed class FaceModuleLoadingView : MonoBehaviour
{
// Inspector 绑定字段。
[SerializeField] private Text m_messageText;
// Inspector 绑定字段。
[SerializeField] private Slider m_progressSlider;
/// <summary>
/// 绑定控件。
/// </summary>
public void Bind(Text messageText, Slider progressSlider)
{
m_messageText = messageText;
m_progressSlider = progressSlider;
Hide();
}
/// <summary>
/// 显示 Loading。
/// </summary>
public void Show(string message)
{
gameObject.SetActive(true);
if (m_messageText != null) m_messageText.text = message;
}
/// <summary>
/// 设置进度。
/// </summary>
public void SetProgress(float value)
{
if (m_progressSlider != null) m_progressSlider.value = Mathf.Clamp01(value);
}
/// <summary>
/// 隐藏 Loading。
/// </summary>
public void Hide()
{
gameObject.SetActive(false);
}
}
/// <summary>
/// 二次确认弹窗。
/// </summary>
public sealed class FaceModuleConfirmDialog : MonoBehaviour
{
// Inspector 绑定字段。
[SerializeField] private Text m_messageText;
// Inspector 绑定字段。
[SerializeField] private Button m_confirmButton;
// Inspector 绑定字段。
[SerializeField] private Button m_cancelButton;
// 内部状态字段。
private Action m_onConfirm;
/// <summary>
/// 绑定控件。
/// </summary>
public void Bind(Text messageText, Button confirmButton, Button cancelButton)
{
m_messageText = messageText;
m_confirmButton = confirmButton;
m_cancelButton = cancelButton;
m_confirmButton.onClick.AddListener(Confirm);
m_cancelButton.onClick.AddListener(Hide);
Hide();
}
/// <summary>
/// 显示确认框。
/// </summary>
public void Show(string message, Action onConfirm)
{
m_onConfirm = onConfirm;
if (m_messageText != null) m_messageText.text = message;
gameObject.SetActive(true);
}
/// <summary>
/// 隐藏确认框。
/// </summary>
public void Hide()
{
gameObject.SetActive(false);
}
/// <summary>
/// 执行 Confirm 方法。
/// </summary>
private void Confirm()
{
Hide();
m_onConfirm?.Invoke();
}
}
/// <summary>
/// 运行时 UI 构建辅助器。
/// </summary>
public sealed class FaceModuleUiBuilder
{
// 内部状态字段。
private readonly Font m_font;
/// <summary>
/// 执行 FaceModuleUiBuilder 方法。
/// </summary>
public FaceModuleUiBuilder(Font font)
{
m_font = font ?? Resources.GetBuiltinResource<Font>("Arial.ttf");
}
/// <summary>
/// 执行 Text 方法。
/// </summary>
public Text Text(Transform parent, string text, int size = 22, TextAnchor anchor = TextAnchor.MiddleLeft)
{
var go = Create("Text", parent);
var component = go.AddComponent<Text>();
component.font = m_font;
component.text = text;
component.fontSize = size;
component.color = new Color(0.08f, 0.09f, 0.1f);
component.alignment = anchor;
component.horizontalOverflow = HorizontalWrapMode.Wrap;
component.verticalOverflow = VerticalWrapMode.Overflow;
return component;
}
/// <summary>
/// 执行 Button 方法。
/// </summary>
public Button Button(Transform parent, string label, UnityEngine.Events.UnityAction onClick)
{
var go = Create("Button_" + label, parent);
var image = go.AddComponent<Image>();
image.color = new Color(0.13f, 0.28f, 0.47f);
var button = go.AddComponent<Button>();
button.targetGraphic = image;
button.onClick.AddListener(onClick);
var text = Text(go.transform, label, 20, TextAnchor.MiddleCenter);
text.color = Color.white;
Stretch(text.rectTransform);
var layout = go.AddComponent<LayoutElement>();
layout.preferredHeight = 42;
layout.minHeight = 36;
return button;
}
/// <summary>
/// 执行 Input 方法。
/// </summary>
public InputField Input(Transform parent, string placeholder, string value = "")
{
var go = Create("Input_" + placeholder, parent);
var image = go.AddComponent<Image>();
image.color = Color.white;
var input = go.AddComponent<InputField>();
var text = Text(go.transform, value, 20);
text.color = Color.black;
Stretch(text.rectTransform, 10, 10, 4, 4);
input.textComponent = text;
var place = Text(go.transform, placeholder, 20);
place.color = new Color(0.45f, 0.48f, 0.52f);
Stretch(place.rectTransform, 10, 10, 4, 4);
input.placeholder = place;
var layout = go.AddComponent<LayoutElement>();
layout.preferredHeight = 38;
return input;
}
/// <summary>
/// 执行 Toggle 方法。
/// </summary>
public Toggle Toggle(Transform parent, string label, bool value)
{
var go = Create("Toggle_" + label, parent);
var toggle = go.AddComponent<Toggle>();
var row = go.AddComponent<HorizontalLayoutGroup>();
row.spacing = 8;
row.childControlWidth = false;
row.childControlHeight = true;
var box = Create("CheckmarkBox", go.transform);
var boxImage = box.AddComponent<Image>();
boxImage.color = Color.white;
var boxLayout = box.AddComponent<LayoutElement>();
boxLayout.preferredWidth = 28;
boxLayout.preferredHeight = 28;
var mark = Create("Checkmark", box.transform);
var markImage = mark.AddComponent<Image>();
markImage.color = new Color(0.1f, 0.5f, 0.25f);
Stretch(markImage.rectTransform, 5, 5, 5, 5);
toggle.targetGraphic = boxImage;
toggle.graphic = markImage;
toggle.isOn = value;
Text(go.transform, label, 20);
return toggle;
}
/// <summary>
/// 执行 Dropdown 方法。
/// </summary>
public Dropdown Dropdown(Transform parent, IList<string> options)
{
var go = Create("Dropdown", parent);
var image = go.AddComponent<Image>();
image.color = Color.white;
var dropdown = go.AddComponent<Dropdown>();
dropdown.captionText = Text(go.transform, options.Count > 0 ? options[0] : string.Empty, 20);
Stretch(dropdown.captionText.rectTransform, 10, 32, 4, 4);
BuildDropdownTemplate(dropdown, go.transform);
dropdown.options.Clear();
foreach (var option in options)
{
dropdown.options.Add(new Dropdown.OptionData(option));
}
var layout = go.AddComponent<LayoutElement>();
layout.preferredHeight = 38;
return dropdown;
}
private void BuildDropdownTemplate(Dropdown dropdown, Transform parent)
{
var template = Create("Template", parent);
template.SetActive(false);
var templateImage = template.AddComponent<Image>();
templateImage.color = Color.white;
var templateRect = template.GetComponent<RectTransform>();
templateRect.anchorMin = new Vector2(0, 0);
templateRect.anchorMax = new Vector2(1, 0);
templateRect.pivot = new Vector2(0.5f, 1);
templateRect.anchoredPosition = new Vector2(0, 2);
templateRect.sizeDelta = new Vector2(0, 180);
var scroll = template.AddComponent<ScrollRect>();
scroll.horizontal = false;
var viewport = Create("Viewport", template.transform);
viewport.AddComponent<Image>().color = Color.white;
var mask = viewport.AddComponent<Mask>();
mask.showMaskGraphic = false;
Stretch(viewport.GetComponent<RectTransform>());
var content = Create("Content", viewport.transform);
var contentRect = content.GetComponent<RectTransform>();
contentRect.anchorMin = new Vector2(0, 1);
contentRect.anchorMax = new Vector2(1, 1);
contentRect.pivot = new Vector2(0.5f, 1);
contentRect.anchoredPosition = Vector2.zero;
contentRect.sizeDelta = new Vector2(0, 0);
var contentLayout = content.AddComponent<VerticalLayoutGroup>();
contentLayout.childControlWidth = true;
contentLayout.childControlHeight = true;
contentLayout.childForceExpandWidth = true;
contentLayout.childForceExpandHeight = false;
content.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
var item = Create("Item", content.transform);
var itemToggle = item.AddComponent<Toggle>();
item.AddComponent<Image>().color = new Color(0.96f, 0.97f, 0.98f);
item.AddComponent<LayoutElement>().preferredHeight = 32;
var checkmark = Create("Item Checkmark", item.transform).AddComponent<Image>();
checkmark.color = new Color(0.1f, 0.5f, 0.25f);
var checkmarkRect = checkmark.rectTransform;
checkmarkRect.anchorMin = new Vector2(0, 0.5f);
checkmarkRect.anchorMax = new Vector2(0, 0.5f);
checkmarkRect.sizeDelta = new Vector2(18, 18);
checkmarkRect.anchoredPosition = new Vector2(14, 0);
var itemLabel = Text(item.transform, "Option", 18);
Stretch(itemLabel.rectTransform, 32, 8, 4, 4);
itemToggle.targetGraphic = item.GetComponent<Image>();
itemToggle.graphic = checkmark;
scroll.viewport = viewport.GetComponent<RectTransform>();
scroll.content = contentRect;
dropdown.template = templateRect;
dropdown.itemText = itemLabel;
}
/// <summary>
/// 执行 Slider 方法。
/// </summary>
public Slider Slider(Transform parent)
{
var go = Create("Slider", parent);
var slider = go.AddComponent<Slider>();
slider.minValue = 0;
slider.maxValue = 1;
var bg = Create("Background", go.transform).AddComponent<Image>();
bg.color = new Color(0.8f, 0.82f, 0.85f);
Stretch(bg.rectTransform, 0, 0, 10, 10);
var fill = Create("Fill", go.transform).AddComponent<Image>();
fill.color = new Color(0.1f, 0.55f, 0.35f);
Stretch(fill.rectTransform, 0, 0, 10, 10);
slider.fillRect = fill.rectTransform;
var layout = go.AddComponent<LayoutElement>();
layout.preferredHeight = 28;
return slider;
}
/// <summary>
/// 执行 Section 方法。
/// </summary>
public GameObject Section(Transform parent, string title)
{
var section = Create("Section_" + title, parent);
var layout = section.AddComponent<VerticalLayoutGroup>();
layout.padding = new RectOffset(12, 12, 10, 10);
layout.spacing = 8;
layout.childControlWidth = true;
layout.childForceExpandWidth = true;
section.AddComponent<Image>().color = new Color(0.94f, 0.96f, 0.98f);
Text(section.transform, title, 24);
return section;
}
/// <summary>
/// 执行 Row 方法。
/// </summary>
public GameObject Row(Transform parent)
{
var row = Create("Row", parent);
var layout = row.AddComponent<HorizontalLayoutGroup>();
layout.spacing = 8;
layout.childControlHeight = true;
layout.childControlWidth = true;
return row;
}
/// <summary>
/// 执行 ScrollView 方法。
/// </summary>
public ScrollRect ScrollView(Transform parent, out Text contentText)
{
var root = Create("ScrollView", parent);
root.AddComponent<Image>().color = Color.white;
var viewport = Create("Viewport", root.transform);
var mask = viewport.AddComponent<Mask>();
mask.showMaskGraphic = false;
viewport.AddComponent<Image>().color = Color.white;
Stretch(viewport.GetComponent<RectTransform>());
var content = Create("Content", viewport.transform);
var contentRect = content.GetComponent<RectTransform>();
contentRect.anchorMin = new Vector2(0, 1);
contentRect.anchorMax = new Vector2(1, 1);
contentRect.pivot = new Vector2(0.5f, 1);
contentText = Text(content.transform, string.Empty, 18, TextAnchor.UpperLeft);
contentText.rectTransform.sizeDelta = new Vector2(0, 800);
Stretch(contentText.rectTransform);
var scroll = root.AddComponent<ScrollRect>();
scroll.viewport = viewport.GetComponent<RectTransform>();
scroll.content = contentRect;
scroll.horizontal = false;
var layout = root.AddComponent<LayoutElement>();
layout.preferredHeight = 180;
return scroll;
}
/// <summary>
/// 执行 Create 方法。
/// </summary>
public static GameObject Create(string name, Transform parent)
{
var go = new GameObject(name, typeof(RectTransform));
go.transform.SetParent(parent, false);
return go;
}
/// <summary>
/// 执行 Stretch 方法。
/// </summary>
public static void Stretch(RectTransform rect, float left = 0, float right = 0, float top = 0, float bottom = 0)
{
rect.anchorMin = Vector2.zero;
rect.anchorMax = Vector2.one;
rect.offsetMin = new Vector2(left, bottom);
rect.offsetMax = new Vector2(-right, -top);
}
}
/// <summary>
/// 主面板,负责页面初始化和切换。
/// </summary>
public sealed class FaceModuleMainPanel : MonoBehaviour
{
// Inspector 绑定字段。
[SerializeField] private FaceModuleManager m_manager;
// Inspector 绑定字段。
[SerializeField] private FaceModuleStatusBar m_statusBar;
// Inspector 绑定字段。
[SerializeField] private FaceModuleNavigationPanel m_navigationPanel;
// Inspector 绑定字段。
[SerializeField] private FaceModuleLogPanel m_logPanel;
// Inspector 绑定字段。
[SerializeField] private FaceModuleLoadingView m_loadingView;
private readonly Dictionary<string, FaceModulePageBase> m_pages = new Dictionary<string, FaceModulePageBase>();
/// <summary>
/// 初始化主面板。
/// </summary>
public void Init(FaceModuleManager manager)
{
m_manager = manager;
m_statusBar?.Init(m_manager);
m_logPanel?.Init(m_manager);
m_navigationPanel?.Init(this);
foreach (var pair in m_pages)
{
pair.Value.Init(m_manager, m_logPanel, m_loadingView);
pair.Value.Hide();
}
}
/// <summary>
/// 绑定公共组件。
/// </summary>
public void Bind(FaceModuleStatusBar statusBar, FaceModuleNavigationPanel navigationPanel, FaceModuleLogPanel logPanel, FaceModuleLoadingView loadingView)
{
m_statusBar = statusBar;
m_navigationPanel = navigationPanel;
m_logPanel = logPanel;
m_loadingView = loadingView;
}
/// <summary>
/// 注册页面。
/// </summary>
public void RegisterPage(string pageName, string label, FaceModulePageBase page, FaceModuleUiBuilder builder)
{
m_pages[pageName] = page;
m_navigationPanel.AddNavigation(pageName, label, builder);
}
/// <summary>
/// 显示指定页面。
/// </summary>
public void ShowPage(string pageName)
{
foreach (var pair in m_pages)
{
if (pair.Key == pageName)
{
pair.Value.Show();
}
else
{
pair.Value.Hide();
}
}
m_logPanel?.Info("切换页面: " + pageName);
}
}
}