feat: 完善关卡创作页面
This commit is contained in:
@@ -131,7 +131,7 @@ export class PageHome extends BaseView {
|
||||
*/
|
||||
private _onPkClick(): void {
|
||||
console.log('[PageHome] PK按钮点击');
|
||||
ToastManager.show('功能正在开发中,敬请期待吧!');
|
||||
ViewManager.instance.open('PageWriteLevels');
|
||||
}
|
||||
|
||||
// ========== 体力消耗动画 ==========
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,29 +1,22 @@
|
||||
import { _decorator, Node, Button, Sprite, Label, ScrollView, instantiate, UITransform, Vec2 } from 'cc';
|
||||
import { _decorator, Node, Button, Label, ScrollView, instantiate, UITransform } from 'cc';
|
||||
import { BaseView } from 'db://assets/scripts/core/BaseView';
|
||||
import { ViewManager } from 'db://assets/scripts/core/ViewManager';
|
||||
import { LevelDataManager } from 'db://assets/scripts/utils/LevelDataManager';
|
||||
import { CompletedLevelsManager } from 'db://assets/scripts/utils/CompletedLevelsManager';
|
||||
import { PreviewLevelItem } from './PreviewLevelItem';
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* 预览试卷页面
|
||||
* 垂直滚动展示用户在 PageWriteLevels 中选中的 6 个关卡
|
||||
* 每个关卡展示:封面图、提示1、提示2、答案
|
||||
* 每个关卡展示:封面图、线索1、线索2、线索3、答案
|
||||
*
|
||||
* prefab 节点结构(已在编辑器中搭建):
|
||||
* 节点结构(仅 ScrollView 侧需要固定):
|
||||
* PagePreviewLevels
|
||||
* ├── Bg
|
||||
* ├── IconBack ← backBtn (返回按钮)
|
||||
* ├── PKTitle ← 标题 "挑战"
|
||||
* ├── ScrollView ← scrollView
|
||||
* │ ├── scrollBar
|
||||
* │ └── view
|
||||
* │ └── content ← listContent
|
||||
* ├── ListTpl ← listTemplate (关卡模板)
|
||||
* │ ├── LevelCover ← Sprite 封面图
|
||||
* │ ├── Tips1 ← Label 提示1
|
||||
* │ ├── Tips2 ← Label 提示2
|
||||
* │ └── Answer ← Label 答案
|
||||
* └── BackButton ← backButton (底部返回按钮)
|
||||
* ├── ScrollView / view / content ← listContent 容器
|
||||
* └── ListTpl ← listTemplate 模板根节点
|
||||
* (挂 PreviewLevelItem 组件,字段由编辑器拖拽绑定)
|
||||
*
|
||||
* item 内部节点层级/命名对本文件透明:所有引用都来自 PreviewLevelItem 的 @property。
|
||||
*/
|
||||
|
||||
/** 布局配置 — 垂直列表 */
|
||||
@@ -197,44 +190,26 @@ export class PagePreviewLevels extends BaseView {
|
||||
* 异步加载关卡数据并填充到 item 节点
|
||||
*/
|
||||
private async _loadLevelData(item: Node, levelIndex: number, displayIndex: number): Promise<void> {
|
||||
const config = await LevelDataManager.instance.ensureLevelReady(levelIndex);
|
||||
if (!config || !item.isValid) return;
|
||||
const level = CompletedLevelsManager.instance.getByIndex(levelIndex);
|
||||
if (!level || !item.isValid) return;
|
||||
|
||||
// 填充封面图
|
||||
const levelCover = item.getChildByName('LevelCover');
|
||||
if (levelCover) {
|
||||
const sprite = levelCover.getComponent(Sprite);
|
||||
if (sprite && config.spriteFrame1) {
|
||||
sprite.spriteFrame = config.spriteFrame1;
|
||||
}
|
||||
const view = item.getComponent(PreviewLevelItem);
|
||||
if (!view) {
|
||||
console.warn('[PagePreviewLevels] listTemplate 缺少 PreviewLevelItem 组件');
|
||||
return;
|
||||
}
|
||||
|
||||
// 填充提示1
|
||||
const tips1 = item.getChildByName('Tips1');
|
||||
if (tips1) {
|
||||
const label = tips1.getComponent(Label);
|
||||
if (label) {
|
||||
label.string = `线索一:${config.clue1 || ''}`;
|
||||
}
|
||||
}
|
||||
view.setTexts({
|
||||
answer: level.answer || '',
|
||||
hint1: level.hint1 || '',
|
||||
hint2: level.hint2 || '',
|
||||
hint3: level.hint3 || '',
|
||||
});
|
||||
|
||||
// 填充提示2
|
||||
const tips2 = item.getChildByName('Tips2');
|
||||
if (tips2) {
|
||||
const label = tips2.getComponent(Label);
|
||||
if (label) {
|
||||
label.string = `线索二:${config.clue2 || ''}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 填充答案
|
||||
const answer = item.getChildByName('Answer');
|
||||
if (answer) {
|
||||
const label = answer.getComponent(Label);
|
||||
if (label) {
|
||||
label.string = `答案:${config.answer || ''}`;
|
||||
}
|
||||
}
|
||||
// 异步加载封面图(通常已由 WriteLevels 预热到缓存)
|
||||
const spriteFrame = await CompletedLevelsManager.instance.loadImage(level.image1Url);
|
||||
if (!spriteFrame || !item.isValid) return;
|
||||
view.setCover(spriteFrame);
|
||||
}
|
||||
|
||||
// ─── 事件处理 ───────────────────────────────────────
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
import { _decorator, Node, Button, Sprite, Label, Toggle, ScrollView, EditBox, instantiate, UITransform, Vec2, EventTouch } from 'cc';
|
||||
import { BaseView } from 'db://assets/scripts/core/BaseView';
|
||||
import { ViewManager } from 'db://assets/scripts/core/ViewManager';
|
||||
import { LevelDataManager } from 'db://assets/scripts/utils/LevelDataManager';
|
||||
import { CompletedLevelsManager } from 'db://assets/scripts/utils/CompletedLevelsManager';
|
||||
import { ToastManager } from 'db://assets/scripts/utils/ToastManager';
|
||||
import { ShareManager } from 'db://assets/scripts/utils/ShareManager';
|
||||
import { StorageManager } from 'db://assets/scripts/utils/StorageManager';
|
||||
@@ -9,28 +9,27 @@ import { WxSDK, getUserProfile } from 'db://assets/scripts/utils/WxSDK';
|
||||
import { AuthManager } from 'db://assets/scripts/utils/AuthManager';
|
||||
import { API_ENDPOINTS, API_TIMEOUT } from 'db://assets/scripts/config/ApiConfig';
|
||||
import { HttpUtil } from 'db://assets/scripts/utils/HttpUtil';
|
||||
import { ApiEnvelope } from 'db://assets/scripts/types/ApiTypes';
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* 布局配置
|
||||
* 基于实际 prefab 尺寸计算:
|
||||
* ScrollView / view 宽 900,高 1300
|
||||
* view (ScrollView 的可视窗口) 宽 900,高 1000,anchor (0.5, 1) 顶部中点
|
||||
* ListTpl (item) 宽 300,高 400
|
||||
*
|
||||
* 水平居中:2 × 300 + 1 × 40 = 640, padding_left = (900 - 640) / 2 = 130
|
||||
* 垂直居中:3 × 400 + 2 × 25 = 1250, padding_top = (1300 - 1250) / 2 = 25
|
||||
* 每页 2 行 × 2 列 = 4 个关卡
|
||||
* PADDING 不再手写,由 VIEW / ITEM / SPACING 自动派生(见 _getHorizontalPadding / _getVerticalPadding),
|
||||
* 保证 item 网格在 view 内始终居中。改 SPACING 时不用再算 PADDING。
|
||||
*/
|
||||
const LAYOUT_CONFIG = {
|
||||
COLS: 2,
|
||||
ROWS: 3,
|
||||
ROWS: 2,
|
||||
ITEM_WIDTH: 300,
|
||||
ITEM_HEIGHT: 400,
|
||||
SPACING_X: 40,
|
||||
SPACING_Y: 25,
|
||||
PADDING_LEFT: 130,
|
||||
PADDING_TOP: 25,
|
||||
SPACING_X: 160,
|
||||
SPACING_Y: 180,
|
||||
VIEW_WIDTH: 900,
|
||||
VIEW_HEIGHT: 1300,
|
||||
VIEW_HEIGHT: 1000,
|
||||
};
|
||||
|
||||
/** 必须选择的关卡数量 */
|
||||
@@ -140,7 +139,7 @@ export class PageWriteLevels extends BaseView {
|
||||
}
|
||||
}
|
||||
|
||||
private _onTouchStart(event: EventTouch): void {
|
||||
private _onTouchStart(_event: EventTouch): void {
|
||||
if (!this._scrollViewComp) return;
|
||||
this._touchStartOffsetX = this._scrollViewComp.getScrollOffset().x;
|
||||
this._touchStartTime = Date.now();
|
||||
@@ -149,7 +148,7 @@ export class PageWriteLevels extends BaseView {
|
||||
/**
|
||||
* 触摸结束:根据滑动距离和速度决定翻页方向
|
||||
*/
|
||||
private _onTouchEnd(event: EventTouch): void {
|
||||
private _onTouchEnd(_event: EventTouch): void {
|
||||
if (!this._scrollViewComp || this._totalPages <= 1) return;
|
||||
|
||||
const currentOffsetX = this._scrollViewComp.getScrollOffset().x;
|
||||
@@ -176,20 +175,27 @@ export class PageWriteLevels extends BaseView {
|
||||
console.log('[PageWriteLevels] onViewShow');
|
||||
// 仅首次初始化列表,从预览页返回时保留选中状态
|
||||
if (this._itemNodes.length === 0) {
|
||||
this._initLevelList();
|
||||
void this._initLevelList();
|
||||
}
|
||||
}
|
||||
|
||||
private _initLevelList(): void {
|
||||
private async _initLevelList(): Promise<void> {
|
||||
this._clearList();
|
||||
|
||||
// TODO: LevelDataManager API 已重构为 NextLevel 驱动,此页面需要重新设计数据来源
|
||||
// this._levelCount = LevelDataManager.instance.getLevelCount();
|
||||
this._levelCount = 0;
|
||||
console.log('[PageWriteLevels] 关卡总数:', this._levelCount);
|
||||
// 拉取当前用户所有已通关关卡
|
||||
const levels = await CompletedLevelsManager.instance.fetch();
|
||||
if (levels === null) {
|
||||
console.warn('[PageWriteLevels] 获取已通关关卡失败');
|
||||
ToastManager.instance.show('获取关卡列表失败,请稍后重试');
|
||||
return;
|
||||
}
|
||||
|
||||
this._levelCount = levels.length;
|
||||
console.log('[PageWriteLevels] 已通关关卡总数:', this._levelCount);
|
||||
|
||||
if (this._levelCount === 0) {
|
||||
console.warn('[PageWriteLevels] 没有关卡数据');
|
||||
console.warn('[PageWriteLevels] 用户尚未通关任何关卡');
|
||||
ToastManager.instance.show('还没有已通关的关卡,快去玩几关吧');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -214,6 +220,25 @@ export class PageWriteLevels extends BaseView {
|
||||
this._selectedIndices.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 水平 padding:让整行 item 在 view 宽度内居中
|
||||
* padding_left = (VIEW_WIDTH - cols*ITEM_WIDTH - (cols-1)*SPACING_X) / 2
|
||||
*/
|
||||
private _getHorizontalPadding(): number {
|
||||
const { VIEW_WIDTH, COLS, ITEM_WIDTH, SPACING_X } = LAYOUT_CONFIG;
|
||||
const gridWidth = COLS * ITEM_WIDTH + (COLS - 1) * SPACING_X;
|
||||
return (VIEW_WIDTH - gridWidth) / 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* 垂直 padding:让整列 item 在 view 高度内居中
|
||||
*/
|
||||
private _getVerticalPadding(): number {
|
||||
const { VIEW_HEIGHT, ROWS, ITEM_HEIGHT, SPACING_Y } = LAYOUT_CONFIG;
|
||||
const gridHeight = ROWS * ITEM_HEIGHT + (ROWS - 1) * SPACING_Y;
|
||||
return (VIEW_HEIGHT - gridHeight) / 2;
|
||||
}
|
||||
|
||||
private _updateContentSize(): void {
|
||||
if (!this.listContent) return;
|
||||
|
||||
@@ -223,11 +248,12 @@ export class PageWriteLevels extends BaseView {
|
||||
const totalWidth = this._totalPages * LAYOUT_CONFIG.VIEW_WIDTH;
|
||||
contentTransform.setContentSize(totalWidth, LAYOUT_CONFIG.VIEW_HEIGHT);
|
||||
|
||||
// anchor=(0,1),将 content 左上角对齐到 view 左上角
|
||||
// content anchor=(0,1),view anchor=(0.5,1)。
|
||||
// view 本地坐标系下,view 的左上角 = (-viewWidth/2, 0)。
|
||||
// content 的 anchor 点(左上角)需要贴到 view 的左上角。
|
||||
if (this._viewTransform) {
|
||||
const viewWidth = this._viewTransform.contentSize.width;
|
||||
const viewHeight = this._viewTransform.contentSize.height;
|
||||
this.listContent.setPosition(-viewWidth / 2, viewHeight / 2, 0);
|
||||
this.listContent.setPosition(-viewWidth / 2, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,12 +303,15 @@ export class PageWriteLevels extends BaseView {
|
||||
const col = localIndex % LAYOUT_CONFIG.COLS;
|
||||
const row = Math.floor(localIndex / LAYOUT_CONFIG.COLS);
|
||||
|
||||
const paddingLeft = this._getHorizontalPadding();
|
||||
const paddingTop = this._getVerticalPadding();
|
||||
|
||||
const x = pageIndex * LAYOUT_CONFIG.VIEW_WIDTH
|
||||
+ LAYOUT_CONFIG.PADDING_LEFT
|
||||
+ paddingLeft
|
||||
+ col * (LAYOUT_CONFIG.ITEM_WIDTH + LAYOUT_CONFIG.SPACING_X)
|
||||
+ LAYOUT_CONFIG.ITEM_WIDTH / 2;
|
||||
|
||||
const y = -(LAYOUT_CONFIG.PADDING_TOP
|
||||
const y = -(paddingTop
|
||||
+ row * (LAYOUT_CONFIG.ITEM_HEIGHT + LAYOUT_CONFIG.SPACING_Y)
|
||||
+ LAYOUT_CONFIG.ITEM_HEIGHT / 2);
|
||||
|
||||
@@ -293,11 +322,12 @@ export class PageWriteLevels extends BaseView {
|
||||
* 初始化 item 的默认名称和选中状态(不设置封面,由异步加载负责)
|
||||
*/
|
||||
private _initItemState(item: Node, index: number): void {
|
||||
const level = CompletedLevelsManager.instance.getByIndex(index);
|
||||
const levelName = item.getChildByName('LevelName');
|
||||
if (levelName) {
|
||||
const label = levelName.getComponent(Label);
|
||||
if (label) {
|
||||
label.string = `第${index + 1}关`;
|
||||
label.string = level ? `第${level.level}关` : `第${index + 1}关`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,27 +347,20 @@ export class PageWriteLevels extends BaseView {
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步加载关卡资源并刷新封面图和名称。
|
||||
* TODO: LevelDataManager API 已重构为 NextLevel 驱动,此方法需要重新设计
|
||||
* 异步加载关卡封面图并填充到 item
|
||||
*/
|
||||
private async _loadAndRefreshCover(item: Node, index: number): Promise<void> {
|
||||
// const config = await LevelDataManager.instance.ensureLevelReady(index);
|
||||
const config = null as any; // TODO: 需要适配新 API
|
||||
if (!config || !item.isValid) return;
|
||||
const level = CompletedLevelsManager.instance.getByIndex(index);
|
||||
if (!level || !item.isValid) return;
|
||||
|
||||
const spriteFrame = await CompletedLevelsManager.instance.loadImage(level.image1Url);
|
||||
if (!spriteFrame || !item.isValid) return;
|
||||
|
||||
const levelCover = item.getChildByName('LevelCover');
|
||||
if (levelCover) {
|
||||
const sprite = levelCover.getComponent(Sprite);
|
||||
if (sprite && config.spriteFrame1) {
|
||||
sprite.spriteFrame = config.spriteFrame1;
|
||||
}
|
||||
}
|
||||
|
||||
const levelName = item.getChildByName('LevelName');
|
||||
if (levelName) {
|
||||
const label = levelName.getComponent(Label);
|
||||
if (label) {
|
||||
label.string = config.name;
|
||||
if (sprite) {
|
||||
sprite.spriteFrame = spriteFrame;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -550,15 +573,14 @@ export class PageWriteLevels extends BaseView {
|
||||
* 将选中的关卡索引转换为关卡 ID 数组
|
||||
*/
|
||||
private _getSelectedLevelIds(): string[] {
|
||||
// TODO: LevelDataManager API 已重构为 NextLevel 驱动,此方法需要重新设计
|
||||
const ids: string[] = [];
|
||||
// const sortedIndices = Array.from(this._selectedIndices).sort((a, b) => a - b);
|
||||
// for (const index of sortedIndices) {
|
||||
// const config = LevelDataManager.instance.getLevelConfig(index);
|
||||
// if (config) {
|
||||
// ids.push(config.id);
|
||||
// }
|
||||
// }
|
||||
const sortedIndices = Array.from(this._selectedIndices).sort((a, b) => a - b);
|
||||
for (const index of sortedIndices) {
|
||||
const level = CompletedLevelsManager.instance.getByIndex(index);
|
||||
if (level) {
|
||||
ids.push(level.id);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
@@ -592,7 +614,7 @@ export class PageWriteLevels extends BaseView {
|
||||
StorageManager.setUserInfo(userInfo);
|
||||
|
||||
// 上传到服务端
|
||||
const response = await HttpUtil.post(
|
||||
const response = await HttpUtil.post<ApiEnvelope<unknown>>(
|
||||
API_ENDPOINTS.USER_INFO,
|
||||
{
|
||||
userId: userId,
|
||||
|
||||
47
assets/prefabs/PreviewLevelItem.ts
Normal file
47
assets/prefabs/PreviewLevelItem.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { _decorator, Component, Node, Sprite, Label, SpriteFrame } from 'cc';
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* 预览页单个关卡 item 的视图组件
|
||||
* 挂在 PagePreviewLevels 的 listTemplate 根节点上,由编辑器拖拽绑定子节点。
|
||||
* instantiate 克隆 item 时,Cocos 会把这些 Node 引用自动重映射到克隆后的子节点,
|
||||
* 所以每个 item 拿到的都是自己的封面/标签引用,与节点层级/命名解耦。
|
||||
*/
|
||||
@ccclass('PreviewLevelItem')
|
||||
export class PreviewLevelItem extends Component {
|
||||
@property({ type: Sprite, tooltip: '封面图 Sprite' })
|
||||
levelCover: Sprite | null = null;
|
||||
|
||||
@property({ type: Label, tooltip: '答案 Label' })
|
||||
answerLabel: Label | null = null;
|
||||
|
||||
@property({ type: Label, tooltip: '线索1 Label' })
|
||||
tips1Label: Label | null = null;
|
||||
|
||||
@property({ type: Label, tooltip: '线索2 Label' })
|
||||
tips2Label: Label | null = null;
|
||||
|
||||
@property({ type: Label, tooltip: '线索3 Label' })
|
||||
tips3Label: Label | null = null;
|
||||
|
||||
/**
|
||||
* 一次性设置所有文本字段
|
||||
*/
|
||||
setTexts(opts: {
|
||||
answer: string;
|
||||
hint1: string;
|
||||
hint2: string;
|
||||
hint3: string;
|
||||
}): void {
|
||||
if (this.answerLabel) this.answerLabel.string = `答案:${opts.answer}`;
|
||||
if (this.tips1Label) this.tips1Label.string = `线索一:${opts.hint1}`;
|
||||
if (this.tips2Label) this.tips2Label.string = `线索二:${opts.hint2}`;
|
||||
if (this.tips3Label) this.tips3Label.string = `线索三:${opts.hint3}`;
|
||||
}
|
||||
|
||||
setCover(spriteFrame: SpriteFrame | null): void {
|
||||
if (this.levelCover && spriteFrame) {
|
||||
this.levelCover.spriteFrame = spriteFrame;
|
||||
}
|
||||
}
|
||||
}
|
||||
9
assets/prefabs/PreviewLevelItem.ts.meta
Normal file
9
assets/prefabs/PreviewLevelItem.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "5fb357ec-a670-4fff-a5cd-f47f1a2d8ea3",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
Reference in New Issue
Block a user