From c6bcca4e7f11e98687ce88907fbe2bd6f144fb9c Mon Sep 17 00:00:00 2001 From: moyin <2443444649@qq.com> Date: Wed, 24 Dec 2025 20:37:00 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E5=AE=9E=E7=8E=B0=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E8=AE=A4=E8=AF=81=E7=B3=BB=E7=BB=9F=E6=A0=B8=E5=BF=83?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现用户登录和注册的完整流程 - 添加邮箱验证码发送和验证功能 - 实现基于邮箱地址的验证码冷却机制 - 添加表单验证和错误提示系统 - 集成Toast消息提示系统 - 支持网络请求处理和错误处理 - 实现按钮状态管理和加载状态显示 --- scripts/scenes/AuthScene.gd | 1098 +++++++++++++++++++++++++++++++++++ 1 file changed, 1098 insertions(+) create mode 100644 scripts/scenes/AuthScene.gd diff --git a/scripts/scenes/AuthScene.gd b/scripts/scenes/AuthScene.gd new file mode 100644 index 0000000..a4e467e --- /dev/null +++ b/scripts/scenes/AuthScene.gd @@ -0,0 +1,1098 @@ +extends Control + +# 信号定义 +signal login_success(username: String) + +# API配置 +const API_BASE_URL = "https://whaletownend.xinghangee.icu" + +# UI节点引用 +@onready var background_image: TextureRect = $BackgroundImage +@onready var login_panel: Panel = $CenterContainer/LoginPanel +@onready var register_panel: Panel = $CenterContainer/RegisterPanel +@onready var title_label: Label = $CenterContainer/LoginPanel/VBoxContainer/TitleLabel +@onready var subtitle_label: Label = $CenterContainer/LoginPanel/VBoxContainer/SubtitleLabel +@onready var whale_frame: TextureRect = $WhaleFrame + +# 登录表单 +@onready var login_username: LineEdit = $CenterContainer/LoginPanel/VBoxContainer/LoginForm/UsernameContainer/UsernameInput +@onready var login_password: LineEdit = $CenterContainer/LoginPanel/VBoxContainer/LoginForm/PasswordContainer/PasswordInput +@onready var login_username_error: Label = $CenterContainer/LoginPanel/VBoxContainer/LoginForm/UsernameContainer/UsernameLabelContainer/UsernameError +@onready var login_password_error: Label = $CenterContainer/LoginPanel/VBoxContainer/LoginForm/PasswordContainer/PasswordLabelContainer/PasswordError +@onready var main_btn: Button = $CenterContainer/LoginPanel/VBoxContainer/MainButton +@onready var login_btn: Button = $CenterContainer/LoginPanel/VBoxContainer/ButtonContainer/LoginBtn +@onready var to_register_btn: Button = $CenterContainer/LoginPanel/VBoxContainer/ButtonContainer/ToRegisterBtn +@onready var forgot_password_btn: Button = $CenterContainer/LoginPanel/VBoxContainer/BottomLinks/ForgotPassword +@onready var register_link_btn: Button = $CenterContainer/LoginPanel/VBoxContainer/BottomLinks/RegisterLink + +# 注册表单 +@onready var register_username: LineEdit = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/UsernameContainer/UsernameInput +@onready var register_email: LineEdit = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/EmailContainer/EmailInput +@onready var register_password: LineEdit = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/PasswordContainer/PasswordInput +@onready var register_confirm: LineEdit = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/ConfirmContainer/ConfirmInput +@onready var verification_input: LineEdit = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/VerificationContainer/VerificationInputContainer/VerificationInput +@onready var send_code_btn: Button = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/VerificationContainer/VerificationInputContainer/SendCodeBtn +@onready var register_btn: Button = $CenterContainer/RegisterPanel/VBoxContainer/ButtonContainer/RegisterBtn +@onready var to_login_btn: Button = $CenterContainer/RegisterPanel/VBoxContainer/ButtonContainer/ToLoginBtn + +# 错误提示标签 +@onready var register_username_error: Label = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/UsernameContainer/UsernameLabelContainer/UsernameError +@onready var register_email_error: Label = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/EmailContainer/EmailLabelContainer/EmailError +@onready var register_password_error: Label = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/PasswordContainer/PasswordLabelContainer/PasswordError +@onready var register_confirm_error: Label = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/ConfirmContainer/ConfirmLabelContainer/ConfirmError +@onready var verification_error: Label = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/VerificationContainer/VerificationLabelContainer/VerificationError + +# HTTP请求节点 +var http_request: HTTPRequest + +# Toast消息节点 +@onready var toast_container: Control = $ToastContainer + +# Toast管理 +var active_toasts: Array = [] +var toast_counter: int = 0 + +# 验证码状态 +var verification_codes_sent: Dictionary = {} # 存储每个邮箱的发送状态 {email: {sent: bool, time: float}} +var code_cooldown: float = 60.0 # 60秒冷却时间 +var current_request_type: String = "" # 跟踪当前请求类型 +var register_data: Dictionary = {} # 存储注册数据 +var cooldown_timer: Timer = null # 倒计时定时器 +var current_email: String = "" # 当前正在倒计时的邮箱 + +func _ready(): + # 获取HTTP请求节点 + http_request = $HTTPRequest + http_request.request_completed.connect(_on_http_request_completed) + + # 连接信号 + connect_signals() + + # 初始显示登录界面 + show_login_panel() + + # 测试Toast系统(延迟一下确保节点已初始化) + await get_tree().process_frame + print("测试Toast系统...") + show_toast("认证系统已加载", true) + + # 测试网络连接 + test_network_connection() + +# 测试网络连接 +func test_network_connection(): + print("=== 测试网络连接 ===") + var url = API_BASE_URL + "/" + var headers = ["Content-Type: application/json"] + + print("测试URL: ", url) + current_request_type = "network_test" + + var error = http_request.request(url, headers, HTTPClient.METHOD_GET) + print("网络测试请求发送结果: ", error) + + if error != OK: + print("网络测试请求发送失败: ", error) + show_toast("网络连接测试失败", false) + else: + print("网络测试请求已发送,等待响应...") + +func connect_signals(): + # 主要按钮 + main_btn.pressed.connect(_on_main_button_pressed) + + # 登录界面按钮 + login_btn.pressed.connect(_on_login_pressed) + to_register_btn.pressed.connect(_on_to_register_pressed) + forgot_password_btn.pressed.connect(_on_forgot_password_pressed) + register_link_btn.pressed.connect(_on_register_link_pressed) + + # 注册界面按钮 + register_btn.pressed.connect(_on_register_pressed) + to_login_btn.pressed.connect(_on_to_login_pressed) + send_code_btn.pressed.connect(_on_send_code_pressed) + + # 回车键登录 + login_password.text_submitted.connect(_on_login_enter) + + # 登录表单失焦验证 + login_username.focus_exited.connect(_on_login_username_focus_exited) + login_password.focus_exited.connect(_on_login_password_focus_exited) + + # 注册表单失焦验证 + register_username.focus_exited.connect(_on_register_username_focus_exited) + register_email.focus_exited.connect(_on_register_email_focus_exited) + register_password.focus_exited.connect(_on_register_password_focus_exited) + register_confirm.focus_exited.connect(_on_register_confirm_focus_exited) + verification_input.focus_exited.connect(_on_verification_focus_exited) + + # 实时输入验证 + register_username.text_changed.connect(_on_register_username_text_changed) + register_email.text_changed.connect(_on_register_email_text_changed) + register_password.text_changed.connect(_on_register_password_text_changed) + register_confirm.text_changed.connect(_on_register_confirm_text_changed) + verification_input.text_changed.connect(_on_verification_text_changed) + +func show_login_panel(): + login_panel.visible = true + register_panel.visible = false + login_username.grab_focus() + +func show_register_panel(): + login_panel.visible = false + register_panel.visible = true + register_username.grab_focus() + +func _on_main_button_pressed(): + if not validate_login_form(): + return + + var username = login_username.text.strip_edges() + var password = login_password.text + + # 显示加载状态 + show_loading(main_btn, "登录中...") + show_toast('正在验证登录信息...', true) + + # 发送登录请求 + send_login_request(username, password) + +func _on_login_pressed(): + if not validate_login_form(): + return + + var username = login_username.text.strip_edges() + var password = login_password.text + + # 显示加载状态 + show_loading(login_btn, "登录中...") + show_toast('正在验证登录信息...', true) + + # 发送登录请求 + send_login_request(username, password) + +func _on_register_pressed(): + print("注册按钮被点击") + + if not validate_register_form(): + print("注册表单验证失败") + show_toast('请检查并完善注册信息', false) + return + + print("注册表单验证通过,开始注册流程") + + var username = register_username.text.strip_edges() + var email = register_email.text.strip_edges() + var password = register_password.text + var verification_code = verification_input.text.strip_edges() + + # 显示加载状态 + show_loading(register_btn, "注册中...") + show_toast('正在验证邮箱验证码...', true) + + # 先验证邮箱验证码,然后注册 + verify_email_then_register(username, email, password, verification_code) + +func _on_send_code_pressed(): + var email = register_email.text.strip_edges() + + # 验证邮箱 + var email_validation = validate_email(email) + if not email_validation.valid: + show_toast(email_validation.message, false) + register_email.grab_focus() + return + + hide_field_error(register_email_error) + + # 检查该邮箱的冷却时间 + var current_time = Time.get_time_dict_from_system() + var current_timestamp = current_time.hour * 3600 + current_time.minute * 60 + current_time.second + + if verification_codes_sent.has(email): + var email_data = verification_codes_sent[email] + if email_data.sent and (current_timestamp - email_data.time) < code_cooldown: + var remaining = code_cooldown - (current_timestamp - email_data.time) + show_toast('该邮箱请等待 %d 秒后再次发送' % remaining, false) + return + + # 如果当前有其他邮箱在倒计时,需要切换到新邮箱 + if current_email != email: + # 停止当前倒计时 + stop_current_cooldown() + current_email = email + + # 立即开始倒计时并禁用按钮 + if not verification_codes_sent.has(email): + verification_codes_sent[email] = {} + + verification_codes_sent[email].sent = true + verification_codes_sent[email].time = current_timestamp + start_cooldown_timer(email) + + # 发送验证码请求 + send_verification_code_request(email) + +# 发送登录请求 +func send_login_request(username: String, password: String): + var url = API_BASE_URL + "/auth/login" + var headers = ["Content-Type: application/json"] + var body = JSON.stringify({ + "username": username, + "password": password + }) + + current_request_type = "login" + + var error = http_request.request(url, headers, HTTPClient.METHOD_POST, body) + if error != OK: + show_toast('网络请求失败', false) + restore_button(login_btn, "密码登录") + current_request_type = "" + +# 发送邮箱验证码请求 +func send_verification_code_request(email: String): + var url = API_BASE_URL + "/auth/send-email-verification" + var headers = ["Content-Type: application/json"] + var body = JSON.stringify({"email": email}) + + print("=== 发送验证码请求 ===") + print("URL: ", url) + print("Headers: ", headers) + print("Body: ", body) + + current_request_type = "send_code" + + var error = http_request.request(url, headers, HTTPClient.METHOD_POST, body) + print("HTTP请求发送结果: ", error, " (OK=", OK, ")") + + if error != OK: + print("HTTP请求发送失败,错误代码: ", error) + show_toast('网络请求失败', false) + restore_button(send_code_btn, "发送验证码") + current_request_type = "" + else: + print("HTTP请求已发送,等待响应...") + +# 验证邮箱然后注册 +func verify_email_then_register(username: String, email: String, password: String, verification_code: String): + var url = API_BASE_URL + "/auth/verify-email" + var headers = ["Content-Type: application/json"] + var body = JSON.stringify({ + "email": email, + "verification_code": verification_code + }) + + current_request_type = "verify_email" + + # 保存注册信息,验证成功后使用 + register_data = { + "username": username, + "email": email, + "password": password, + "verification_code": verification_code + } + + var error = http_request.request(url, headers, HTTPClient.METHOD_POST, body) + if error != OK: + show_toast('网络请求失败', false) + restore_button(register_btn, "注册") + current_request_type = "" + +# 发送注册请求 +func send_register_request(username: String, email: String, password: String, verification_code: String = ""): + var url = API_BASE_URL + "/auth/register" + var headers = ["Content-Type: application/json"] + var body_data = { + "username": username, + "password": password, + "nickname": username, # 使用用户名作为昵称 + "email": email + } + + # 如果提供了验证码,则添加到请求体中 + if verification_code != "": + body_data["email_verification_code"] = verification_code + + var body = JSON.stringify(body_data) + + current_request_type = "register" + + var error = http_request.request(url, headers, HTTPClient.METHOD_POST, body) + if error != OK: + show_toast('网络请求失败', false) + restore_button(register_btn, "注册") + current_request_type = "" + +# HTTP请求完成回调 +func _on_http_request_completed(_result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray): + var response_text = body.get_string_from_utf8() + + print("=== HTTP响应接收 ===") + print("请求类型: ", current_request_type) + print("响应状态码: ", response_code) + print("响应头: ", _headers) + print("响应体: ", response_text) + print("响应体长度: ", body.size(), " 字节") + + # 恢复按钮状态(排除验证码按钮,因为它有自己的状态管理) + if current_request_type != "send_code": + restore_button(send_code_btn, "发送验证码") + restore_button(register_btn, "注册") + restore_button(login_btn, "密码登录") + restore_button(main_btn, "进入小镇") + + # 处理网络连接失败 + if response_code == 0: + show_toast('网络连接失败,请检查网络连接', false) + current_request_type = "" + return + + # 解析JSON响应 + var json = JSON.new() + var parse_result = json.parse(response_text) + if parse_result != OK: + show_toast('服务器响应格式错误', false) + current_request_type = "" + return + + var response_data = json.data + + # 根据请求类型处理响应 + var request_type = current_request_type + current_request_type = "" # 提前清空,避免异步调用时的竞态条件 + + match request_type: + "network_test": + handle_network_test_response(response_code, response_data) + "login": + handle_login_response(response_code, response_data) + "send_code": + handle_verification_code_response(response_code, response_data) + "verify_email": + handle_verify_email_response(response_code, response_data) + "register": + handle_register_response(response_code, response_data) + +# 处理网络测试响应 +func handle_network_test_response(response_code: int, data: Dictionary): + print("=== 网络测试响应 ===") + print("状态码: ", response_code) + print("响应数据: ", data) + + if response_code == 200: + print("✅ 网络连接正常,后端服务可访问") + show_toast("网络连接正常", true) + else: + print("❌ 网络连接异常,状态码: ", response_code) + show_toast("网络连接异常: " + str(response_code), false) + +# 处理登录响应 +func handle_login_response(response_code: int, data: Dictionary): + match response_code: + 200: + show_toast('登录成功!正在进入鲸鱼镇...', true) + # 获取用户信息 + var username = login_username.text.strip_edges() + if data.has("data") and data.data.has("user"): + var user_data = data.data.user + if user_data.has("username"): + username = user_data.username + + # 清空登录表单 + login_username.text = "" + login_password.text = "" + hide_field_error(login_username_error) + hide_field_error(login_password_error) + + # 延迟一下再发送信号,让用户看到成功消息 + await get_tree().create_timer(1.0).timeout + + # 发送登录成功信号 + login_success.emit(username) + 400: + # 参数错误,统一使用Toast显示 + var message = data.get("message", "登录参数错误") + if "用户名" in message or "username" in message.to_lower(): + show_toast('用户名格式错误', false) + elif "密码" in message or "password" in message.to_lower(): + show_toast('密码格式错误', false) + else: + show_toast('登录信息有误,请检查后重试', false) + 401: + # 认证失败 + show_toast('用户名或密码错误,请检查后重试', false) + 404: + # 用户不存在 + show_toast('用户不存在,请先注册', false) + 429: + # 请求过频 + show_toast('登录请求过于频繁,请稍后再试', false) + 500: + # 服务器错误 + show_toast('服务器繁忙,请稍后再试', false) + _: + var message = data.get("message", "登录失败") + show_toast(message, false) + +# 处理验证码响应 +func handle_verification_code_response(response_code: int, data: Dictionary): + match response_code: + 200: + show_toast('验证码已发送到您的邮箱,请查收', true) + + # 开发环境下显示验证码(仅用于测试) + if data.has("data") and data.data.has("verification_code"): + print("开发环境验证码: ", data.data.verification_code) + 206: + # 测试模式 + show_toast('测试模式:验证码已生成,请查看控制台', true) + if data.has("data") and data.data.has("verification_code"): + print("测试模式验证码: ", data.data.verification_code) + 400: + # 根据具体错误信息显示相应的Toast + var message = data.get("message", "发送验证码失败") + var error_code = data.get("error_code", "") + + # 根据错误代码或消息内容判断具体错误类型 + if "邮箱格式" in message or "INVALID_EMAIL" in error_code: + show_toast('请输入有效的邮箱地址', false) + elif "每小时发送次数" in message or "HOURLY_LIMIT" in error_code: + show_toast('每小时发送次数已达上限,请稍后再试', false) + elif "频率" in message or "RATE_LIMITED" in error_code: + show_toast('发送过于频繁,请稍后再试', false) + else: + # 未知400错误,显示通用消息 + show_toast('发送验证码失败,请检查邮箱地址或稍后再试', false) + + reset_verification_button() + 429: + # 频率限制 + var message = data.get("message", "请求过于频繁,请稍后再试") + show_toast(message, false) + reset_verification_button() + 500: + show_toast('服务器繁忙,请稍后再试', false) + reset_verification_button() + _: + var message = data.get("message", "发送验证码失败") + show_toast(message, false) + reset_verification_button() + +# 处理邮箱验证响应 +func handle_verify_email_response(response_code: int, data: Dictionary): + match response_code: + 200: + show_toast('邮箱验证成功,正在注册...', true) + # 邮箱验证成功,继续注册 + if register_data.has("username") and register_data.has("email") and register_data.has("password") and register_data.has("verification_code"): + send_register_request(register_data.username, register_data.email, register_data.password, register_data.verification_code) + else: + show_toast('注册数据丢失,请重新填写', false) + 400: + show_toast('验证码错误或已过期', false) + 404: + show_toast('请先获取验证码', false) + 500: + show_toast('验证失败,请稍后再试', false) + _: + var message = data.get("message", "邮箱验证失败") + show_toast(message, false) + +# 处理注册响应 +func handle_register_response(response_code: int, data: Dictionary): + match response_code: + 201: + show_toast('注册成功!欢迎加入鲸鱼镇', true) + # 清空表单 + clear_register_form() + # 返回登录界面 + show_login_panel() + # 自动填入用户名 + login_username.text = register_data.get("username", "") + register_data.clear() + 400: + # 根据具体错误处理 + var message = data.get("message", "参数验证失败") + + # 针对常见错误提供友好提示,统一使用Toast显示 + if "邮箱验证码" in message or "verification_code" in message: + show_toast('请先获取并输入邮箱验证码', false) + elif "用户名" in message: + show_toast('用户名格式不正确', false) + elif "邮箱" in message: + show_toast('邮箱格式不正确', false) + elif "密码" in message: + show_toast('密码格式不符合要求', false) + elif "验证码" in message: + show_toast('验证码错误或已过期', false) + else: + # 显示用户友好的通用错误信息 + show_toast('注册信息有误,请检查后重试', false) + 409: + # 用户名或邮箱已存在 + var message = data.get("message", "用户名或邮箱已被使用") + if "用户名" in message: + show_toast('用户名已被使用,请换一个', false) + elif "邮箱" in message: + show_toast('邮箱已被使用,请换一个', false) + else: + show_toast('用户名或邮箱已被使用,请换一个', false) + 429: + # 注册请求过于频繁 + var message = data.get("message", "注册请求过于频繁,请稍后再试") + show_toast(message, false) + 500: + show_toast('注册失败,请稍后再试', false) + _: + var message = data.get("message", "注册失败") + show_toast(message, false) + +# 开始冷却计时器 +func start_cooldown_timer(email: String): + # 清理之前的计时器 + if cooldown_timer != null: + cooldown_timer.queue_free() + + # 设置当前邮箱 + current_email = email + + # 立即设置按钮状态 + send_code_btn.disabled = true + send_code_btn.text = "重新发送(60)" + + # 创建新的计时器 + cooldown_timer = Timer.new() + add_child(cooldown_timer) + cooldown_timer.wait_time = 1.0 + cooldown_timer.timeout.connect(_on_cooldown_timer_timeout) + cooldown_timer.start() + +func _on_cooldown_timer_timeout(): + # 检查当前邮箱输入框的邮箱 + var input_email = register_email.text.strip_edges() + + # 如果用户换了邮箱,停止当前倒计时 + if input_email != current_email: + stop_current_cooldown() + return + + # 检查当前邮箱的剩余时间 + if verification_codes_sent.has(current_email): + var current_time = Time.get_time_dict_from_system() + var current_timestamp = current_time.hour * 3600 + current_time.minute * 60 + current_time.second + var email_data = verification_codes_sent[current_email] + var remaining = code_cooldown - (current_timestamp - email_data.time) + + if remaining > 0: + send_code_btn.text = "重新发送(%d)" % remaining + else: + # 倒计时结束,恢复按钮 + send_code_btn.text = "重新发送" + send_code_btn.disabled = false + + # 清理计时器 + if cooldown_timer != null: + cooldown_timer.queue_free() + cooldown_timer = null + current_email = "" + +# 停止当前倒计时 +func stop_current_cooldown(): + if cooldown_timer != null: + cooldown_timer.queue_free() + cooldown_timer = null + + # 恢复按钮状态 + send_code_btn.disabled = false + send_code_btn.text = "发送验证码" + current_email = "" + +# 重置验证码按钮状态(发送失败时调用) +func reset_verification_button(): + # 清除当前邮箱的发送状态 + if current_email != "" and verification_codes_sent.has(current_email): + verification_codes_sent[current_email].sent = false + + stop_current_cooldown() + +# 清空注册表单 +func clear_register_form(): + register_username.text = "" + register_email.text = "" + register_password.text = "" + register_confirm.text = "" + verification_input.text = "" + + # 重置验证码状态 + stop_current_cooldown() + verification_codes_sent.clear() + + # 隐藏所有错误提示 + hide_field_error(register_username_error) + hide_field_error(register_email_error) + hide_field_error(register_password_error) + hide_field_error(register_confirm_error) + hide_field_error(verification_error) + +# ============ Toast消息系统 ============ + +# 显示Toast消息 +func show_toast(message: String, is_success: bool = true): + print("显示Toast消息: ", message, " 成功: ", is_success) + + # 确保容器存在 + if toast_container == null: + print("错误: toast_container 节点不存在") + return + + # 创建新的Toast实例 + create_toast_instance(message, is_success) + +# 创建Toast实例 +func create_toast_instance(message: String, is_success: bool): + toast_counter += 1 + + # 创建Toast Panel + var toast_panel = Panel.new() + toast_panel.name = "Toast_" + str(toast_counter) + + # 设置Toast样式 + var style = StyleBoxFlat.new() + if is_success: + style.bg_color = Color(0.2, 0.8, 0.2, 0.95) # 绿色背景 + else: + style.bg_color = Color(0.8, 0.2, 0.2, 0.95) # 红色背景 + + style.border_width_left = 2 + style.border_width_top = 2 + style.border_width_right = 2 + style.border_width_bottom = 2 + style.border_color = Color(1, 1, 1, 0.8) # 白色边框 + style.corner_radius_top_left = 8 + style.corner_radius_top_right = 8 + style.corner_radius_bottom_left = 8 + style.corner_radius_bottom_right = 8 + + toast_panel.add_theme_stylebox_override("panel", style) + + # 设置Toast尺寸和位置(右上角外侧开始) + var toast_width = 280 + var toast_height = 50 + var margin = 20 + var start_x = get_viewport().get_visible_rect().size.x # 屏幕外右侧 + var final_x = get_viewport().get_visible_rect().size.x - toast_width - margin # 最终位置 + var y_position = margin + (active_toasts.size() * (toast_height + 10)) # 垂直堆叠 + + toast_panel.position = Vector2(start_x, y_position) + toast_panel.size = Vector2(toast_width, toast_height) + + # 创建Label + var toast_label = Label.new() + toast_label.text = message + toast_label.add_theme_color_override("font_color", Color(1, 1, 1, 1)) + toast_label.add_theme_font_size_override("font_size", 14) + toast_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + toast_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + toast_label.autowrap_mode = TextServer.AUTOWRAP_OFF # 禁用自动换行 + + # 设置Label的位置和大小(不使用anchors_preset) + toast_label.position = Vector2(10, 5) + toast_label.size = Vector2(toast_width - 20, toast_height - 10) # 留出边距 + + # 组装Toast + toast_panel.add_child(toast_label) + toast_container.add_child(toast_panel) + active_toasts.append(toast_panel) + + # 执行滑入动画(快慢快) + animate_toast_in(toast_panel, final_x) + +# Toast滑入动画(快慢快) +func animate_toast_in(toast_panel: Panel, final_x: float): + var tween = create_tween() + tween.set_ease(Tween.EASE_OUT) # 开始快,结束慢 + tween.set_trans(Tween.TRANS_BACK) # 回弹效果 + + # 滑入动画 + tween.tween_property(toast_panel, "position:x", final_x, 0.5) + + # 等待2秒后滑出 + await get_tree().create_timer(2.0).timeout + animate_toast_out(toast_panel) + +# Toast滑出动画 +func animate_toast_out(toast_panel: Panel): + if not is_instance_valid(toast_panel): + return + + var tween = create_tween() + tween.set_ease(Tween.EASE_IN) # 开始慢,结束快 + tween.set_trans(Tween.TRANS_QUART) + + # 滑出到右侧屏幕外 + var end_x = get_viewport().get_visible_rect().size.x + 50 + tween.tween_property(toast_panel, "position:x", end_x, 0.3) + + # 动画完成后清理 + await tween.finished + cleanup_toast(toast_panel) + +# 清理Toast实例 +func cleanup_toast(toast_panel: Panel): + if not is_instance_valid(toast_panel): + return + + # 从活动列表中移除 + active_toasts.erase(toast_panel) + + # 重新排列剩余的Toast + rearrange_toasts() + + # 删除节点 + toast_panel.queue_free() + +# 重新排列Toast位置 +func rearrange_toasts(): + var margin = 20 + var toast_height = 50 + + for i in range(active_toasts.size()): + var toast = active_toasts[i] + if is_instance_valid(toast): + var new_y = margin + (i * (toast_height + 10)) + var tween = create_tween() + tween.tween_property(toast, "position:y", new_y, 0.2) + +# 显示加载状态 +func show_loading(button: Button, loading_text: String): + button.disabled = true + button.text = loading_text + +# 恢复按钮状态 +func restore_button(button: Button, original_text: String): + button.disabled = false + button.text = original_text + +# 验证邮箱格式 +func is_valid_email(email: String) -> bool: + var regex = RegEx.new() + regex.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$") + return regex.search(email) != null + +# 显示错误信息 +func show_field_error(error_label: Label, message: String): + error_label.text = message + error_label.visible = true + +# 隐藏错误信息 +func hide_field_error(error_label: Label): + error_label.visible = false + +# 验证用户名 +func validate_username(username: String) -> Dictionary: + var result = {"valid": false, "message": ""} + + if username.is_empty(): + result.message = "用户名不能为空" + return result + + if username.length() < 1 or username.length() > 50: + result.message = "用户名长度应为1-50字符" + return result + + # 检查用户名格式(字母、数字、下划线) + var regex = RegEx.new() + regex.compile("^[a-zA-Z0-9_]+$") + if not regex.search(username): + result.message = "用户名只能包含字母、数字和下划线" + return result + + result.valid = true + return result + +# 验证邮箱 +func validate_email(email: String) -> Dictionary: + var result = {"valid": false, "message": ""} + + if email.is_empty(): + result.message = "邮箱不能为空" + return result + + if not is_valid_email(email): + result.message = "请输入有效的邮箱地址" + return result + + result.valid = true + return result + +# 验证密码 +func validate_password(password: String) -> Dictionary: + var result = {"valid": false, "message": ""} + + if password.is_empty(): + result.message = "密码不能为空" + return result + + if password.length() < 8: + result.message = "密码长度至少8位" + return result + + if password.length() > 128: + result.message = "密码长度不能超过128位" + return result + + # 验证密码格式(必须包含字母和数字) + var has_letter = false + var has_digit = false + for i in range(password.length()): + var character = password[i] + if character >= 'a' and character <= 'z' or character >= 'A' and character <= 'Z': + has_letter = true + elif character >= '0' and character <= '9': + has_digit = true + + if not (has_letter and has_digit): + result.message = "密码必须包含字母和数字" + return result + + result.valid = true + return result + +# 验证确认密码 +func validate_confirm_password(password: String, confirm: String) -> Dictionary: + var result = {"valid": false, "message": ""} + + if confirm.is_empty(): + result.message = "确认密码不能为空" + return result + + if password != confirm: + result.message = "两次输入的密码不一致" + return result + + result.valid = true + return result + +# 验证验证码 +func validate_verification_code(code: String) -> Dictionary: + var result = {"valid": false, "message": ""} + + if code.is_empty(): + result.message = "验证码不能为空" + return result + + if code.length() != 6: + result.message = "验证码必须是6位数字" + return result + + # 验证是否为纯数字 + for i in range(code.length()): + var character = code[i] + if not (character >= '0' and character <= '9'): + result.message = "验证码必须是6位数字" + return result + + result.valid = true + return result + +# ============ 登录表单验证事件 ============ + +func _on_login_username_focus_exited(): + var username = login_username.text.strip_edges() + if username.is_empty(): + show_field_error(login_username_error, "用户名不能为空") + else: + hide_field_error(login_username_error) + +func _on_login_password_focus_exited(): + var password = login_password.text + if password.is_empty(): + show_field_error(login_password_error, "密码不能为空") + else: + hide_field_error(login_password_error) + +# ============ 注册表单验证事件 ============ + +func _on_register_username_focus_exited(): + var username = register_username.text.strip_edges() + var validation = validate_username(username) + if not validation.valid: + show_field_error(register_username_error, validation.message) + else: + hide_field_error(register_username_error) + +func _on_register_email_focus_exited(): + var email = register_email.text.strip_edges() + var validation = validate_email(email) + if not validation.valid: + show_field_error(register_email_error, validation.message) + else: + hide_field_error(register_email_error) + +func _on_register_password_focus_exited(): + var password = register_password.text + var validation = validate_password(password) + if not validation.valid: + show_field_error(register_password_error, validation.message) + else: + hide_field_error(register_password_error) + # 如果确认密码已填写,重新验证确认密码 + if not register_confirm.text.is_empty(): + _on_register_confirm_focus_exited() + +func _on_register_confirm_focus_exited(): + var password = register_password.text + var confirm = register_confirm.text + var validation = validate_confirm_password(password, confirm) + if not validation.valid: + show_field_error(register_confirm_error, validation.message) + else: + hide_field_error(register_confirm_error) + +func _on_verification_focus_exited(): + var code = verification_input.text.strip_edges() + var validation = validate_verification_code(code) + if not validation.valid: + show_field_error(verification_error, validation.message) + else: + hide_field_error(verification_error) + +# ============ 实时输入验证事件 ============ + +func _on_register_username_text_changed(new_text: String): + # 输入时隐藏错误提示 + if register_username_error.visible and not new_text.is_empty(): + hide_field_error(register_username_error) + +func _on_register_email_text_changed(new_text: String): + if register_email_error.visible and not new_text.is_empty(): + hide_field_error(register_email_error) + +func _on_register_password_text_changed(new_text: String): + if register_password_error.visible and not new_text.is_empty(): + hide_field_error(register_password_error) + +func _on_register_confirm_text_changed(new_text: String): + if register_confirm_error.visible and not new_text.is_empty(): + hide_field_error(register_confirm_error) + +func _on_verification_text_changed(new_text: String): + if verification_error.visible and not new_text.is_empty(): + hide_field_error(verification_error) + +# ============ 表单整体验证 ============ + +# 验证登录表单 +func validate_login_form() -> bool: + var is_valid = true + + var username = login_username.text.strip_edges() + var password = login_password.text + + if username.is_empty(): + show_field_error(login_username_error, "用户名不能为空") + is_valid = false + else: + hide_field_error(login_username_error) + + if password.is_empty(): + show_field_error(login_password_error, "密码不能为空") + is_valid = false + else: + hide_field_error(login_password_error) + + return is_valid + +# 验证注册表单 +func validate_register_form() -> bool: + print("开始验证注册表单") + var is_valid = true + + var username = register_username.text.strip_edges() + var email = register_email.text.strip_edges() + var password = register_password.text + var confirm = register_confirm.text + var verification_code = verification_input.text.strip_edges() + + print("表单数据: 用户名='%s', 邮箱='%s', 密码长度=%d, 确认密码长度=%d, 验证码='%s'" % [username, email, password.length(), confirm.length(), verification_code]) + + # 验证用户名 + var username_validation = validate_username(username) + if not username_validation.valid: + print("用户名验证失败: ", username_validation.message) + show_field_error(register_username_error, username_validation.message) + is_valid = false + else: + hide_field_error(register_username_error) + + # 验证邮箱 + var email_validation = validate_email(email) + if not email_validation.valid: + print("邮箱验证失败: ", email_validation.message) + show_field_error(register_email_error, email_validation.message) + is_valid = false + else: + hide_field_error(register_email_error) + + # 验证密码 + var password_validation = validate_password(password) + if not password_validation.valid: + print("密码验证失败: ", password_validation.message) + show_field_error(register_password_error, password_validation.message) + is_valid = false + else: + hide_field_error(register_password_error) + + # 验证确认密码 + var confirm_validation = validate_confirm_password(password, confirm) + if not confirm_validation.valid: + print("确认密码验证失败: ", confirm_validation.message) + show_field_error(register_confirm_error, confirm_validation.message) + is_valid = false + else: + hide_field_error(register_confirm_error) + + # 验证验证码 + var code_validation = validate_verification_code(verification_code) + if not code_validation.valid: + print("验证码格式验证失败: ", code_validation.message) + show_field_error(verification_error, code_validation.message) + is_valid = false + else: + hide_field_error(verification_error) + + # 检查是否已发送验证码(检查当前邮箱) + var current_email_input = register_email.text.strip_edges() + var has_sent_code = false + + if verification_codes_sent.has(current_email_input): + var email_data = verification_codes_sent[current_email_input] + has_sent_code = email_data.get("sent", false) + + if not has_sent_code: + print("当前邮箱验证码未发送,email = ", current_email_input) + show_toast("请先获取邮箱验证码", false) + is_valid = false + + print("表单验证结果: ", is_valid) + return is_valid + +func _on_to_register_pressed(): + show_toast('验证码登录功能待实现', false) + +func _on_forgot_password_pressed(): + show_toast('忘记密码功能待实现', false) + +func _on_register_link_pressed(): + show_register_panel() + +func _on_to_login_pressed(): + show_login_panel() + +func _on_login_enter(_text: String): + _on_login_pressed() + +func _input(event): + if event.is_action_pressed("ui_cancel"): + get_tree().quit()