feat: 接入激励视频

This commit is contained in:
richarjiang
2026-06-29 19:55:40 +08:00
parent 411ca8c772
commit e2fd4eeda3
16 changed files with 9215 additions and 73 deletions

View File

@@ -0,0 +1,609 @@
# 梗中作乐 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;
```