Files
whale-town-front/scripts/scenes/AuthScene.gd
moyin c6bcca4e7f feat:实现用户认证系统核心功能
- 实现用户登录和注册的完整流程
- 添加邮箱验证码发送和验证功能
- 实现基于邮箱地址的验证码冷却机制
- 添加表单验证和错误提示系统
- 集成Toast消息提示系统
- 支持网络请求处理和错误处理
- 实现按钮状态管理和加载状态显示
2025-12-24 20:37:00 +08:00

1099 lines
36 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()