610 lines
15 KiB
Markdown
610 lines
15 KiB
Markdown
# 梗中作乐 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<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)
|
||
|
||
```typescript
|
||
/**
|
||
* 服务端 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)
|
||
|
||
```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;
|
||
```
|