diff --git a/client/assets/scripts/App/Login.meta b/client/assets/scripts/App/Login.meta new file mode 100644 index 0000000..4d3a00a --- /dev/null +++ b/client/assets/scripts/App/Login.meta @@ -0,0 +1,9 @@ +{ + "ver": "1.2.0", + "importer": "directory", + "imported": true, + "uuid": "6cbaee3d-3077-4a01-96f8-bf0ffd8225b0", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/client/assets/scripts/App/Login/README.md b/client/assets/scripts/App/Login/README.md new file mode 100644 index 0000000..cdbf273 --- /dev/null +++ b/client/assets/scripts/App/Login/README.md @@ -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; + + // 查找 UI 节点 + private findNodes(): void; + + // 绑定事件 + private bindEvents(): void; + + // 解绑事件 + private unbindEvents(): void; + + // 登录按钮点击 + private async onLoginClick(): Promise; + + // 执行登录 + private async login(account: string): Promise; + + // 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 { + // 加载并显示登录界面 + 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('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 { + // 防止重复点击 + 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 { + // 调用登录 API + const result = await NetManager.getInstance() + .callApi('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('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 { + // ... 其他初始化 + + // 自动填充上次的账号 + 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) - 应用状态机 diff --git a/client/assets/scripts/App/Login/README.md.meta b/client/assets/scripts/App/Login/README.md.meta new file mode 100644 index 0000000..c738b81 --- /dev/null +++ b/client/assets/scripts/App/Login/README.md.meta @@ -0,0 +1,11 @@ +{ + "ver": "1.0.1", + "importer": "text", + "imported": true, + "uuid": "4319cc73-c0a0-4a35-bafc-60b997c63e49", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": {} +} diff --git a/client/assets/scripts/App/Login/UILogin.ts b/client/assets/scripts/App/Login/UILogin.ts new file mode 100644 index 0000000..dc25089 --- /dev/null +++ b/client/assets/scripts/App/Login/UILogin.ts @@ -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 { + 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 { + 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 { + const netManager = NetManager.getInstance(); + + // 使用类型化的登录协议 + const result = await netManager.callApi('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] 登录界面清理'); + } +} diff --git a/client/assets/scripts/App/Login/UILogin.ts.meta b/client/assets/scripts/App/Login/UILogin.ts.meta new file mode 100644 index 0000000..333cb08 --- /dev/null +++ b/client/assets/scripts/App/Login/UILogin.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "e0b2fb88-6d47-45ff-97cf-f6f4ad861e75", + "files": [], + "subMetas": {}, + "userData": {} +}