# 梗中作乐 V1.0 - 源代码文档(第 2 部分) 软件名称:梗中作乐 版本号:V1.0 --- ## 第 2 部分:基类与类型定义 本部分包含 5 个源文件:页面 / 弹窗的基类,以及全项目共用的 API 类型与配置常量。 ### 2.1 视图基类 (assets/scripts/core/BaseView.ts) ```typescript import { _decorator, Component, Prefab } from 'cc'; const { ccclass } = _decorator; /** * 页面配置接口 */ export interface ViewConfig { prefab: Prefab; // 预制体引用(主包资源) cache?: boolean; // 是否缓存页面,默认 true zIndex?: number; // 层级,默认 0 } /** * 页面打开选项 */ export interface ViewOptions { params?: any; // 传递给页面的参数 onComplete?: (view: BaseView) => void; onError?: (err: Error) => void; } /** * 页面基类 * 所有页面组件应继承此类,实现生命周期方法 */ @ccclass('BaseView') export class BaseView extends Component { /** 页面唯一标识 */ viewId: string = ''; /** 页面配置 */ config: ViewConfig | null = null; /** 是否正在显示 */ isShowing: boolean = false; /** 传递给页面的参数 */ protected _params: any = null; /** * 设置页面参数 */ setParams(params: any): void { this._params = params; } /** * 获取页面参数 */ getParams(): any { return this._params; } /** * 页面加载时调用(首次创建时) * 子类应重写此方法 */ onViewLoad(): void { // 子类实现 } /** * 页面显示时调用(每次打开时) * 子类应重写此方法 */ onViewShow(): void { // 子类实现 } /** * 页面隐藏时调用(关闭或被其他页面覆盖时) * 子类应重写此方法 */ onViewHide(): void { // 子类实现 } /** * 页面销毁时调用 * 子类应重写此方法 */ onViewDestroy(): void { // 子类实现 } // ========== 内部方法,由 ViewManager 调用 ========== /** * 内部方法:执行显示逻辑 */ _doShow(): void { if (this.isShowing) return; this.isShowing = true; this.node.active = true; this.onViewShow(); } /** * 内部方法:执行隐藏逻辑 */ _doHide(): void { if (!this.isShowing) return; this.isShowing = false; this.onViewHide(); this.node.active = false; } /** * 内部方法:执行销毁逻辑 */ _doDestroy(): void { // 标记已销毁,防止 onDestroy 中重复调用 this._destroyed = true; this.node.destroy(); } /** 是否已标记销毁 */ private _destroyed: boolean = false; // ========== Cocos 生命周期 ========== protected onDestroy(): void { // 仅在未被 _doDestroy 调用时执行生命周期 if (!this._destroyed) { if (this.isShowing) { this.onViewHide(); } } this.onViewDestroy(); } } ``` ### 2.2 弹窗基类 (assets/scripts/core/BaseModal.ts) ```typescript import { _decorator, Node, Tween, UIOpacity, Vec3, tween } from 'cc'; import { BaseView } from './BaseView'; const { ccclass, property } = _decorator; @ccclass('BaseModal') export class BaseModal extends BaseView { @property([Node]) protected animationNodes: Node[] = []; @property(Node) protected backdropNode: Node | null = null; @property protected openAnimationEnabled: boolean = true; @property protected openAnimationDuration: number = 0.36; private readonly _originalScales: Map = new Map(); onViewShow(): void { this.playOpenAnimation(); } onViewHide(): void { this.stopOpenAnimation(); } protected playOpenAnimation(): void { if (!this.openAnimationEnabled) { return; } this.playBackdropFadeIn(); const targets = this.getAnimationTargets(); targets.forEach((target, index) => { this.playBounceIn(target, index * 0.035); }); } protected stopOpenAnimation(): void { this.getAnimationTargets().forEach((target) => { Tween.stopAllByTarget(target); }); const opacity = this.backdropNode?.getComponent(UIOpacity); if (opacity) { Tween.stopAllByTarget(opacity); } } private getAnimationTargets(): Node[] { const configuredTargets = this.animationNodes.filter((node) => node?.isValid); return configuredTargets.length > 0 ? configuredTargets : [this.node]; } private getOriginalScale(target: Node): Vec3 { const cachedScale = this._originalScales.get(target); if (cachedScale) { return cachedScale; } const originalScale = target.scale.clone(); this._originalScales.set(target, originalScale); return originalScale; } private playBounceIn(target: Node, delay: number): void { const originalScale = this.getOriginalScale(target); const startScale = this.multiplyScale(originalScale, 0.82); const peakScale = this.multiplyScale(originalScale, 1.045); Tween.stopAllByTarget(target); target.setScale(startScale); tween(target) .delay(delay) .to(this.openAnimationDuration * 0.58, { scale: peakScale }, { easing: 'backOut' }) .to(this.openAnimationDuration * 0.24, { scale: this.multiplyScale(originalScale, 0.985) }, { easing: 'sineOut' }) .to(this.openAnimationDuration * 0.18, { scale: originalScale }, { easing: 'sineOut' }) .start(); } private playBackdropFadeIn(): void { if (!this.backdropNode?.isValid) { return; } const opacity = this.backdropNode.getComponent(UIOpacity) ?? this.backdropNode.addComponent(UIOpacity); Tween.stopAllByTarget(opacity); opacity.opacity = 0; tween(opacity) .to(0.18, { opacity: 255 }, { easing: 'sineOut' }) .start(); } private multiplyScale(scale: Vec3, factor: number): Vec3 { return new Vec3(scale.x * factor, scale.y * factor, scale.z); } } ``` ### 2.3 接口类型定义 (assets/scripts/types/ApiTypes.ts) ```typescript /** * 服务端 API 通用响应类型 */ /** 服务端标准响应封装 */ export interface ApiEnvelope { success: boolean; data: T | null; message: string | null; timestamp: string; } /** 体力值信息 */ export interface StaminaInfo { /** 当前体力值(已计算恢复) */ current: number; /** 体力上限,固定为 50 */ max: number; /** 下一点体力恢复的时间(ISO 8601),满体力时为 null */ nextRecoverAt: string | null; } /** 下一关完整数据(多个接口共用) */ export interface NextLevelData { /** 关卡 ID */ id: string; /** 关卡编号(sortOrder) */ level: number; /** 图片1 URL */ image1Url: string; /** 图片1 文本说明 */ image1Description: string | null; /** 图片2 URL */ image2Url: string; /** 图片2 文本说明 */ image2Description: string | null; /** 答案 */ answer: string; /** 谐音梗说明 */ punchline: string | null; /** 线索1 */ hint1: string | null; /** 线索2 */ hint2: string | null; /** 线索3 */ hint3: string | null; /** 限时(秒),null 表示不限时 */ timeLimit: number | null; } /** 登录响应数据 */ export interface WxLoginData { token: string; user: { id: string; nickname: string | null; stamina: number; }; } /** 用户资料响应数据 */ export interface UserProfileData { id: string; nickname: string | null; stamina: StaminaInfo; } /** 游戏数据响应(Loading 页面) */ export interface GameData { user: { id: string; stamina: StaminaInfo; }; /** 已通关的关卡数量 */ completedLevelCount: number; /** 下一个待通关的关卡(全部通关时为 null) */ nextLevel: NextLevelData | null; } /** 进入关卡响应 */ export interface EnterLevelData { id: string; level: number; image1Url: string; image1Description: string | null; image2Url: string; image2Description: string | null; answer: string; punchline: string | null; hint1: string | null; hint2: string | null; hint3: string | null; stamina: StaminaInfo; /** 预加载的下一关数据(无下一关时为 null) */ preloadNextLevel: NextLevelData | null; } /** 通关上报响应 */ export interface CompleteLevelData { firstClear: boolean; levelId: string; timeSpent: number; /** 下一个待通关的关卡(全部通关时为 null) */ nextLevel: NextLevelData | null; } /** 创建分享响应 */ export interface CreateShareData { shareCode: string; title: string; levelCount: number; } /** 分享关卡数据 */ export interface ShareLevelData { id: string; level: number; image1Url: string; image1Description: string | null; image2Url: string; image2Description: string | null; answer: string; punchline: string | null; hint1: string | null; hint2: string | null; hint3: string | null; sortOrder: number; } /** 加入分享响应 */ export interface JoinShareData { shareCode: string; title: string; levels: ShareLevelData[]; } /** 分享挑战单关提交 */ export interface SubmitShareLevel { levelId: string; answer: string; timeSpent: number; } /** 分享挑战提交后的单关结果 */ export interface SubmittedShareLevelData extends ShareLevelData { submittedAnswer: string; timeSpent: number; isCorrect: boolean; timeLimit: number | null; withinTimeLimit: boolean; } /** 分享挑战整场提交响应 */ export interface SubmitShareData { shareCode: string; title: string; rank: number; correctCount: number; levelCount: number; participantCount: number; totalTimeSpent: number; levels: SubmittedShareLevelData[]; } /** 分享挑战参与者排行摘要 */ export interface ShareParticipantRankSummary { userId?: string | null; participantId?: string | null; nickname?: string | null; nickName?: string | null; avatarUrl?: string | null; rank?: number | null; correctCount?: number | null; totalTimeSpent?: number | null; } /** 我创建的分享挑战条目 */ export interface CreatedShareItem { id: string; shareCode: string; title: string; levelCount: number; participantCount: number; userRank: number | null; createdAt: string; firstPlaceUser?: ShareParticipantRankSummary | null; topParticipant?: ShareParticipantRankSummary | null; firstParticipant?: ShareParticipantRankSummary | null; champion?: ShareParticipantRankSummary | null; } /** 我创建的分享挑战列表响应 */ export interface CreatedShareListData { items: CreatedShareItem[]; } /** 我参与的分享挑战条目 */ export interface ParticipatedShareItem { title: string; participantCount: number; userRank: number | null; } /** 我参与的分享挑战列表响应 */ export interface ParticipatedShareListData { items: ParticipatedShareItem[]; } /** 分享挑战详情响应 */ export interface ShareDetailData { id: string; shareCode: string; title: string; levelCount: number; participantCount: number; userRank: number | null; createdAt: string; rankings: ShareParticipantRankSummary[]; } /** 已通关关卡数据(成就墙 / 关卡回看场景) */ export interface CompletedLevel { /** 关卡 ID */ id: string; /** 关卡编号(sortOrder) */ level: number; /** 图片1 URL */ image1Url: string; /** 图片1 文本说明 */ image1Description: string | null; /** 图片2 URL */ image2Url: string; /** 图片2 文本说明 */ image2Description: string | null; /** 答案 */ answer: string; /** 谐音梗说明 */ punchline: string | null; /** 线索1 */ hint1: string | null; /** 线索2 */ hint2: string | null; /** 线索3 */ hint3: string | null; /** 限时(秒),null 表示不限时 */ timeLimit: number | null; /** 首次通关时长(秒) */ timeSpent: number; /** 通关时间(ISO 8601) */ completedAt: string; } ``` ### 2.4 关卡类型定义 (assets/scripts/types/LevelTypes.ts) ```typescript import { SpriteFrame } from 'cc'; /** * 运行时关卡配置(包含已加载的图片) */ export interface RuntimeLevelConfig { /** 关卡 ID */ id: string; /** 关卡名称 */ name: string; /** 图片1 SpriteFrame(可能为 null 如果加载失败) */ spriteFrame1: SpriteFrame | null; /** 图片2 SpriteFrame(可能为 null 如果加载失败) */ spriteFrame2: SpriteFrame | null; /** 图片1 文本说明 */ image1Description: string | null; /** 图片2 文本说明 */ image2Description: string | null; /** 谐音梗说明 */ punchline: string | null; /** 线索1 */ clue1: string | null; /** 线索2 */ clue2: string | null; /** 线索3 */ clue3: string | null; /** 答案 */ answer: string | null; /** 是否已通关 */ completed: boolean; /** 限时(秒),null 表示不限时 */ timeLimit: number | null; } ``` ### 2.5 API 配置常量 (assets/scripts/config/ApiConfig.ts) ```typescript /** * API 配置常量 * 统一管理所有服务端 API 地址 */ /** 服务端 API 基础地址 */ export const API_BASE = 'https://ilookai.cn/api/v1'; /** API 端点 */ export const API_ENDPOINTS = { /** 微信登录 */ WX_LOGIN: `${API_BASE}/auth/wx-login`, /** 用户资料(含实时体力) */ USER_PROFILE: `${API_BASE}/user/profile`, /** 游戏数据(体力 + 通关进度 + 下一关) */ USER_GAME_DATA: `${API_BASE}/user/game-data`, /** 游戏配置 */ GAME_CONFIGS: `${API_BASE}/game-configs`, /** 分享相关 */ SHARE_CREATE: `${API_BASE}/share`, SHARE_CREATED: `${API_BASE}/share/created`, SHARE_PARTICIPATED: `${API_BASE}/share/participated`, /** 用户信息 */ USER_INFO: `${API_BASE}/user/info`, /** 用户所有已通关的关卡(成就墙 / 关卡回看) */ COMPLETED_LEVELS: `${API_BASE}/levels/completed`, } as const; export function getLevelEnterUrl(levelId: string): string { return `${API_BASE}/levels/${levelId}/enter`; } export function getLevelCompleteUrl(levelId: string): string { return `${API_BASE}/levels/${levelId}/complete`; } export function getShareJoinUrl(code: string): string { return `${API_BASE}/share/${code}/join`; } export function getShareDetailUrl(code: string): string { return `${API_BASE}/share/${code}`; } export function getShareSubmitUrl(code: string): string { return `${API_BASE}/share/${code}/submit`; } export function getGameConfigUrl(key: string): string { return `${API_BASE}/game-configs/${key}`; } /** 请求超时时间(毫秒) */ export const API_TIMEOUT = { DEFAULT: 8000, SHORT: 5000, } as const; ```