feat: 完善新版 UI

This commit is contained in:
richarjiang
2026-04-24 21:44:22 +08:00
parent 4f93725779
commit ecc82ae9a7
9 changed files with 354 additions and 117 deletions

View File

@@ -24,6 +24,12 @@ export class PageLevel extends BaseView {
/** 默认体力上限,服务端未返回 max 时使用 */
private static readonly DEFAULT_STAMINA_MAX = 50;
/** 答案正确后展示包袱答案的停留时间 */
private static readonly PASS_MODAL_DELAY_MS = 2000;
/** 答案错误后清空输入的延迟,给失败音效和错误答案留出反馈时间 */
private static readonly CLEAR_INPUT_DELAY_MS = 500;
// ========== 节点引用 ==========
@property(Node)
inputLayout: Node | null = null;
@@ -80,6 +86,10 @@ export class PageLevel extends BaseView {
@property(Label)
liveLabel: Label | null = null;
/** 关卡标题标签,显示为“第 N 关” */
@property(Label)
titleLevelLabel: Label | null = null;
// ========== 配置属性 ==========
@property({
min: 0,
@@ -263,6 +273,9 @@ export class PageLevel extends BaseView {
this.currentLevelIndex,
{
answer: enterData.answer,
image1Description: enterData.image1Description,
image2Description: enterData.image2Description,
punchline: enterData.punchline,
hint1: enterData.hint1,
hint2: enterData.hint2,
hint3: enterData.hint3,
@@ -308,8 +321,11 @@ export class PageLevel extends BaseView {
// 设置图片描述
this.setImageDescriptions(config.image1Description, config.image2Description);
// 隐藏谐音梗说明(通关后才显示)
this.setPunchline(null);
// 设置关卡标题
this.updateTitleLevelLabel();
// 隐藏包袱答案,通关后再按 punchline 展示
this.hidePunchline();
// 设置线索1默认解锁如果有的话
if (config.clue1) {
@@ -381,6 +397,7 @@ export class PageLevel extends BaseView {
editBox.placeholder = '';
editBox.maxLength = chars.length;
editBox.string = '';
editBox.node.on(EditBox.EventType.EDITING_DID_BEGAN, this.onInputEditingBegan, this);
editBox.node.on(EditBox.EventType.TEXT_CHANGED, this.onInputTextChanged, this);
editBox.node.on(EditBox.EventType.EDITING_DID_ENDED, this.onInputEditingEnded, this);
}
@@ -404,6 +421,7 @@ export class PageLevel extends BaseView {
if (node.isValid) {
const editBox = node.getComponent(EditBox);
if (editBox) {
editBox.node.off(EditBox.EventType.EDITING_DID_BEGAN, this.onInputEditingBegan, this);
editBox.node.off(EditBox.EventType.TEXT_CHANGED, this.onInputTextChanged, this);
editBox.node.off(EditBox.EventType.EDITING_DID_ENDED, this.onInputEditingEnded, this);
editBox.string = '';
@@ -457,42 +475,58 @@ export class PageLevel extends BaseView {
// ========== EditBox 事件回调 ==========
/**
* 输入框文本变化回调
* 输入框开始编辑时,把当前所有格子的内容合并到当前输入框里
*/
private onInputTextChanged(editBox: EditBox): void {
private onInputEditingBegan(editBox: EditBox): void {
if (this._isSyncingInputText) return;
const inputIndex = this._inputNodes.findIndex(node => node === editBox.node);
if (inputIndex < 0) return;
this.distributeInputText(inputIndex, editBox.string);
this.tryAutoSubmitAnswer();
const answer = this.getAnswer();
this._isSyncingInputText = true;
try {
for (let i = 0; i < this._inputNodes.length; i++) {
const itemEditBox = this._inputNodes[i].getComponent(EditBox);
if (itemEditBox) {
itemEditBox.string = i === inputIndex ? answer : '';
}
}
} finally {
this._isSyncingInputText = false;
}
}
/**
* 输入框文本变化回调
*/
private onInputTextChanged(_editBox: EditBox): void {
this._lastAutoSubmittedAnswer = '';
}
/**
* 输入框编辑结束回调
*/
private onInputEditingEnded(_editBox: EditBox): void {
console.log('[PageLevel] 输入编辑结束');
private onInputEditingEnded(editBox: EditBox): void {
if (this._isSyncingInputText) return;
const inputIndex = this._inputNodes.findIndex(node => node === editBox.node);
if (inputIndex < 0) return;
this.distributeInputText(editBox.string);
this.tryAutoSubmitAnswer();
}
private distributeInputText(startIndex: number, text: string): void {
private distributeInputText(text: string): void {
const chars = Array.from(text);
this._isSyncingInputText = true;
try {
if (chars.length <= 1) {
const editBox = this._inputNodes[startIndex]?.getComponent(EditBox);
if (editBox) {
editBox.string = chars[0] ?? '';
}
return;
}
for (let i = startIndex; i < this._inputNodes.length; i++) {
for (let i = 0; i < this._inputNodes.length; i++) {
const editBox = this._inputNodes[i].getComponent(EditBox);
if (editBox) {
editBox.string = chars[i - startIndex] ?? '';
editBox.string = chars[i] ?? '';
}
}
} finally {
@@ -500,6 +534,22 @@ export class PageLevel extends BaseView {
}
}
private clearInputText(): void {
this._isSyncingInputText = true;
try {
for (const node of this._inputNodes) {
const editBox = node.getComponent(EditBox);
if (editBox) {
editBox.string = '';
}
}
this._lastAutoSubmittedAnswer = '';
} finally {
this._isSyncingInputText = false;
}
}
private tryAutoSubmitAnswer(): void {
if (!this._currentConfig || this._isTransitioning) return;
@@ -575,20 +625,20 @@ export class PageLevel extends BaseView {
const tipsItem = this.getTipsItem(index);
if (!tipsItem) return;
// 查找 TipsLabel 节点Content -> TipsLabel
const contentNode = tipsItem.getChildByName('Content');
if (!contentNode) return;
const tipsLabelNode = contentNode.getChildByName('TipsLabel');
if (!tipsLabelNode) return;
const label = tipsLabelNode.getComponent(Label);
const label = this.getTipsLabel(tipsItem);
if (label) {
label.string = `提示 ${index}: ${content}`;
label.string = `提示${index}${content}`;
console.log(`[PageLevel] 设置线索${index}: ${content}`);
}
}
private getTipsLabel(tipsItem: Node): Label | null {
const directLabel = tipsItem.getChildByName('TipsLabel')?.getComponent(Label);
if (directLabel) return directLabel;
return tipsItem.getChildByName('Content')?.getChildByName('TipsLabel')?.getComponent(Label) ?? null;
}
/**
* 显示线索
*/
@@ -748,6 +798,12 @@ export class PageLevel extends BaseView {
}
}
private updateTitleLevelLabel(): void {
if (!this.titleLevelLabel) return;
this.titleLevelLabel.string = `${this.currentLevelIndex + 1}`;
}
/**
* 设置谐音梗说明(通关后逐字展示,未通关时传 null 隐藏)
*/
@@ -756,8 +812,7 @@ export class PageLevel extends BaseView {
const chars = Array.from(punchline ?? '');
if (chars.length === 0) {
this.punchLayout.active = false;
this.clearPunchBlocks();
this.hidePunchline();
return;
}
@@ -768,6 +823,7 @@ export class PageLevel extends BaseView {
}
this.clearPunchBlocks();
this.removeUnexpectedPunchLayoutChildren(template);
this.punchLayout.active = true;
for (let i = 0; i < chars.length; i++) {
@@ -778,7 +834,12 @@ export class PageLevel extends BaseView {
const label = this.getPunchBlockLabel(blockNode);
if (label) {
label.node.active = true;
label.enabled = true;
label.string = chars[i];
console.log(`[PageLevel] 设置包袱块${i + 1}: ${chars[i]}`);
} else {
console.warn(`[PageLevel] 包袱块${i + 1} 未找到 Label 组件`);
}
if (blockNode.parent !== this.punchLayout) {
@@ -801,6 +862,7 @@ export class PageLevel extends BaseView {
if (node === template) {
node.active = false;
} else {
node.removeFromParent();
node.destroy();
}
}
@@ -808,6 +870,42 @@ export class PageLevel extends BaseView {
this._punchBlockNodes = [];
}
private hidePunchline(): void {
if (!this.punchLayout) return;
const template = this.getPunchBlockTemplateNode();
if (!template) {
this.punchLayout.active = false;
return;
}
this.clearPunchBlocks();
this.removeUnexpectedPunchLayoutChildren(template);
this.punchLayout.active = false;
template.active = false;
template.name = 'block';
const label = this.getPunchBlockLabel(template);
if (label) {
label.node.active = true;
label.enabled = true;
label.string = '';
}
this._punchBlockNodes = [];
}
private removeUnexpectedPunchLayoutChildren(template: Node): void {
if (!this.punchLayout) return;
for (const child of [...this.punchLayout.children]) {
if (child !== template) {
child.removeFromParent();
child.destroy();
}
}
}
private getPunchBlockTemplateNode(): Node | null {
if (this._punchBlockTemplateNode?.isValid) return this._punchBlockTemplateNode;
@@ -816,7 +914,19 @@ export class PageLevel extends BaseView {
}
private getPunchBlockLabel(blockNode: Node): Label | null {
return blockNode.getChildByName('Label')?.getComponent(Label) ?? blockNode.getComponent(Label);
return this.findLabelInNode(blockNode);
}
private findLabelInNode(node: Node): Label | null {
const label = node.getComponent(Label);
if (label) return label;
for (const child of node.children) {
const childLabel = this.findLabelInNode(child);
if (childLabel) return childLabel;
}
return null;
}
// ========== 音效相关方法 ==========
@@ -1023,31 +1133,39 @@ export class PageLevel extends BaseView {
// 播放成功音效
this.playSuccessSound();
// 通关后展示谐音梗说明
if (this._currentConfig?.punchline) {
this.setPunchline(this._currentConfig.punchline);
}
// 通关后根据 punchline 字数重建包袱答案块
this.setPunchline(this._currentConfig?.punchline ?? null);
const levelId = this._currentConfig?.id ?? '';
const timeSpent = Math.max(0, Math.round((Date.now() - this._levelStartTime) / 1000));
if (!this._isShareMode) {
// 上报通关耗时
const result = await StaminaManager.instance.completeLevel(levelId, timeSpent);
if (result) {
console.log(`[PageLevel] 通关上报成功,首次通关: ${result.firstClear}`);
}
// 标记关卡为已通关(本地缓存)
LevelDataManager.instance.markLevelCompleted(this.currentLevelIndex);
} else {
// fire-and-forget: errors are logged inside reportLevelProgress
void ShareManager.instance.reportLevelProgress(levelId, true, timeSpent);
}
this.reportLevelCompleted(levelId, timeSpent);
await this.delay(PageLevel.PASS_MODAL_DELAY_MS);
// 显示通关弹窗
this._showPassModal();
}
private reportLevelCompleted(levelId: string, timeSpent: number): void {
if (!this._isShareMode) {
// 标记关卡为已通关(本地缓存),通关上报并行执行,不阻塞包袱展示节奏
LevelDataManager.instance.markLevelCompleted(this.currentLevelIndex);
void StaminaManager.instance.completeLevel(levelId, timeSpent).then((result) => {
if (result) {
console.log(`[PageLevel] 通关上报成功,首次通关: ${result.firstClear}`);
}
});
return;
}
// fire-and-forget: errors are logged inside reportLevelProgress
void ShareManager.instance.reportLevelProgress(levelId, true, timeSpent);
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* 显示通关弹窗
* 将弹窗添加到 Canvas 根节点下(而非 PageLevel 子节点)
@@ -1125,6 +1243,13 @@ export class PageLevel extends BaseView {
// 显示 Toast 提示
ToastManager.show('答案错误,再试试吧!');
// 输入识别失败或答案错误后延迟清空,避免错误内容瞬间消失
void this.delay(PageLevel.CLEAR_INPUT_DELAY_MS).then(() => {
if (!this._isTransitioning) {
this.clearInputText();
}
});
}
/**