Files
rougelike-demo/client/assets/scripts/App/Login/README.md
2025-12-14 23:34:50 +08:00

9.1 KiB

登录模块 (App/Login)

📋 模块概述

用户登录功能模块,包括登录界面 UI 和登录业务逻辑,登录成功后切换到游戏状态。

🎯 核心特性

  • 登录界面 UI
  • 账号输入处理
  • 登录按钮交互
  • 网络登录请求
  • 状态切换
  • 错误处理

🗂️ 文件结构

App/Login/
└── UILogin.ts          # 登录界面组件

📘 核心类详解

UILogin - 登录界面

职责: 显示登录 UI,处理用户输入,发送登录请求

@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. 在登录状态中使用

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)

interface ReqLogin {
    account: string;     // 账号
    password?: string;   // 密码(可选)
}

登录响应 (ResLogin)

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;  // 是否新玩家
}

使用示例

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
    });
}

🔄 完整登录流程

// 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_accountbtn_login 节点
  3. EditBox 组件: input_account 节点必须挂载 EditBox 组件
  4. 协议同步: 运行 npm run sync-shared 同步服务端协议后,替换临时协议定义
  5. 模块归属: 业务 UI 必须放在 App/ 对应的业务模块下
  6. 防重复点击: 登录过程中禁用按钮,防止重复请求

🔍 调试技巧

日志输出

// UILogin 包含详细日志
// [UILogin] 登录界面初始化
// [UILogin] 已绑定登录按钮事件
// [UILogin] 开始登录,账号: xxx
// [UILogin] 登录成功
// [UILogin] 登录界面清理

检查节点

// 在 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: 节点找不到

// 检查预制体中的节点路径是否正确
// 确保节点名称完全匹配(区分大小写)

问题2: EditBox 组件为 null

// 确保 input_account 节点挂载了 EditBox 组件
const editBox = inputNode.getComponent(EditBox);
if (!editBox) {
    console.error('未挂载 EditBox 组件');
}

问题3: 登录请求无响应

// 检查网络是否已连接
// 检查服务器地址是否正确
// 检查协议是否已同步

💡 最佳实践

  1. 输入验证: 登录前验证账号格式
  2. 加载状态: 显示加载动画或禁用按钮
  3. 错误提示: 友好的错误提示 UI
  4. 记住账号: 可选的记住账号功能
  5. 自动登录: 可选的自动登录功能
  6. 超时处理: 设置合理的请求超时时间

🎯 扩展功能

添加密码输入

// 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
    });

添加记住账号功能

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;
    }
}

添加加载动画

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;
    }
}

📚 相关文档