diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..3c85312 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,13 @@ +{ + "permissions": { + "allow": [ + "WebFetch(domain:whaletownend.xinghangee.icu)", + "mcp__ide__getDiagnostics", + "Bash(dir /s /b *.gd)", + "Bash(findstr:*)", + "Bash(del nul)", + "Bash(git checkout:*)", + "Bash(git add:*)" + ] + } +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..df748fe --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "GDScript Godot", + "type": "godot", + "request": "launch", + "project": "${workspaceFolder}", + "port": 6007, + "address": "127.0.0.1", + "launch_game_instance": true, + "launch_scene": false + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..bcf848f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "godotTools.editorPath.godot4": "d:\\software\\godot\\Godot_v4.5.1-stable_win64.exe" +} diff --git a/assets/login/enter.png b/assets/login/enter.png new file mode 100644 index 0000000..1cfeb93 Binary files /dev/null and b/assets/login/enter.png differ diff --git a/assets/login/enter.png.import b/assets/login/enter.png.import new file mode 100644 index 0000000..ffb1888 --- /dev/null +++ b/assets/login/enter.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cswuqnhiiypb2" +path="res://.godot/imported/enter.png-ed3d67e95e66053ad0fb61054985bfb8.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/login/enter.png" +dest_files=["res://.godot/imported/enter.png-ed3d67e95e66053ad0fb61054985bfb8.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/login/login-bg.png b/assets/login/login-bg.png new file mode 100644 index 0000000..310ee1e Binary files /dev/null and b/assets/login/login-bg.png differ diff --git a/assets/login/login-bg.png.import b/assets/login/login-bg.png.import new file mode 100644 index 0000000..4f56997 --- /dev/null +++ b/assets/login/login-bg.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://byw73r2dt6xb8" +path="res://.godot/imported/login-bg.png-3f4c0c8160ab08ab0791dc1de267dd7a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/login/login-bg.png" +dest_files=["res://.godot/imported/login-bg.png-3f4c0c8160ab08ab0791dc1de267dd7a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/login/whaletown.png b/assets/login/whaletown.png new file mode 100644 index 0000000..a68a5a0 Binary files /dev/null and b/assets/login/whaletown.png differ diff --git a/assets/login/whaletown.png.import b/assets/login/whaletown.png.import new file mode 100644 index 0000000..23bdbbc --- /dev/null +++ b/assets/login/whaletown.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bqvel7n6dfo1d" +path="res://.godot/imported/whaletown.png-c44c9408e8b817d0db57fd8eec9de194.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/login/whaletown.png" +dest_files=["res://.godot/imported/whaletown.png-c44c9408e8b817d0db57fd8eec9de194.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/project.godot b/project.godot index 5645744..ae6ce57 100644 --- a/project.godot +++ b/project.godot @@ -11,5 +11,13 @@ config_version=5 [application] config/name="whaleTown" +run/main_scene="res://scenes/login_scene.tscn" config/features=PackedStringArray("4.5", "Forward Plus") config/icon="res://icon.svg" + +[autoload] + +EncryptionService="*res://scripts/managers/EncryptionService.gd" +StorageService="*res://scripts/managers/StorageService.gd" +NetworkService="*res://scripts/managers/NetworkService.gd" +AuthManager="*res://scripts/managers/AuthManager.gd" diff --git a/scenes/login_scene.tscn b/scenes/login_scene.tscn new file mode 100644 index 0000000..413eed5 --- /dev/null +++ b/scenes/login_scene.tscn @@ -0,0 +1,196 @@ +[gd_scene load_steps=5 format=3 uid="uid://bqj1ykrxo0x8s"] + +[ext_resource type="Script" uid="uid://c6f1awwaw1nim" path="res://scripts/ui/UI_Login.gd" id="1_p2w8r"] +[ext_resource type="Texture2D" uid="uid://byw73r2dt6xb8" path="res://assets/login/login-bg.png" id="2_login_bg"] +[ext_resource type="Texture2D" uid="uid://cswuqnhiiypb2" path="res://assets/login/enter.png" id="3_enter_btn"] +[ext_resource type="Texture2D" uid="uid://bqvel7n6dfo1d" path="res://assets/login/whaletown.png" id="4_whaletown"] + +[node name="loginScene" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_p2w8r") + +[node name="Background" type="ColorRect" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +color = Color(0.85098, 0.929412, 0.964706, 1) + +[node name="LoginPanel" type="TextureRect" parent="."] +layout_mode = 0 +offset_left = 250.0 +offset_top = 8.0 +offset_right = 1274.0 +offset_bottom = 1032.0 +scale = Vector2(0.6, 0.6) +texture = ExtResource("2_login_bg") +stretch_mode = 1 + +[node name="MainTitle" type="TextureRect" parent="LoginPanel"] +layout_mode = 0 +offset_left = 325.00003 +offset_top = 243.33331 +offset_right = 713.3334 +offset_bottom = 393.3333 +texture = ExtResource("4_whaletown") +expand_mode = 1 +stretch_mode = 5 + +[node name="SubTitle" type="Label" parent="LoginPanel"] +layout_mode = 0 +offset_left = 351.66666 +offset_top = 408.3333 +offset_right = 651.6666 +offset_bottom = 431.3333 +theme_override_colors/font_color = Color(0, 0, 0, 1) +text = "开始你的小镇之旅!" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="PasswordLoginTab" type="Button" parent="LoginPanel"] +layout_mode = 0 +offset_left = 355.0 +offset_top = 468.3333 +offset_right = 500.0 +offset_bottom = 503.3333 +toggle_mode = true +button_pressed = true +text = "密码登录" + +[node name="CodeLoginTab" type="Button" parent="LoginPanel"] +layout_mode = 0 +offset_left = 510.0 +offset_top = 468.3333 +offset_right = 655.0 +offset_bottom = 503.3333 +toggle_mode = true +text = "验证码登录" + +[node name="UsernameInput" type="LineEdit" parent="LoginPanel"] +layout_mode = 0 +offset_left = 355.0 +offset_top = 523.3333 +offset_right = 655.0 +offset_bottom = 563.3333 +placeholder_text = "用户名/手机/邮箱" +caret_blink = true + +[node name="PasswordInput" type="LineEdit" parent="LoginPanel"] +layout_mode = 0 +offset_left = 355.0 +offset_top = 578.3333 +offset_right = 615.0 +offset_bottom = 618.3333 +placeholder_text = "输入密码" +caret_blink = true +secret = true + +[node name="TogglePasswordButton" type="Button" parent="LoginPanel"] +layout_mode = 0 +offset_left = 615.0 +offset_top = 578.3333 +offset_right = 655.0 +offset_bottom = 618.3333 +text = "👁" + +[node name="RememberPasswordCheckBox" type="CheckBox" parent="LoginPanel"] +layout_mode = 0 +offset_left = 325.0 +offset_top = 703.3333 +offset_right = 425.0 +offset_bottom = 734.3333 +theme_override_colors/font_color = Color(0, 0, 0, 1) +text = "记住密码" + +[node name="AutoLoginCheckBox" type="CheckBox" parent="LoginPanel"] +layout_mode = 0 +offset_left = 575.0 +offset_top = 688.3333 +offset_right = 675.0 +offset_bottom = 719.3333 +theme_override_colors/font_color = Color(0, 0, 0, 1) +text = "自动登录" + +[node name="EnterButton" type="TextureButton" parent="LoginPanel"] +layout_mode = 0 +offset_left = 286.66666 +offset_top = 774.99994 +offset_right = 1243.6666 +offset_bottom = 958.99994 +scale = Vector2(0.5, 0.5) +texture_normal = ExtResource("3_enter_btn") +stretch_mode = 0 + +[node name="EnterButtonLabel" type="Label" parent="LoginPanel/EnterButton"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_font_sizes/font_size = 64 +text = "进入小镇" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="ForgotPasswordButton" type="LinkButton" parent="LoginPanel"] +layout_mode = 0 +offset_left = 328.3333 +offset_top = 893.3333 +offset_right = 428.3333 +offset_bottom = 918.3333 +theme_override_colors/font_color = Color(0, 0, 0, 1) +text = "忘记密码?" + +[node name="RegisterButton" type="LinkButton" parent="LoginPanel"] +layout_mode = 0 +offset_left = 595.0 +offset_top = 890.0 +offset_right = 695.0 +offset_bottom = 915.0 +theme_override_colors/font_color = Color(0, 0, 0, 1) +text = "注册居民身份" + +[node name="StatusLabel" type="Label" parent="LoginPanel"] +layout_mode = 0 +offset_left = 50.0 +offset_top = 480.0 +offset_right = 350.0 +offset_bottom = 510.0 +text = " " +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="LoadingOverlay" type="ColorRect" parent="."] +visible = false +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +color = Color(0, 0, 0, 0.5) + +[node name="LoadingLabel" type="Label" parent="LoadingOverlay"] +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -50.0 +offset_top = -11.5 +offset_right = 50.0 +offset_bottom = 11.5 +grow_horizontal = 2 +grow_vertical = 2 +text = "登录中..." +horizontal_alignment = 1 +vertical_alignment = 1 diff --git a/scenes/main_menu_scene.tscn b/scenes/main_menu_scene.tscn new file mode 100644 index 0000000..197b0c9 --- /dev/null +++ b/scenes/main_menu_scene.tscn @@ -0,0 +1,40 @@ +[gd_scene load_steps=2 format=3 uid="uid://c3mxv7d1qw7ba"] + +[ext_resource type="Script" path="res://scripts/ui/UI_MainMenu.gd" id="1_main_menu"] + +[node name="mainMenuScene" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_main_menu") + +[node name="centerContainer" type="CenterContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="vboxContainer" type="VBoxContainer" parent="centerContainer"] +layout_mode = 2 +theme_override_constants/separation = 20 + +[node name="welcomeLabel" type="Label" parent="centerContainer/vboxContainer"] +layout_mode = 2 +text = "欢迎来到鲸鱼小镇!" +horizontal_alignment = 1 + +[node name="userInfoLabel" type="Label" parent="centerContainer/vboxContainer"] +layout_mode = 2 +text = "用户名:-- +等级:--" +horizontal_alignment = 1 + +[node name="logoutButton" type="Button" parent="centerContainer/vboxContainer"] +custom_minimum_size = Vector2(200, 40) +layout_mode = 2 +text = "退出登录" diff --git a/scripts/data/ErrorCode.gd b/scripts/data/ErrorCode.gd new file mode 100644 index 0000000..f1de2bc --- /dev/null +++ b/scripts/data/ErrorCode.gd @@ -0,0 +1,45 @@ +class_name ErrorCode + +## 错误码定义 +## 用于统一管理登录系统中的所有错误码和错误消息 + +# 成功 +const SUCCESS: int = 0 + +# 网络相关错误 (1000-1999) +const NETWORK_ERROR: int = 1001 +const TIMEOUT: int = 1002 +const SERVER_ERROR: int = 1003 + +# 输入验证错误 (2000-2999) +const INVALID_INPUT: int = 2001 +const INVALID_USERNAME: int = 2002 +const INVALID_PASSWORD: int = 2003 +const PASSWORDS_NOT_MATCH: int = 2004 + +# 认证相关错误 (3000-3999) +const WRONG_CREDENTIALS: int = 3001 +const USER_NOT_FOUND: int = 3002 +const ACCOUNT_DISABLED: int = 3003 +const TOKEN_EXPIRED: int = 3004 +const TOKEN_INVALID: int = 3005 + +# 错误消息映射 +const MESSAGES: Dictionary = { + NETWORK_ERROR: "网络连接失败,请检查网络设置", + TIMEOUT: "请求超时,请稍后重试", + SERVER_ERROR: "服务器错误,请稍后重试", + INVALID_INPUT: "用户名或密码不能为空", + INVALID_USERNAME: "用户名格式不正确(3-16个字符)", + INVALID_PASSWORD: "密码格式不正确(6-20个字符)", + PASSWORDS_NOT_MATCH: "两次输入的密码不一致", + WRONG_CREDENTIALS: "用户名或密码错误", + USER_NOT_FOUND: "用户不存在", + ACCOUNT_DISABLED: "账号已被禁用", + TOKEN_EXPIRED: "登录已过期,请重新登录", + TOKEN_INVALID: "登录状态无效,请重新登录" +} + +## 获取错误消息 +static func getMessage(errorCode: int) -> String: + return MESSAGES.get(errorCode, "未知错误 (代码: %d)" % errorCode) diff --git a/scripts/data/ErrorCode.gd.uid b/scripts/data/ErrorCode.gd.uid new file mode 100644 index 0000000..977f8eb --- /dev/null +++ b/scripts/data/ErrorCode.gd.uid @@ -0,0 +1 @@ +uid://cfrgxhalop5rg diff --git a/scripts/data/UserData.gd b/scripts/data/UserData.gd new file mode 100644 index 0000000..6e61791 --- /dev/null +++ b/scripts/data/UserData.gd @@ -0,0 +1,66 @@ +class_name UserData + +## 用户数据类 +## 用于存储用户的基本信息 + +var id: String = "" +var username: String = "" +var nickname: String = "" +var email: String = "" +var phone: String = "" +var avatarUrl: String = "" +var role: int = 1 +var createdAt: String = "" + +# 兼容旧字段 +var level: int = 1 +var exp: int = 0 +var createTime: int = 0 + +## 从字典创建 UserData 对象 +static func fromDict(data: Dictionary) -> UserData: + var userData = UserData.new() + + # 新字段(处理可能为 null 的值) + userData.id = str(data.get("id", "")) + userData.username = _getStringOrEmpty(data, "username") + userData.nickname = _getStringOrEmpty(data, "nickname") + userData.email = _getStringOrEmpty(data, "email") + userData.phone = _getStringOrEmpty(data, "phone") + userData.avatarUrl = _getStringOrEmpty(data, "avatar_url", _getStringOrEmpty(data, "avatarUrl")) + userData.role = int(data.get("role", 1)) + userData.createdAt = _getStringOrEmpty(data, "created_at", _getStringOrEmpty(data, "createdAt")) + + # 兼容旧字段 + userData.level = data.get("level", 1) + userData.exp = data.get("exp", 0) + userData.createTime = data.get("create_time", data.get("createTime", 0)) + + return userData + +## 辅助方法:从字典中获取字符串,如果为 null 则返回空字符串 +static func _getStringOrEmpty(data: Dictionary, key: String, defaultValue: String = "") -> String: + var value = data.get(key, defaultValue) + if value == null: + return "" + return str(value) + +## 转换为字典 +func toDict() -> Dictionary: + return { + "id": id, + "username": username, + "nickname": nickname, + "email": email, + "phone": phone, + "avatar_url": avatarUrl, + "role": role, + "created_at": createdAt, + "level": level, + "exp": exp, + "create_time": createTime + } + +## 打印用户信息(用于调试) +func toString() -> String: + return "UserData(id=%s, username=%s, nickname=%s, role=%d)" % [id, username, nickname, role] diff --git a/scripts/data/UserData.gd.uid b/scripts/data/UserData.gd.uid new file mode 100644 index 0000000..64ead69 --- /dev/null +++ b/scripts/data/UserData.gd.uid @@ -0,0 +1 @@ +uid://bxcp8nyo8mha7 diff --git a/scripts/managers/AuthManager.gd b/scripts/managers/AuthManager.gd new file mode 100644 index 0000000..0eb645c --- /dev/null +++ b/scripts/managers/AuthManager.gd @@ -0,0 +1,183 @@ +extends Node + +## 认证管理器 +## 统一管理用户登录、登出、Token等认证相关功能 + +# 登录状态枚举 +enum LoginState { + NOT_LOGGED_IN, # 未登录 + LOGGING_IN, # 登录中 + LOGGED_IN, # 已登录 + GUEST # 游客模式 +} + +# 信号 +signal login_success(userData: UserData) +signal login_failed(errorCode: int, errorMessage: String) +signal logout_success() + +# 当前登录状态 +var currentState: LoginState = LoginState.NOT_LOGGED_IN + +# Token +var accessToken: String = "" +var tokenExpireTime: int = 0 + +# 当前用户信息 +var currentUser: UserData = null + +# 是否正在登录(防止重复请求) +var _isLoggingIn: bool = false + +func _ready(): + # 游戏启动时尝试从本地加载 Token 和用户信息 + _tryLoadLocalAuth() + +## 登录 +func login(username: String, password: String, rememberPassword: bool) -> Dictionary: + # 防止重复登录 + if _isLoggingIn: + return { + "success": false, + "error_code": ErrorCode.INVALID_INPUT, + "message": "登录中,请稍候..." + } + + _isLoggingIn = true + currentState = LoginState.LOGGING_IN + + # 构建登录请求数据 + var requestData = { + "identifier": username, + "password": password + } + + # 发送登录请求 + var response = await NetworkService.post("/api/auth/login", requestData) + + _isLoggingIn = false + + # 处理响应 + if response.success: + # 解析用户数据和 Token + var data = response.get("data", {}) + var token = data.get("access_token", "") + var userData = data.get("user", {}) + + if token.is_empty(): + var errorMsg = "服务器返回数据格式错误" + login_failed.emit(ErrorCode.SERVER_ERROR, errorMsg) + currentState = LoginState.NOT_LOGGED_IN + return { + "success": false, + "error_code": ErrorCode.SERVER_ERROR, + "message": errorMsg + } + + # 保存 Token + accessToken = token + tokenExpireTime = int(Time.get_unix_time_from_system()) + 3600 * 24 # 假设24小时过期 + StorageService.saveToken(accessToken, tokenExpireTime) + + # 保存用户信息 + currentUser = UserData.fromDict(userData) + StorageService.saveUserData(currentUser) + + # 如果勾选记住密码,保存账号密码 + if rememberPassword: + StorageService.saveAccount(username, password, true) + else: + # 只保存用户名,不保存密码 + StorageService.save("account", "last_username", username) + StorageService.clearSavedPassword() + + # 更新状态 + currentState = LoginState.LOGGED_IN + + # 发送登录成功信号 + login_success.emit(currentUser) + + return { + "success": true, + "message": "登录成功", + "user": currentUser + } + else: + # 登录失败 + var errorCode = response.get("error_code", ErrorCode.WRONG_CREDENTIALS) + var errorMsg = response.get("message", "登录失败") + + currentState = LoginState.NOT_LOGGED_IN + login_failed.emit(errorCode, errorMsg) + + return { + "success": false, + "error_code": errorCode, + "message": errorMsg + } + +## 登出 +func logout() -> void: + # 清除 Token 和用户信息 + accessToken = "" + tokenExpireTime = 0 + currentUser = null + + # 清除本地存储(保留用户名和记住密码设置) + StorageService.clearToken() + + # 更新状态 + currentState = LoginState.NOT_LOGGED_IN + + # 发送登出成功信号 + logout_success.emit() + + print("用户已登出") + +## 检查是否已登录 +func isLoggedIn() -> bool: + return currentState == LoginState.LOGGED_IN and currentUser != null + +## 获取当前用户 +func getCurrentUser() -> UserData: + return currentUser + +## 获取当前 Token +func getToken() -> String: + return accessToken + +## 尝试从本地加载认证信息 +func _tryLoadLocalAuth() -> void: + # 加载 Token + var tokenData = StorageService.loadToken() + var token = tokenData.get("token", "") + var expireTime = tokenData.get("expire_time", 0) + + # 检查 Token 是否有效 + var currentTime = int(Time.get_unix_time_from_system()) + if token.is_empty() or expireTime <= currentTime: + print("本地 Token 无效或已过期") + return + + # Token 有效,加载用户信息 + var userData = StorageService.loadUserData() + if userData == null: + print("本地用户信息不存在") + return + + # 恢复登录状态 + accessToken = token + tokenExpireTime = expireTime + currentUser = userData + currentState = LoginState.LOGGED_IN + + print("从本地恢复登录状态: ", currentUser.username) + + # 可选:静默刷新 Token(如果接近过期) + # _refreshTokenIfNeeded() + +## 检查 Token 是否即将过期 +func isTokenExpiring() -> bool: + var currentTime = int(Time.get_unix_time_from_system()) + var timeLeft = tokenExpireTime - currentTime + return timeLeft < 300 # 少于5分钟 diff --git a/scripts/managers/AuthManager.gd.uid b/scripts/managers/AuthManager.gd.uid new file mode 100644 index 0000000..fb74f72 --- /dev/null +++ b/scripts/managers/AuthManager.gd.uid @@ -0,0 +1 @@ +uid://dt434a8o6ye2p diff --git a/scripts/managers/EncryptionService.gd b/scripts/managers/EncryptionService.gd new file mode 100644 index 0000000..306b79d --- /dev/null +++ b/scripts/managers/EncryptionService.gd @@ -0,0 +1,98 @@ +extends Node + +## 加密服务 +## 提供密码加密、解密等安全功能 + +# 固定的 Salt(用于生成设备密钥) +const FIXED_SALT: String = "whaleTown_2024_secret_key" + +# AES 加密密钥(基于设备ID生成) +var _encryptionKey: PackedByteArray + +func _ready(): + _initEncryptionKey() + +## 初始化加密密钥 +func _initEncryptionKey() -> void: + # 使用设备唯一ID + 固定Salt 生成加密密钥 + var deviceId = OS.get_unique_id() + var keyString = deviceId + FIXED_SALT + + # 使用 SHA-256 生成 32 字节密钥 + var crypto = Crypto.new() + var hash = crypto.generate_random_bytes(32) # 临时方案,实际应该用 hash + + # 简单实现:取字符串的 MD5 哈希的前32字节 + _encryptionKey = keyString.md5_buffer() + # 如果需要32字节,重复一次 + _encryptionKey.append_array(keyString.md5_buffer()) + +## 加密字符串(用于本地存储密码) +func encrypt(plainText: String) -> String: + if plainText.is_empty(): + return "" + + var crypto = Crypto.new() + var aes = AESContext.new() + + # 生成随机 IV(初始化向量) + var iv = crypto.generate_random_bytes(16) + + # 转换明文为字节 + var plainBytes = plainText.to_utf8_buffer() + + # 使用 AES-256-CBC 加密 + aes.start(AESContext.MODE_CBC_ENCRYPT, _encryptionKey.slice(0, 32), iv) + var encryptedBytes = aes.update(plainBytes) + aes.finish() + + # 将 IV 和加密数据组合(IV + encrypted_data) + var combined = PackedByteArray() + combined.append_array(iv) + combined.append_array(encryptedBytes) + + # 转换为 Base64 字符串 + return Marshalls.raw_to_base64(combined) + +## 解密字符串 +func decrypt(encryptedText: String) -> String: + if encryptedText.is_empty(): + return "" + + # Base64 解码 + var combined = Marshalls.base64_to_raw(encryptedText) + if combined.size() < 16: + push_error("加密数据格式错误") + return "" + + # 分离 IV 和加密数据 + var iv = combined.slice(0, 16) + var encryptedBytes = combined.slice(16) + + # 解密 + var aes = AESContext.new() + aes.start(AESContext.MODE_CBC_DECRYPT, _encryptionKey.slice(0, 32), iv) + var decryptedBytes = aes.update(encryptedBytes) + aes.finish() + + # 转换为字符串 + return decryptedBytes.get_string_from_utf8() + +## 生成密码哈希(用于网络传输) +## 注意:这是简单实现,实际应该使用更安全的算法(如 bcrypt, scrypt) +func hashPassword(password: String, salt: String = "") -> String: + if password.is_empty(): + return "" + + var combined = password + salt + return combined.sha256_text() + +## 验证密码哈希 +func verifyPassword(password: String, hashedPassword: String, salt: String = "") -> bool: + return hashPassword(password, salt) == hashedPassword + +## 生成随机 Salt +func generateSalt(length: int = 16) -> String: + var crypto = Crypto.new() + var randomBytes = crypto.generate_random_bytes(length) + return Marshalls.raw_to_base64(randomBytes) diff --git a/scripts/managers/EncryptionService.gd.uid b/scripts/managers/EncryptionService.gd.uid new file mode 100644 index 0000000..768746f --- /dev/null +++ b/scripts/managers/EncryptionService.gd.uid @@ -0,0 +1 @@ +uid://d1dmca6vem1hn diff --git a/scripts/managers/NetworkService.gd b/scripts/managers/NetworkService.gd new file mode 100644 index 0000000..52bb3ab --- /dev/null +++ b/scripts/managers/NetworkService.gd @@ -0,0 +1,282 @@ +extends Node + +## 网络服务 +## 封装 HTTP 请求,提供统一的网络通信接口 + +# API 基础 URL +const API_BASE_URL: String = "https://whaletownui.angforever.top" + +# 请求超时时间(秒) +const REQUEST_TIMEOUT: float = 30.0 + +# 最大重试次数 +const MAX_RETRY: int = 3 + +# 是否开启调试日志(显示请求和响应详情) +const DEBUG_MODE: bool = true + +## 发送 POST 请求 +func post(endpoint: String, data: Dictionary, headers: Array = []) -> Dictionary: + var url = API_BASE_URL + endpoint + return await _request(url, HTTPClient.METHOD_POST, data, headers) + +## 发送 GET 请求 +func sendGet(endpoint: String, headers: Array = []) -> Dictionary: + var url = API_BASE_URL + endpoint + return await _request(url, HTTPClient.METHOD_GET, {}, headers) + +## 发送带 Token 的 POST 请求 +func authenticatedPost(endpoint: String, data: Dictionary) -> Dictionary: + var tokenData = StorageService.loadToken() + var token = tokenData.get("token", "") + + if token.is_empty(): + return { + "success": false, + "error_code": ErrorCode.TOKEN_INVALID, + "message": ErrorCode.getMessage(ErrorCode.TOKEN_INVALID) + } + + var headers = [ + "Authorization: Bearer " + token + ] + + return await post(endpoint, data, headers) + +## 核心请求方法 +func _request(url: String, method: HTTPClient.Method, data: Dictionary, customHeaders: Array) -> Dictionary: + # 创建 HTTPRequest 节点 + var httpRequest = HTTPRequest.new() + add_child(httpRequest) + + # 设置超时 + httpRequest.timeout = REQUEST_TIMEOUT + + # 构建请求头 + var headers = [ + "Content-Type: application/json", + "Accept: application/json" + ] + headers.append_array(customHeaders) + + # 准备请求体 + var body = "" + if method == HTTPClient.METHOD_POST or method == HTTPClient.METHOD_PUT: + body = JSON.stringify(data) + + # 调试日志:请求信息 + if DEBUG_MODE: + _logRequest(url, method, headers, body) + + # 发送请求 + var error = httpRequest.request(url, headers, method, body) + + if error != OK: + httpRequest.queue_free() + return _createErrorResponse(ErrorCode.NETWORK_ERROR, "发送请求失败") + + # 等待响应 + var result = await httpRequest.request_completed + + # 调试日志:响应信息 + if DEBUG_MODE: + _logResponse(result) + + # 清理 + httpRequest.queue_free() + + # 解析响应 + return _parseResponse(result) + +## 解析 HTTP 响应 +func _parseResponse(result: Array) -> Dictionary: + var httpResult: int = result[0] + var responseCode: int = result[1] + var _headers: PackedStringArray = result[2] + var body: PackedByteArray = result[3] + + # 检查 HTTP 请求错误 + if httpResult != HTTPRequest.RESULT_SUCCESS: + return _handleHttpError(httpResult) + + # 检查 HTTP 状态码 + if responseCode < 200 or responseCode >= 300: + return _handleStatusCodeError(responseCode, body) + + # 解析 JSON 响应体 + var bodyString = body.get_string_from_utf8() + if bodyString.is_empty(): + return _createErrorResponse(ErrorCode.SERVER_ERROR, "服务器返回空响应") + + var json = JSON.new() + var parseError = json.parse(bodyString) + + if parseError != OK: + push_error("JSON 解析失败: " + bodyString) + return _createErrorResponse(ErrorCode.SERVER_ERROR, "响应数据格式错误") + + var responseData = json.data + + # 根据后端API格式解析(通用格式,后续可能需要调整) + if typeof(responseData) == TYPE_DICTIONARY: + # 检查是否有 success 字段(新版 API 格式) + if responseData.has("success"): + var success = responseData.get("success", false) + if success: + return { + "success": true, + "data": responseData.get("data", {}), + "message": responseData.get("message", "操作成功") + } + else: + # 业务错误 + return { + "success": false, + "error_code": responseData.get("error_code", ErrorCode.SERVER_ERROR), + "message": responseData.get("message", "操作失败") + } + # 检查是否有 code 字段(旧版 API 格式) + elif responseData.has("code"): + var code = responseData.get("code", -1) + if code == 0 or code == 200: # 成功 + return { + "success": true, + "data": responseData.get("data", {}), + "message": responseData.get("message", "操作成功") + } + else: # 业务错误 + return { + "success": false, + "error_code": code, + "message": responseData.get("message", "操作失败") + } + else: + # 假设整个响应就是数据 + return { + "success": true, + "data": responseData, + "message": "操作成功" + } + + return _createErrorResponse(ErrorCode.SERVER_ERROR, "响应格式不正确") + +## 处理 HTTP 请求错误 +func _handleHttpError(httpResult: int) -> Dictionary: + match httpResult: + HTTPRequest.RESULT_TIMEOUT: + return _createErrorResponse(ErrorCode.TIMEOUT, "请求超时") + HTTPRequest.RESULT_CONNECTION_ERROR: + return _createErrorResponse(ErrorCode.NETWORK_ERROR, "网络连接失败") + HTTPRequest.RESULT_CANT_CONNECT: + return _createErrorResponse(ErrorCode.NETWORK_ERROR, "无法连接到服务器") + _: + return _createErrorResponse(ErrorCode.NETWORK_ERROR, "网络请求失败: " + str(httpResult)) + +## 处理 HTTP 状态码错误 +func _handleStatusCodeError(statusCode: int, body: PackedByteArray) -> Dictionary: + var bodyString = body.get_string_from_utf8() + + # 尝试解析错误信息 + var json = JSON.new() + if json.parse(bodyString) == OK: + var data = json.data + if typeof(data) == TYPE_DICTIONARY and data.has("message"): + var message = data.get("message", "") + # 确保 message 是字符串类型 + if typeof(message) == TYPE_ARRAY: + message = ", ".join(message) + elif typeof(message) != TYPE_STRING: + message = str(message) + return _createErrorResponse(statusCode, message) + + # 通用状态码错误处理 + match statusCode: + 400: + return _createErrorResponse(statusCode, "请求参数错误") + 401: + return _createErrorResponse(statusCode, "未授权,请重新登录") + 403: + return _createErrorResponse(statusCode, "没有权限访问") + 404: + return _createErrorResponse(statusCode, "请求的资源不存在") + 500: + return _createErrorResponse(statusCode, "服务器内部错误") + 502: + return _createErrorResponse(statusCode, "网关错误") + 503: + return _createErrorResponse(statusCode, "服务暂时不可用") + _: + return _createErrorResponse(statusCode, "HTTP 错误: " + str(statusCode)) + +## 创建错误响应 +func _createErrorResponse(errorCode: int, message: String) -> Dictionary: + return { + "success": false, + "error_code": errorCode, + "message": message + } + +## 记录请求日志 +func _logRequest(url: String, method: HTTPClient.Method, headers: Array, body: String) -> void: + print("\n========== HTTP 请求 ==========") + print("URL: ", url) + print("方法: ", _getMethodName(method)) + print("请求头:") + for header in headers: + print(" ", header) + if not body.is_empty(): + print("请求体:") + # 尝试格式化 JSON + var json = JSON.new() + if json.parse(body) == OK: + print(" ", JSON.stringify(json.data, " ")) + else: + print(" ", body) + print("==============================\n") + +## 记录响应日志 +func _logResponse(result: Array) -> void: + var httpResult: int = result[0] + var responseCode: int = result[1] + var responseHeaders: PackedStringArray = result[2] + var body: PackedByteArray = result[3] + + print("\n========== HTTP 响应 ==========") + print("请求结果: ", _getHttpResultName(httpResult)) + print("状态码: ", responseCode) + print("响应头:") + for header in responseHeaders: + print(" ", header) + + var bodyString = body.get_string_from_utf8() + if not bodyString.is_empty(): + print("响应体:") + # 尝试格式化 JSON + var json = JSON.new() + if json.parse(bodyString) == OK: + print(" ", JSON.stringify(json.data, " ")) + else: + print(" ", bodyString) + else: + print("响应体: (空)") + print("==============================\n") + +## 获取 HTTP 方法名称 +func _getMethodName(method: HTTPClient.Method) -> String: + match method: + HTTPClient.METHOD_GET: return "GET" + HTTPClient.METHOD_POST: return "POST" + HTTPClient.METHOD_PUT: return "PUT" + HTTPClient.METHOD_DELETE: return "DELETE" + HTTPClient.METHOD_PATCH: return "PATCH" + _: return "UNKNOWN" + +## 获取 HTTP 请求结果名称 +func _getHttpResultName(result: int) -> String: + match result: + HTTPRequest.RESULT_SUCCESS: return "成功" + HTTPRequest.RESULT_TIMEOUT: return "超时" + HTTPRequest.RESULT_CONNECTION_ERROR: return "连接错误" + HTTPRequest.RESULT_CANT_CONNECT: return "无法连接" + HTTPRequest.RESULT_NO_RESPONSE: return "无响应" + _: return "错误 (" + str(result) + ")" diff --git a/scripts/managers/NetworkService.gd.uid b/scripts/managers/NetworkService.gd.uid new file mode 100644 index 0000000..ae94ab2 --- /dev/null +++ b/scripts/managers/NetworkService.gd.uid @@ -0,0 +1 @@ +uid://ple3a8luflmb diff --git a/scripts/managers/StorageService.gd b/scripts/managers/StorageService.gd new file mode 100644 index 0000000..c8182f6 --- /dev/null +++ b/scripts/managers/StorageService.gd @@ -0,0 +1,124 @@ +extends Node + +## 存储服务 +## 负责本地配置的读写,包括用户账号、密码、Token 等 + +# 配置文件路径 +const CONFIG_PATH: String = "user://user_config.cfg" + +# ConfigFile 实例 +var _config: ConfigFile + +func _ready(): + _config = ConfigFile.new() + _loadConfig() + +## 加载配置文件 +func _loadConfig() -> void: + var error = _config.load(CONFIG_PATH) + if error != OK and error != ERR_FILE_NOT_FOUND: + push_error("加载配置文件失败: " + str(error)) + +## 保存配置文件 +func _saveConfig() -> void: + var error = _config.save(CONFIG_PATH) + if error != OK: + push_error("保存配置文件失败: " + str(error)) + +## 保存键值对 +func save(section: String, key: String, value: Variant) -> void: + _config.set_value(section, key, value) + _saveConfig() + +## 读取值 +func getValue(section: String, key: String, defaultValue: Variant = null) -> Variant: + return _config.get_value(section, key, defaultValue) + +## 保存账号信息(包括加密的密码) +func saveAccount(username: String, password: String, remember: bool) -> void: + save("account", "last_username", username) + + if remember and not password.is_empty(): + # 加密密码后保存 + var encryptedPassword = EncryptionService.encrypt(password) + save("account", "saved_password", encryptedPassword) + save("account", "remember_password", true) + else: + # 不记住密码,清除保存的密码 + clearSavedPassword() + +## 读取保存的账号信息 +func loadAccount() -> Dictionary: + var username = getValue("account", "last_username", "") + var rememberPassword = getValue("account", "remember_password", false) + var encryptedPassword = getValue("account", "saved_password", "") + + var result = { + "username": username, + "remember_password": rememberPassword + } + + # 如果记住密码,解密密码 + if rememberPassword and not encryptedPassword.is_empty(): + var decryptedPassword = EncryptionService.decrypt(encryptedPassword) + result["password"] = decryptedPassword + else: + result["password"] = "" + + return result + +## 清除保存的密码 +func clearSavedPassword() -> void: + save("account", "saved_password", "") + save("account", "remember_password", false) + +## 保存 Token +func saveToken(token: String, expireTime: int = 0) -> void: + save("auth", "token", token) + save("auth", "expire_time", expireTime) + +## 读取 Token +func loadToken() -> Dictionary: + var token = getValue("auth", "token", "") + var expireTime = getValue("auth", "expire_time", 0) + + return { + "token": token, + "expire_time": expireTime + } + +## 清除 Token +func clearToken() -> void: + save("auth", "token", "") + save("auth", "expire_time", 0) + +## 保存用户信息 +func saveUserData(userData: UserData) -> void: + if userData == null: + return + + save("user", "id", userData.id) + save("user", "username", userData.username) + save("user", "email", userData.email) + save("user", "level", userData.level) + save("user", "exp", userData.exp) + save("user", "avatar_url", userData.avatarUrl) + +## 读取用户信息 +func loadUserData() -> UserData: + var userData = UserData.new() + userData.id = str(getValue("user", "id", "")) + userData.username = getValue("user", "username", "") + userData.email = getValue("user", "email", "") + userData.level = getValue("user", "level", 1) + userData.exp = getValue("user", "exp", 0) + userData.avatarUrl = getValue("user", "avatar_url", "") + + return userData if not userData.id.is_empty() else null + +## 清除所有用户数据 +func clearAll() -> void: + clearSavedPassword() + clearToken() + _config.clear() + _saveConfig() diff --git a/scripts/managers/StorageService.gd.uid b/scripts/managers/StorageService.gd.uid new file mode 100644 index 0000000..c84f295 --- /dev/null +++ b/scripts/managers/StorageService.gd.uid @@ -0,0 +1 @@ +uid://cotaeupq0tpea diff --git a/scripts/ui/UI_Login.gd b/scripts/ui/UI_Login.gd new file mode 100644 index 0000000..b2d6c9d --- /dev/null +++ b/scripts/ui/UI_Login.gd @@ -0,0 +1,217 @@ +extends Control + +## 登录界面控制器 +## 处理登录界面的用户交互、输入验证、错误提示等 + +# UI 节点引用 +@onready var passwordLoginTab: Button = $LoginPanel/PasswordLoginTab +@onready var codeLoginTab: Button = $LoginPanel/CodeLoginTab +@onready var usernameInput: LineEdit = $LoginPanel/UsernameInput +@onready var passwordInput: LineEdit = $LoginPanel/PasswordInput +@onready var togglePasswordButton: Button = $LoginPanel/TogglePasswordButton +@onready var rememberPasswordCheckBox: CheckBox = $LoginPanel/RememberPasswordCheckBox +@onready var autoLoginCheckBox: CheckBox = $LoginPanel/AutoLoginCheckBox +@onready var enterButton: TextureButton = $LoginPanel/EnterButton +@onready var forgotPasswordButton: LinkButton = $LoginPanel/ForgotPasswordButton +@onready var registerButton: LinkButton = $LoginPanel/RegisterButton +@onready var statusLabel: Label = $LoginPanel/StatusLabel +@onready var loadingOverlay: ColorRect = $LoadingOverlay +@onready var loadingLabel: Label = $LoadingOverlay/LoadingLabel + +# 当前登录模式 +enum LoginMode { + PASSWORD, # 密码登录 + CODE # 验证码登录 +} +var currentMode: LoginMode = LoginMode.PASSWORD + +func _ready(): + # 连接Tab按钮信号 + passwordLoginTab.pressed.connect(_onPasswordTabPressed) + codeLoginTab.pressed.connect(_onCodeTabPressed) + + # 连接主要按钮信号 + enterButton.pressed.connect(_onEnterButtonPressed) + togglePasswordButton.pressed.connect(_onTogglePasswordPressed) + + # 连接底部链接按钮 + forgotPasswordButton.pressed.connect(_onForgotPasswordPressed) + registerButton.pressed.connect(_onRegisterPressed) + + # 连接 AuthManager 信号 + AuthManager.login_success.connect(_onLoginSuccess) + AuthManager.login_failed.connect(_onLoginFailed) + + # 连接输入框回车事件 + usernameInput.text_submitted.connect(_onUsernameSubmitted) + passwordInput.text_submitted.connect(_onPasswordSubmitted) + + # 自动填充记住的账号密码 + _loadSavedAccount() + + # 初始化状态 + _hideLoading() + _clearStatus() + _updateLoginMode() + +## Tab切换 - 密码登录 +func _onPasswordTabPressed() -> void: + if currentMode != LoginMode.PASSWORD: + currentMode = LoginMode.PASSWORD + codeLoginTab.button_pressed = false + passwordLoginTab.button_pressed = true + _updateLoginMode() + +## Tab切换 - 验证码登录 +func _onCodeTabPressed() -> void: + if currentMode != LoginMode.CODE: + currentMode = LoginMode.CODE + passwordLoginTab.button_pressed = false + codeLoginTab.button_pressed = true + _updateLoginMode() + +## 更新登录模式UI +func _updateLoginMode() -> void: + match currentMode: + LoginMode.PASSWORD: + passwordInput.placeholder_text = "输入密码" + passwordInput.secret = true + togglePasswordButton.visible = true + LoginMode.CODE: + passwordInput.placeholder_text = "输入验证码" + passwordInput.secret = false + togglePasswordButton.visible = false + +## 切换密码显示/隐藏 +func _onTogglePasswordPressed() -> void: + passwordInput.secret = !passwordInput.secret + togglePasswordButton.text = "👁" if passwordInput.secret else "🔒" + +## 加载保存的账号密码 +func _loadSavedAccount() -> void: + var savedAccount = StorageService.loadAccount() + + var username = savedAccount.get("username", "") + var password = savedAccount.get("password", "") + var rememberPassword = savedAccount.get("remember_password", false) + + # 设置用户名 + usernameInput.text = username + + if rememberPassword and not password.is_empty(): + passwordInput.text = password + rememberPasswordCheckBox.button_pressed = true + else: + rememberPasswordCheckBox.button_pressed = false + +## 进入小镇按钮点击事件 +func _onEnterButtonPressed() -> void: + match currentMode: + LoginMode.PASSWORD: + _handlePasswordLogin() + LoginMode.CODE: + _handleCodeLogin() + +## 处理密码登录 +func _handlePasswordLogin() -> void: + # 验证输入 + var validation = _validateInput() + if not validation.valid: + _showError(validation.error_code) + return + + # 显示加载状态 + _showLoading("登录中...") + + # 获取输入 + var username = usernameInput.text.strip_edges() + var password = passwordInput.text + var remember = rememberPasswordCheckBox.button_pressed + + # 调用 AuthManager 登录 + var result = await AuthManager.login(username, password, remember) + + # 隐藏加载状态 + _hideLoading() + +## 处理验证码登录 +func _handleCodeLogin() -> void: + # TODO: 实现验证码登录逻辑 + _showError(ErrorCode.SERVER_ERROR, "验证码登录功能暂未开放") + +## 用户名输入框回车事件 +func _onUsernameSubmitted(_text: String) -> void: + passwordInput.grab_focus() + +## 密码输入框回车事件 +func _onPasswordSubmitted(_text: String) -> void: + _onEnterButtonPressed() + +## 忘记密码按钮点击 +func _onForgotPasswordPressed() -> void: + # TODO: 跳转到忘记密码页面 + _showError(ErrorCode.SERVER_ERROR, "忘记密码功能暂未开放") + +## 注册按钮点击 +func _onRegisterPressed() -> void: + # TODO: 跳转到注册页面 + _showError(ErrorCode.SERVER_ERROR, "注册功能暂未开放") + +## 登录成功回调 +func _onLoginSuccess(userData: UserData) -> void: + _hideLoading() + _showSuccess("登录成功!") + + # 延迟0.5秒后跳转到主菜单 + await get_tree().create_timer(0.5).timeout + get_tree().change_scene_to_file("res://scenes/main_menu_scene.tscn") + +## 登录失败回调 +func _onLoginFailed(errorCode: int, errorMessage: String) -> void: + _hideLoading() + _showError(errorCode, errorMessage) + +## 验证输入 +func _validateInput() -> Dictionary: + var username = usernameInput.text.strip_edges() + var password = passwordInput.text + + # 检查是否为空 + if username.is_empty() or password.is_empty(): + return { + "valid": false, + "error_code": ErrorCode.INVALID_INPUT + } + + return {"valid": true} + +## 显示加载状态 +func _showLoading(message: String = "加载中...") -> void: + loadingLabel.text = message + loadingOverlay.visible = true + enterButton.disabled = true + +## 隐藏加载状态 +func _hideLoading() -> void: + loadingOverlay.visible = false + enterButton.disabled = false + +## 显示错误提示 +func _showError(errorCode: int, customMessage: String = "") -> void: + var message = customMessage if not customMessage.is_empty() else ErrorCode.getMessage(errorCode) + statusLabel.text = message + statusLabel.modulate = Color.RED + + # 3秒后自动清除 + await get_tree().create_timer(3.0).timeout + _clearStatus() + +## 显示成功提示 +func _showSuccess(message: String) -> void: + statusLabel.text = message + statusLabel.modulate = Color.GREEN + +## 清除状态提示 +func _clearStatus() -> void: + statusLabel.text = " " + statusLabel.modulate = Color.WHITE diff --git a/scripts/ui/UI_Login.gd.uid b/scripts/ui/UI_Login.gd.uid new file mode 100644 index 0000000..7ca5311 --- /dev/null +++ b/scripts/ui/UI_Login.gd.uid @@ -0,0 +1 @@ +uid://c6f1awwaw1nim diff --git a/scripts/ui/UI_MainMenu.gd b/scripts/ui/UI_MainMenu.gd new file mode 100644 index 0000000..d103eb7 --- /dev/null +++ b/scripts/ui/UI_MainMenu.gd @@ -0,0 +1,37 @@ +extends Control + +## 主菜单界面控制器 +## 显示用户信息并提供退出登录功能 + +@onready var welcomeLabel: Label = $centerContainer/vboxContainer/welcomeLabel +@onready var userInfoLabel: Label = $centerContainer/vboxContainer/userInfoLabel +@onready var logoutButton: Button = $centerContainer/vboxContainer/logoutButton + +func _ready(): + # 连接退出登录按钮 + logoutButton.pressed.connect(_onLogoutPressed) + + # 连接 AuthManager 信号 + AuthManager.logout_success.connect(_onLogoutSuccess) + + # 显示用户信息 + _displayUserInfo() + +## 显示用户信息 +func _displayUserInfo() -> void: + var currentUser = AuthManager.getCurrentUser() + + if currentUser != null: + userInfoLabel.text = "用户名:%s\n等级:%d" % [currentUser.username, currentUser.level] + else: + userInfoLabel.text = "未登录" + +## 退出登录按钮点击事件 +func _onLogoutPressed() -> void: + # 调用 AuthManager 登出 + AuthManager.logout() + +## 登出成功回调 +func _onLogoutSuccess() -> void: + # 返回登录界面 + get_tree().change_scene_to_file("res://scenes/login_scene.tscn") diff --git a/scripts/ui/UI_MainMenu.gd.uid b/scripts/ui/UI_MainMenu.gd.uid new file mode 100644 index 0000000..685bd93 --- /dev/null +++ b/scripts/ui/UI_MainMenu.gd.uid @@ -0,0 +1 @@ +uid://cfpv68lwjwove