using AppUI.Localization; using System; using System.Text.RegularExpressions; using JCUnityLib; using TMPro; using UnityEngine; using UnityEngine.Events; namespace AppUI.Util.Input { /// 输入用途,决定校验规则与 默认配置。 public enum AppUIInputKind { Generic, Nickname, Password, Email, Phone, SmsCode, } /// 校验结果,供外部逻辑分支处理。 public readonly struct AppUIInputValidation { public bool IsValid { get; } /// 失败时已本地化的提示文案。 public string Message { get; } public static AppUIInputValidation Ok => new AppUIInputValidation(true, null); public AppUIInputValidation(bool isValid, string message) { IsValid = isValid; Message = message; } } /// /// 挂在 TMPInput 预制体根节点,统一管理左侧 Title、 及可选 。 /// 外部通过 / / 事件获取输入,无需直接操作子节点。 /// [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]"); /// 左侧标题文案。 public string Title { get => title; set { title = value ?? string.Empty; ApplyTitleToLabel(); } } public AppUIInputKind Kind => kind; /// 原始输入(未 Trim)。 public string Text => inputField != null ? inputField.text : string.Empty; /// 去除首尾空白后的输入,提交表单时优先使用。 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; /// Inspector 中 onClick 的代码订阅入口。 public event Action OnClicked; /// 输入内容变化(与 TMP 一致,参数为当前全文)。 public event Action OnValueChanged; /// 结束编辑(失焦 / 提交)。 public event Action 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(); } if (inputField == null) { var fieldTr = transform.Find(PathInputField); if (fieldTr != null) inputField = fieldTr.GetComponent(); if (inputField == null) inputField = GetComponentInChildren(true); } if (actionBar == null) actionBar = GetComponentInChildren(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); } /// 同步只读状态;运行中切换 时会重新绑定监听。 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(); } /// 供外部 Button 等转发调用,与点击输入区域效果相同。 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(); /// 按 titleKey / placeholderKey(或明文 fallback)刷新 Title 与 Placeholder。 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; } /// 配置 ContentType、长度与密码过滤等。 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(); /// 按类型做格式/非空校验,返回本地化提示文案(失败时)。 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; } /// 校验是否通过。 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("请输入账号"); } } }