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("请输入账号");
}
}
}