| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477 |
- using AppUI.Localization;
- using System;
- using System.Text.RegularExpressions;
- using JCUnityLib;
- using TMPro;
- using UnityEngine;
- using UnityEngine.Events;
- namespace AppUI.Util.Input
- {
- /// <summary>输入用途,决定校验规则与 <see cref="TMP_InputField"/> 默认配置。</summary>
- public enum AppUIInputKind
- {
- Generic,
- Nickname,
- Password,
- Email,
- Phone,
- SmsCode,
- }
- /// <summary>校验结果,供外部逻辑分支处理。</summary>
- public readonly struct AppUIInputValidation
- {
- public bool IsValid { get; }
- /// <summary>失败时已本地化的提示文案。</summary>
- public string Message { get; }
- public static AppUIInputValidation Ok => new AppUIInputValidation(true, null);
- public AppUIInputValidation(bool isValid, string message)
- {
- IsValid = isValid;
- Message = message;
- }
- }
- /// <summary>
- /// 挂在 <c>TMPInput</c> 预制体根节点,统一管理左侧 Title、<see cref="TMP_InputField"/> 及可选 <see cref="TMP_InputFieldActionBar"/>。
- /// 外部通过 <see cref="Text"/> / <see cref="Validate"/> / 事件获取输入,无需直接操作子节点。
- /// </summary>
- [DisallowMultipleComponent]
- [ExecuteAlways]
- public class AppUITMPInputField : MonoBehaviour
- {
- const string PathTitleLabel = "Content/Title/Label";
- const string PathInputField = "Content/InputField (TMP)";
- [Header("展示")]
- [Tooltip("多语言 key;非空时优先于 Title 文案")]
- [SerializeField]
- string titleKey;
- [Tooltip("左侧 Title/Label 文案;titleKey 为空时使用,Editor 预览用")]
- [SerializeField]
- string title = "Password";
- [Tooltip("Placeholder 多语言 key;非空时优先于 Placeholder 文案")]
- [SerializeField]
- string placeholderKey;
- [Tooltip("占位符;placeholderKey 为空时使用;两者都为空则不修改预制体 Placeholder")]
- [SerializeField]
- string placeholder = "";
- [SerializeField]
- AppUIInputKind kind = AppUIInputKind.Generic;
- [Header("引用(可留空,运行时按路径自动绑定)")]
- [SerializeField]
- TMP_Text titleLabel;
- [SerializeField]
- TMP_InputField inputField;
- [SerializeField]
- TMP_InputFieldActionBar actionBar;
- [Header("校验(0 表示使用类型默认值)")]
- [SerializeField]
- int minLength;
- [SerializeField]
- int maxLength;
- [SerializeField]
- bool required = true;
- [Header("仅点击(不弹出键盘)")]
- [Tooltip("开启后输入框只读,点击时触发 OnClick,可在 Inspector 绑定其他对象方法")]
- [SerializeField]
- bool clickOnly;
- [SerializeField]
- UnityEvent onClick = new UnityEvent();
- static readonly Regex PasswordAlphanumeric = new Regex("[^A-Za-z0-9]");
- /// <summary>左侧标题文案。</summary>
- public string Title
- {
- get => title;
- set
- {
- title = value ?? string.Empty;
- ApplyTitleToLabel();
- }
- }
- public AppUIInputKind Kind => kind;
- /// <summary>原始输入(未 Trim)。</summary>
- public string Text => inputField != null ? inputField.text : string.Empty;
- /// <summary>去除首尾空白后的输入,提交表单时优先使用。</summary>
- public string TrimmedText => Text.Trim();
- public bool HasValue => TrimmedText.Length > 0;
- public TMP_InputField InputField => inputField;
- public TMP_InputFieldActionBar ActionBar => actionBar;
- public bool ClickOnly => clickOnly;
- public string TitleKey => titleKey;
- public string PlaceholderKey => placeholderKey;
- /// <summary>Inspector 中 <c>onClick</c> 的代码订阅入口。</summary>
- public event Action OnClicked;
- /// <summary>输入内容变化(与 TMP 一致,参数为当前全文)。</summary>
- public event Action<string> OnValueChanged;
- /// <summary>结束编辑(失焦 / 提交)。</summary>
- public event Action<string> OnEndEdit;
- void Awake()
- {
- ResolveReferences();
- ApplyKindSettings();
- }
- void OnEnable()
- {
- ResolveReferences();
- ApplyLocalizedTexts();
- ApplyClickOnlyMode();
- BindInputListeners();
- AppUILocalization.OnLanguageChanged += HandleLanguageChanged;
- }
- void OnDisable()
- {
- AppUILocalization.OnLanguageChanged -= HandleLanguageChanged;
- UnbindInputListeners();
- }
- #if UNITY_EDITOR
- void OnValidate()
- {
- ResolveReferences();
- ApplyLocalizedTexts();
- if (!Application.isPlaying)
- ApplyKindSettings();
- if (inputField != null)
- inputField.readOnly = clickOnly;
- }
- #endif
- void ResolveReferences()
- {
- if (titleLabel == null)
- {
- var labelTr = transform.Find(PathTitleLabel);
- if (labelTr != null)
- titleLabel = labelTr.GetComponent<TMP_Text>();
- }
- if (inputField == null)
- {
- var fieldTr = transform.Find(PathInputField);
- if (fieldTr != null)
- inputField = fieldTr.GetComponent<TMP_InputField>();
- if (inputField == null)
- inputField = GetComponentInChildren<TMP_InputField>(true);
- }
- if (actionBar == null)
- actionBar = GetComponentInChildren<TMP_InputFieldActionBar>(true);
- }
- void BindInputListeners()
- {
- if (inputField == null)
- return;
- inputField.onValueChanged.RemoveListener(HandleValueChanged);
- inputField.onEndEdit.RemoveListener(HandleEndEdit);
- inputField.onSelect.RemoveListener(HandleClickOnlySelect);
- if (!clickOnly)
- {
- inputField.onValueChanged.AddListener(HandleValueChanged);
- inputField.onEndEdit.AddListener(HandleEndEdit);
- }
- else
- inputField.onSelect.AddListener(HandleClickOnlySelect);
- }
- void UnbindInputListeners()
- {
- if (inputField == null)
- return;
- inputField.onValueChanged.RemoveListener(HandleValueChanged);
- inputField.onEndEdit.RemoveListener(HandleEndEdit);
- inputField.onSelect.RemoveListener(HandleClickOnlySelect);
- }
- /// <summary>同步只读状态;运行中切换 <see cref="clickOnly"/> 时会重新绑定监听。</summary>
- public void ApplyClickOnlyMode()
- {
- if (inputField == null)
- return;
- inputField.readOnly = clickOnly;
- if (isActiveAndEnabled)
- BindInputListeners();
- }
- void HandleClickOnlySelect(string _)
- {
- if (!clickOnly)
- return;
- if (inputField != null)
- inputField.DeactivateInputField();
- InvokeClick();
- }
- /// <summary>供外部 Button 等转发调用,与点击输入区域效果相同。</summary>
- public void InvokeClick()
- {
- onClick?.Invoke();
- OnClicked?.Invoke();
- }
- void HandleValueChanged(string value)
- {
- var filtered = value;
- var changed = false;
- if (kind == AppUIInputKind.Password)
- changed = FilterPasswordAlphanumeric(ref filtered);
- else if (kind == AppUIInputKind.SmsCode || kind == AppUIInputKind.Phone)
- changed = FilterDigitsOnly(ref filtered, GetEffectiveMaxLength());
- if (changed && inputField != null)
- inputField.SetTextWithoutNotify(filtered);
- OnValueChanged?.Invoke(inputField != null ? inputField.text : filtered);
- }
- void HandleEndEdit(string value) => OnEndEdit?.Invoke(value);
- void HandleLanguageChanged(LanguageEnum _) => ApplyLocalizedTexts();
- /// <summary>按 titleKey / placeholderKey(或明文 fallback)刷新 Title 与 Placeholder。</summary>
- public void ApplyLocalizedTexts()
- {
- #if UNITY_EDITOR
- if (!Application.isPlaying)
- AppUILocalization.Reload();
- #endif
- ApplyTitleToLabel();
- ApplyPlaceholder();
- }
- void ApplyTitleToLabel()
- {
- if (titleLabel == null)
- return;
- if (string.IsNullOrEmpty(titleKey) && string.IsNullOrEmpty(title))
- return;
- titleLabel.text = ResolveDisplayText(titleKey, title);
- }
- void ApplyPlaceholder()
- {
- if (inputField == null)
- return;
- string text = ResolveDisplayText(placeholderKey, placeholder);
- if (string.IsNullOrEmpty(text))
- return;
- if (inputField.placeholder is TMP_Text ph)
- ph.text = text;
- }
- static string ResolveDisplayText(string key, string fallback)
- {
- if (!string.IsNullOrEmpty(key))
- {
- AppUILocalization.Init();
- return AppUILocalization.GetTextByKey(key);
- }
- return fallback ?? string.Empty;
- }
- /// <summary>按 <see cref="kind"/> 配置 ContentType、长度与密码过滤等。</summary>
- public void ApplyKindSettings()
- {
- if (inputField == null)
- return;
- inputField.lineType = TMP_InputField.LineType.SingleLine;
- inputField.characterValidation = TMP_InputField.CharacterValidation.None;
- switch (kind)
- {
- case AppUIInputKind.Password:
- inputField.contentType = TMP_InputField.ContentType.Password;
- break;
- case AppUIInputKind.Email:
- inputField.contentType = TMP_InputField.ContentType.EmailAddress;
- break;
- case AppUIInputKind.Phone:
- inputField.contentType = TMP_InputField.ContentType.IntegerNumber;
- break;
- case AppUIInputKind.SmsCode:
- inputField.contentType = TMP_InputField.ContentType.IntegerNumber;
- break;
- case AppUIInputKind.Nickname:
- inputField.contentType = TMP_InputField.ContentType.Standard;
- break;
- default:
- inputField.contentType = TMP_InputField.ContentType.Standard;
- break;
- }
- inputField.characterLimit = GetEffectiveMaxLength();
- inputField.ForceLabelUpdate();
- }
- int GetEffectiveMaxLength()
- {
- if (maxLength > 0)
- return maxLength;
- switch (kind)
- {
- case AppUIInputKind.Phone:
- return 11;
- case AppUIInputKind.SmsCode:
- return 6;
- case AppUIInputKind.Nickname:
- return 32;
- default:
- return 0;
- }
- }
- int GetEffectiveMinLength()
- {
- if (minLength > 0)
- return minLength;
- if (kind == AppUIInputKind.Password)
- return 6;
- return required ? 1 : 0;
- }
- static bool FilterPasswordAlphanumeric(ref string value)
- {
- var match = PasswordAlphanumeric.Match(value);
- if (!match.Success)
- return false;
- value = value.Replace(match.Value, string.Empty);
- return true;
- }
- static bool FilterDigitsOnly(ref string value, int maxLen)
- {
- if (string.IsNullOrEmpty(value))
- return false;
- var filtered = string.Empty;
- foreach (var c in value)
- {
- if (!char.IsDigit(c))
- continue;
- filtered += c;
- if (maxLen > 0 && filtered.Length >= maxLen)
- break;
- }
- if (filtered == value)
- return false;
- value = filtered;
- return true;
- }
- public void SetText(string value)
- {
- if (inputField == null)
- return;
- inputField.text = value ?? string.Empty;
- }
- public void Clear() => SetText(string.Empty);
- public void Focus() => inputField?.ActivateInputField();
- /// <summary>按类型做格式/非空校验,返回本地化提示文案(失败时)。</summary>
- public AppUIInputValidation Validate()
- {
- var text = TrimmedText;
- var min = GetEffectiveMinLength();
- if (required && text.Length == 0)
- return Fail(EmptyMessageForKind());
- if (min > 0 && text.Length > 0 && text.Length < min)
- return Fail(TooShortMessageForKind(min));
- switch (kind)
- {
- case AppUIInputKind.Email:
- if (text.Length > 0 && !ValidateHelper.IsEmail(text))
- return Fail(AppUILocalization.GetTextByKey("RelateValidateView-a0"));
- break;
- case AppUIInputKind.Phone:
- if (text.Length > 0 && !ValidateHelper.IsMobilePhone(text))
- return Fail(AppUILocalization.GetTextByKey("RelateValidateView-a1"));
- break;
- case AppUIInputKind.SmsCode:
- if (text.Length > 0 && (text.Length != 6 || !int.TryParse(text, out _)))
- return Fail(AppUILocalization.GetTextByKey("RelateValidateView-a3"));
- break;
- }
- return AppUIInputValidation.Ok;
- }
- /// <summary>校验是否通过。</summary>
- public bool TryValidate(out string errorMessage)
- {
- var result = Validate();
- errorMessage = result.IsValid ? null : result.Message;
- return result.IsValid;
- }
- static AppUIInputValidation Fail(string message) =>
- new AppUIInputValidation(false, message);
- string EmptyMessageForKind()
- {
- switch (kind)
- {
- case AppUIInputKind.Nickname:
- return AppUILocalization.GetTextByCNKey("请输入游戏昵称");
- case AppUIInputKind.Password:
- return AppUILocalization.GetTextByCNKey("请输入密码");
- case AppUIInputKind.Email:
- return AppUILocalization.GetTextByKey("RelateValidateView-a0");
- case AppUIInputKind.Phone:
- return AppUILocalization.GetTextByKey("RelateValidateView-a1");
- case AppUIInputKind.SmsCode:
- return AppUILocalization.GetTextByKey("RelateValidateView-a3");
- default:
- return AppUILocalization.GetTextByCNKey("请输入账号");
- }
- }
- string TooShortMessageForKind(int min)
- {
- if (kind == AppUIInputKind.Password)
- return AppUILocalization.GetTextByCNKey("密码长度至少6位");
- return AppUILocalization.GetTextByCNKey("请输入账号");
- }
- }
- }
|