forked from datawhale/whale-town-front
Compare commits
1 Commits
b1f3c0feff
...
feature/lo
| Author | SHA1 | Date | |
|---|---|---|---|
| d623c705b6 |
13
.claude/settings.local.json
Normal file
13
.claude/settings.local.json
Normal file
@@ -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:*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
15
.vscode/launch.json
vendored
Normal file
15
.vscode/launch.json
vendored
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"godotTools.editorPath.godot4": "d:\\software\\godot\\Godot_v4.5.1-stable_win64.exe"
|
||||||
|
}
|
||||||
BIN
assets/login/enter.png
Normal file
BIN
assets/login/enter.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 183 KiB |
40
assets/login/enter.png.import
Normal file
40
assets/login/enter.png.import
Normal file
@@ -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
|
||||||
BIN
assets/login/login-bg.png
Normal file
BIN
assets/login/login-bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 840 KiB |
40
assets/login/login-bg.png.import
Normal file
40
assets/login/login-bg.png.import
Normal file
@@ -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
|
||||||
BIN
assets/login/whaletown.png
Normal file
BIN
assets/login/whaletown.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 218 KiB |
40
assets/login/whaletown.png.import
Normal file
40
assets/login/whaletown.png.import
Normal file
@@ -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
|
||||||
@@ -11,5 +11,13 @@ config_version=5
|
|||||||
[application]
|
[application]
|
||||||
|
|
||||||
config/name="whaleTown"
|
config/name="whaleTown"
|
||||||
|
run/main_scene="res://scenes/login_scene.tscn"
|
||||||
config/features=PackedStringArray("4.5", "Forward Plus")
|
config/features=PackedStringArray("4.5", "Forward Plus")
|
||||||
config/icon="res://icon.svg"
|
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"
|
||||||
|
|||||||
196
scenes/login_scene.tscn
Normal file
196
scenes/login_scene.tscn
Normal file
@@ -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
|
||||||
40
scenes/main_menu_scene.tscn
Normal file
40
scenes/main_menu_scene.tscn
Normal file
@@ -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 = "退出登录"
|
||||||
45
scripts/data/ErrorCode.gd
Normal file
45
scripts/data/ErrorCode.gd
Normal file
@@ -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)
|
||||||
1
scripts/data/ErrorCode.gd.uid
Normal file
1
scripts/data/ErrorCode.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cfrgxhalop5rg
|
||||||
66
scripts/data/UserData.gd
Normal file
66
scripts/data/UserData.gd
Normal file
@@ -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]
|
||||||
1
scripts/data/UserData.gd.uid
Normal file
1
scripts/data/UserData.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bxcp8nyo8mha7
|
||||||
183
scripts/managers/AuthManager.gd
Normal file
183
scripts/managers/AuthManager.gd
Normal file
@@ -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分钟
|
||||||
1
scripts/managers/AuthManager.gd.uid
Normal file
1
scripts/managers/AuthManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dt434a8o6ye2p
|
||||||
98
scripts/managers/EncryptionService.gd
Normal file
98
scripts/managers/EncryptionService.gd
Normal file
@@ -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)
|
||||||
1
scripts/managers/EncryptionService.gd.uid
Normal file
1
scripts/managers/EncryptionService.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://d1dmca6vem1hn
|
||||||
282
scripts/managers/NetworkService.gd
Normal file
282
scripts/managers/NetworkService.gd
Normal file
@@ -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) + ")"
|
||||||
1
scripts/managers/NetworkService.gd.uid
Normal file
1
scripts/managers/NetworkService.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://ple3a8luflmb
|
||||||
124
scripts/managers/StorageService.gd
Normal file
124
scripts/managers/StorageService.gd
Normal file
@@ -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()
|
||||||
1
scripts/managers/StorageService.gd.uid
Normal file
1
scripts/managers/StorageService.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cotaeupq0tpea
|
||||||
217
scripts/ui/UI_Login.gd
Normal file
217
scripts/ui/UI_Login.gd
Normal file
@@ -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
|
||||||
1
scripts/ui/UI_Login.gd.uid
Normal file
1
scripts/ui/UI_Login.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://c6f1awwaw1nim
|
||||||
37
scripts/ui/UI_MainMenu.gd
Normal file
37
scripts/ui/UI_MainMenu.gd
Normal file
@@ -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")
|
||||||
1
scripts/ui/UI_MainMenu.gd.uid
Normal file
1
scripts/ui/UI_MainMenu.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cfpv68lwjwove
|
||||||
Reference in New Issue
Block a user