Files
mp-xieyingeng/软著源代码/02-基类与类型定义.md
2026-06-29 19:55:40 +08:00

15 KiB
Raw Permalink Blame History

梗中作乐 V1.0 - 源代码文档(第 2 部分)

软件名称:梗中作乐 版本号V1.0


第 2 部分:基类与类型定义

本部分包含 5 个源文件:页面 / 弹窗的基类,以及全项目共用的 API 类型与配置常量。

2.1 视图基类 (assets/scripts/core/BaseView.ts)

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)

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<Node, Vec3> = 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)

/**
 * 服务端 API 通用响应类型
 */

/** 服务端标准响应封装 */
export interface ApiEnvelope<T> {
    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)

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)

/**
 * 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;