登录模块
This commit is contained in:
9
client/assets/scripts/App/Login.meta
Normal file
9
client/assets/scripts/App/Login.meta
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "1.2.0",
|
||||||
|
"importer": "directory",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "6cbaee3d-3077-4a01-96f8-bf0ffd8225b0",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
390
client/assets/scripts/App/Login/README.md
Normal file
390
client/assets/scripts/App/Login/README.md
Normal file
@@ -0,0 +1,390 @@
|
|||||||
|
# 登录模块 (App/Login)
|
||||||
|
|
||||||
|
## 📋 模块概述
|
||||||
|
用户登录功能模块,包括登录界面 UI 和登录业务逻辑,登录成功后切换到游戏状态。
|
||||||
|
|
||||||
|
## 🎯 核心特性
|
||||||
|
- ✅ 登录界面 UI
|
||||||
|
- ✅ 账号输入处理
|
||||||
|
- ✅ 登录按钮交互
|
||||||
|
- ✅ 网络登录请求
|
||||||
|
- ✅ 状态切换
|
||||||
|
- ✅ 错误处理
|
||||||
|
|
||||||
|
## 🗂️ 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
App/Login/
|
||||||
|
└── UILogin.ts # 登录界面组件
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📘 核心类详解
|
||||||
|
|
||||||
|
### UILogin - 登录界面
|
||||||
|
|
||||||
|
**职责**: 显示登录 UI,处理用户输入,发送登录请求
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@ccclass('UILogin')
|
||||||
|
class UILogin extends UIBase {
|
||||||
|
private _inputAccount: EditBox | null; // 账号输入框
|
||||||
|
private _btnLogin: Node | null; // 登录按钮
|
||||||
|
private _isLogging: boolean; // 是否正在登录中
|
||||||
|
|
||||||
|
// 获取 UI 资源路径
|
||||||
|
onGetUrl(): string {
|
||||||
|
return 'res/UI/Login/UILogin';
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI 初始化
|
||||||
|
async onStart(params?: any): Promise<void>;
|
||||||
|
|
||||||
|
// 查找 UI 节点
|
||||||
|
private findNodes(): void;
|
||||||
|
|
||||||
|
// 绑定事件
|
||||||
|
private bindEvents(): void;
|
||||||
|
|
||||||
|
// 解绑事件
|
||||||
|
private unbindEvents(): void;
|
||||||
|
|
||||||
|
// 登录按钮点击
|
||||||
|
private async onLoginClick(): Promise<void>;
|
||||||
|
|
||||||
|
// 执行登录
|
||||||
|
private async login(account: string): Promise<void>;
|
||||||
|
|
||||||
|
// UI 清理
|
||||||
|
onEnd(): void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 UI 结构要求
|
||||||
|
|
||||||
|
### 资源路径
|
||||||
|
`assets/res/UI/Login/UILogin.prefab`
|
||||||
|
|
||||||
|
### 节点结构
|
||||||
|
|
||||||
|
```
|
||||||
|
UILogin (根节点)
|
||||||
|
├── mid/
|
||||||
|
│ └── input_account (EditBox) # 账号输入框
|
||||||
|
└── btn_login # 登录按钮
|
||||||
|
```
|
||||||
|
|
||||||
|
### 组件要求
|
||||||
|
|
||||||
|
| 节点路径 | 必需组件 | 说明 |
|
||||||
|
|---------|---------|------|
|
||||||
|
| `mid/input_account` | EditBox | 账号输入框 |
|
||||||
|
| `btn_login` | - | 登录按钮(监听点击事件) |
|
||||||
|
|
||||||
|
## 📝 使用指南
|
||||||
|
|
||||||
|
### 1. 创建 UI 预制体
|
||||||
|
|
||||||
|
1. 在 Cocos Creator 中创建 UI 预制体
|
||||||
|
2. 路径: `assets/res/UI/Login/UILogin.prefab`
|
||||||
|
3. 添加必需节点和组件
|
||||||
|
4. 保存预制体
|
||||||
|
|
||||||
|
### 2. 在登录状态中使用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { UIMgr } from '../../Framework/UI/UIMgr';
|
||||||
|
import { UILogin } from '../Login/UILogin';
|
||||||
|
|
||||||
|
// 在 AppStatusLogin 中加载登录 UI
|
||||||
|
export class AppStatusLogin extends BaseState {
|
||||||
|
async onEnter(params?: any): Promise<void> {
|
||||||
|
// 加载并显示登录界面
|
||||||
|
await UIMgr.getInstance().load(UILogin);
|
||||||
|
}
|
||||||
|
|
||||||
|
onExit(): void {
|
||||||
|
// 隐藏登录界面
|
||||||
|
UIMgr.getInstance().hide(UILogin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 登录流程
|
||||||
|
|
||||||
|
```
|
||||||
|
用户打开应用
|
||||||
|
↓
|
||||||
|
AppStatusLogin.onEnter()
|
||||||
|
↓
|
||||||
|
加载并显示 UILogin
|
||||||
|
↓
|
||||||
|
用户输入账号
|
||||||
|
↓
|
||||||
|
点击登录按钮
|
||||||
|
↓
|
||||||
|
onLoginClick()
|
||||||
|
↓
|
||||||
|
login(account)
|
||||||
|
↓
|
||||||
|
调用 NetManager.callApi('Login', ...)
|
||||||
|
↓
|
||||||
|
收到服务器响应
|
||||||
|
↓
|
||||||
|
登录成功?
|
||||||
|
├─ 是: 切换到 Game 状态
|
||||||
|
└─ 否: 显示错误提示
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📡 网络协议
|
||||||
|
|
||||||
|
### 登录请求 (ReqLogin)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ReqLogin {
|
||||||
|
account: string; // 账号
|
||||||
|
password?: string; // 密码(可选)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 登录响应 (ResLogin)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ResLogin {
|
||||||
|
success: boolean; // 是否成功
|
||||||
|
message?: string; // 消息
|
||||||
|
player?: { // 玩家信息
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
position: { x: number; y: number; z: number };
|
||||||
|
spawnPoint: { x: number; y: number; z: number };
|
||||||
|
hp: number;
|
||||||
|
maxHp: number;
|
||||||
|
isAlive: boolean;
|
||||||
|
createdAt: number;
|
||||||
|
lastLoginAt: number;
|
||||||
|
};
|
||||||
|
isNewPlayer?: boolean; // 是否新玩家
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { NetManager } from '../../Framework/Net/NetManager';
|
||||||
|
import { ReqLogin, ResLogin } from '../../Framework/Net/LoginProtocol';
|
||||||
|
|
||||||
|
// 发送登录请求
|
||||||
|
const result = await NetManager.getInstance().callApi<ReqLogin, ResLogin>('Login', {
|
||||||
|
account: 'player123'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result && result.success) {
|
||||||
|
console.log('登录成功:', result.player);
|
||||||
|
// 切换到游戏状态
|
||||||
|
AppStatusManager.getInstance().changeState('Game', {
|
||||||
|
player: result.player,
|
||||||
|
isNewPlayer: result.isNewPlayer
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 完整登录流程
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// UILogin.ts 中的登录流程
|
||||||
|
|
||||||
|
// 1. 用户点击登录按钮
|
||||||
|
private async onLoginClick(): Promise<void> {
|
||||||
|
// 防止重复点击
|
||||||
|
if (this._isLogging) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取输入的账号
|
||||||
|
const account = this._inputAccount?.string?.trim();
|
||||||
|
if (!account) {
|
||||||
|
console.warn('[UILogin] 请输入账号');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标记登录中
|
||||||
|
this._isLogging = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 执行登录
|
||||||
|
await this.login(account);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[UILogin] 登录失败:', error);
|
||||||
|
} finally {
|
||||||
|
this._isLogging = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 执行登录逻辑
|
||||||
|
private async login(account: string): Promise<void> {
|
||||||
|
// 调用登录 API
|
||||||
|
const result = await NetManager.getInstance()
|
||||||
|
.callApi<ReqLogin, ResLogin>('Login', { account });
|
||||||
|
|
||||||
|
if (result && result.success) {
|
||||||
|
// 登录成功,切换状态
|
||||||
|
AppStatusManager.getInstance().changeState('Game', {
|
||||||
|
player: result.player,
|
||||||
|
isNewPlayer: result.isNewPlayer
|
||||||
|
});
|
||||||
|
|
||||||
|
// 隐藏登录界面
|
||||||
|
this.hide();
|
||||||
|
} else {
|
||||||
|
// 登录失败,显示错误
|
||||||
|
console.error('[UILogin] 登录失败:', result?.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠️ 注意事项
|
||||||
|
|
||||||
|
1. **UI 资源准备**: 必须在 `assets/res/UI/Login/` 创建 UILogin 预制体
|
||||||
|
2. **节点结构**: 预制体必须包含 `mid/input_account` 和 `btn_login` 节点
|
||||||
|
3. **EditBox 组件**: `input_account` 节点必须挂载 EditBox 组件
|
||||||
|
4. **协议同步**: 运行 `npm run sync-shared` 同步服务端协议后,替换临时协议定义
|
||||||
|
5. **模块归属**: 业务 UI 必须放在 `App/` 对应的业务模块下
|
||||||
|
6. **防重复点击**: 登录过程中禁用按钮,防止重复请求
|
||||||
|
|
||||||
|
## 🔍 调试技巧
|
||||||
|
|
||||||
|
### 日志输出
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// UILogin 包含详细日志
|
||||||
|
// [UILogin] 登录界面初始化
|
||||||
|
// [UILogin] 已绑定登录按钮事件
|
||||||
|
// [UILogin] 开始登录,账号: xxx
|
||||||
|
// [UILogin] 登录成功
|
||||||
|
// [UILogin] 登录界面清理
|
||||||
|
```
|
||||||
|
|
||||||
|
### 检查节点
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 在 onStart 中检查节点是否找到
|
||||||
|
private findNodes(): void {
|
||||||
|
const inputNode = this._node.getChildByPath('mid/input_account');
|
||||||
|
console.log('input_account 节点:', inputNode ? '找到' : '未找到');
|
||||||
|
|
||||||
|
const btnNode = this._node.getChildByPath('btn_login');
|
||||||
|
console.log('btn_login 节点:', btnNode ? '找到' : '未找到');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
**问题1**: 节点找不到
|
||||||
|
```typescript
|
||||||
|
// 检查预制体中的节点路径是否正确
|
||||||
|
// 确保节点名称完全匹配(区分大小写)
|
||||||
|
```
|
||||||
|
|
||||||
|
**问题2**: EditBox 组件为 null
|
||||||
|
```typescript
|
||||||
|
// 确保 input_account 节点挂载了 EditBox 组件
|
||||||
|
const editBox = inputNode.getComponent(EditBox);
|
||||||
|
if (!editBox) {
|
||||||
|
console.error('未挂载 EditBox 组件');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**问题3**: 登录请求无响应
|
||||||
|
```typescript
|
||||||
|
// 检查网络是否已连接
|
||||||
|
// 检查服务器地址是否正确
|
||||||
|
// 检查协议是否已同步
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💡 最佳实践
|
||||||
|
|
||||||
|
1. **输入验证**: 登录前验证账号格式
|
||||||
|
2. **加载状态**: 显示加载动画或禁用按钮
|
||||||
|
3. **错误提示**: 友好的错误提示 UI
|
||||||
|
4. **记住账号**: 可选的记住账号功能
|
||||||
|
5. **自动登录**: 可选的自动登录功能
|
||||||
|
6. **超时处理**: 设置合理的请求超时时间
|
||||||
|
|
||||||
|
## 🎯 扩展功能
|
||||||
|
|
||||||
|
### 添加密码输入
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 1. 在 UI 中添加密码输入框
|
||||||
|
private _inputPassword: EditBox | null = null;
|
||||||
|
|
||||||
|
// 2. 在 findNodes 中查找
|
||||||
|
this._inputPassword = this._node.getChildByPath('mid/input_password')
|
||||||
|
?.getComponent(EditBox) || null;
|
||||||
|
|
||||||
|
// 3. 登录时传递密码
|
||||||
|
const result = await NetManager.getInstance()
|
||||||
|
.callApi<ReqLogin, ResLogin>('Login', {
|
||||||
|
account: account,
|
||||||
|
password: password
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 添加记住账号功能
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { sys } from 'cc';
|
||||||
|
|
||||||
|
// 保存账号
|
||||||
|
private saveAccount(account: string): void {
|
||||||
|
sys.localStorage.setItem('last_account', account);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取账号
|
||||||
|
private loadAccount(): string {
|
||||||
|
return sys.localStorage.getItem('last_account') || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在 onStart 中自动填充
|
||||||
|
async onStart(): Promise<void> {
|
||||||
|
// ... 其他初始化
|
||||||
|
|
||||||
|
// 自动填充上次的账号
|
||||||
|
const lastAccount = this.loadAccount();
|
||||||
|
if (this._inputAccount && lastAccount) {
|
||||||
|
this._inputAccount.string = lastAccount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 添加加载动画
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private _loadingNode: Node | null = null;
|
||||||
|
|
||||||
|
private showLoading(): void {
|
||||||
|
if (this._loadingNode) {
|
||||||
|
this._loadingNode.active = true;
|
||||||
|
}
|
||||||
|
// 禁用登录按钮
|
||||||
|
if (this._btnLogin) {
|
||||||
|
this._btnLogin.getComponent(Button)!.interactable = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private hideLoading(): void {
|
||||||
|
if (this._loadingNode) {
|
||||||
|
this._loadingNode.active = false;
|
||||||
|
}
|
||||||
|
// 启用登录按钮
|
||||||
|
if (this._btnLogin) {
|
||||||
|
this._btnLogin.getComponent(Button)!.interactable = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 相关文档
|
||||||
|
|
||||||
|
- [Framework/UI README](../../Framework/UI/README.md) - UI 系统
|
||||||
|
- [Framework/Net README](../../Framework/Net/README.md) - 网络通信
|
||||||
|
- [App/AppStatus README](../AppStatus/README.md) - 应用状态机
|
||||||
11
client/assets/scripts/App/Login/README.md.meta
Normal file
11
client/assets/scripts/App/Login/README.md.meta
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"ver": "1.0.1",
|
||||||
|
"importer": "text",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "4319cc73-c0a0-4a35-bafc-60b997c63e49",
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
119
client/assets/scripts/App/Login/UILogin.ts
Normal file
119
client/assets/scripts/App/Login/UILogin.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { _decorator, EditBox, Button } from 'cc';
|
||||||
|
import { UIBase } from '../../Framework/UI/UIBase';
|
||||||
|
import { NetManager } from '../../Framework/Net/NetManager';
|
||||||
|
import { ReqLogin, ResLogin } from '../../Framework/Net/LoginProtocol';
|
||||||
|
import { AppStatusManager } from '../AppStatus/AppStatusManager';
|
||||||
|
|
||||||
|
const { ccclass } = _decorator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录界面
|
||||||
|
* 职责:
|
||||||
|
* - 显示登录UI
|
||||||
|
* - 处理账号输入
|
||||||
|
* - 处理登录按钮点击
|
||||||
|
* - 发送登录请求
|
||||||
|
* - 登录成功后切换到游戏状态
|
||||||
|
*/
|
||||||
|
@ccclass('UILogin')
|
||||||
|
export class UILogin extends UIBase {
|
||||||
|
/** 账号输入框 */
|
||||||
|
private _inputAccount: EditBox | null = null;
|
||||||
|
|
||||||
|
/** 登录按钮 */
|
||||||
|
private _btnLogin: Button | null = null;
|
||||||
|
|
||||||
|
/** 是否正在登录中 */
|
||||||
|
private _isLogging: boolean = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取UI资源路径
|
||||||
|
*/
|
||||||
|
onGetUrl(): string {
|
||||||
|
return 'res://UI/Login/UILogin';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UI初始化
|
||||||
|
*/
|
||||||
|
async onStart(params?: any): Promise<void> {
|
||||||
|
console.log('[UILogin] 登录界面初始化', params);
|
||||||
|
|
||||||
|
// 使用GetChild查找子节点
|
||||||
|
this._inputAccount = this.GetChild('mid/input_account', EditBox);
|
||||||
|
this._btnLogin = this.GetChild('mid/btn_login', Button);
|
||||||
|
|
||||||
|
// 使用SetClick绑定点击事件
|
||||||
|
this.SetClick(this._btnLogin, this.onLoginClick, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录按钮点击
|
||||||
|
*/
|
||||||
|
private async onLoginClick(): Promise<void> {
|
||||||
|
if (this._isLogging) {
|
||||||
|
console.log('[UILogin] 正在登录中,请稍候...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取账号
|
||||||
|
const account = this._inputAccount?.string?.trim() || '';
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
console.warn('[UILogin] 请输入账号');
|
||||||
|
// TODO: 显示提示UI
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[UILogin] 开始登录,账号: ${account}`);
|
||||||
|
|
||||||
|
// 开始登录
|
||||||
|
this._isLogging = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.login(account);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[UILogin] 登录失败:', error);
|
||||||
|
// TODO: 显示错误提示UI
|
||||||
|
} finally {
|
||||||
|
this._isLogging = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行登录
|
||||||
|
* @param account 账号
|
||||||
|
*/
|
||||||
|
private async login(account: string): Promise<void> {
|
||||||
|
const netManager = NetManager.getInstance();
|
||||||
|
|
||||||
|
// 使用类型化的登录协议
|
||||||
|
const result = await netManager.callApi<ReqLogin, ResLogin>('Login', {
|
||||||
|
account: account
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result && result.success) {
|
||||||
|
console.log('[UILogin] 登录成功:', result);
|
||||||
|
|
||||||
|
// 登录成功,切换到游戏状态
|
||||||
|
const appStatusManager = AppStatusManager.getInstance();
|
||||||
|
appStatusManager.changeState('Game', {
|
||||||
|
player: result.player,
|
||||||
|
isNewPlayer: result.isNewPlayer || false
|
||||||
|
});
|
||||||
|
|
||||||
|
// 隐藏登录界面
|
||||||
|
this.hide();
|
||||||
|
} else {
|
||||||
|
console.error('[UILogin] 登录失败:', result?.message || '返回结果为空');
|
||||||
|
// TODO: 显示错误提示UI
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UI清理
|
||||||
|
*/
|
||||||
|
onEnd(): void {
|
||||||
|
console.log('[UILogin] 登录界面清理');
|
||||||
|
}
|
||||||
|
}
|
||||||
9
client/assets/scripts/App/Login/UILogin.ts.meta
Normal file
9
client/assets/scripts/App/Login/UILogin.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "4.0.24",
|
||||||
|
"importer": "typescript",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "e0b2fb88-6d47-45ff-97cf-f6f4ad861e75",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user