feat: 支持动态创建输入框

This commit is contained in:
richarjiang
2026-04-24 20:18:36 +08:00
parent 8d54ffdbf8
commit 4f93725779
4 changed files with 1621 additions and 863 deletions

View File

@@ -19,7 +19,34 @@ Git 历史采用 Conventional Commits且摘要多为中文例如 `feat:
<claude-mem-context>
# Memory Context
# [mp-xieyingeng] recent context, 2026-04-24 8:45am GMT+8
# [mp-xieyingeng] recent context, 2026-04-24 8:08pm GMT+8
No previous sessions found.
Legend: 🎯session 🔴bugfix 🟣feature 🔄refactor ✅change 🔵discovery ⚖decision 🚨security_alert 🔐security_note
Format: ID TIME TYPE TITLE
Fetch details: get_observations([IDs]) | Search: mem-search skill
Stats: 19 obs (4,277t read) | 178,750t work | 98% savings
### Apr 24, 2026
101 8:46a 🟣 Live label display format updated to X/Y format
114 6:40p 🟣 Dynamic Input Layout Initialization in PageLevel Prefab
115 6:41p 🟣 Dynamic Punch Block Layout for PageLevel.prefab
116 " 🔵 Layout Component Configuration for Input and Punch Blocks
119 6:42p 🟣 Dynamic Input Blocks and Punch Layout System Implemented
121 " 🟣 PageLevel Prefab Updated with punchLayout Property
122 " 🔄 Cleanup After Dynamic Block Refactoring
124 " ✅ TypeScript Compilation Check in Progress
126 6:43p ✅ TypeScript Compilation Check Extended
127 " 🔄 Complete Diff of PageLevel.ts Dynamic Block System
128 " 🔵 PageLevel.prefab Changes Not Persisted
129 6:44p 🟣 PageLevel Prefab Correctly Updated with punchLayout
130 " ✅ TypeScript Compilation Blocked - Permission Required
133 6:45p 🔄 Extracted getPunchBlockLabel Helper Method
134 " 🔄 Template Node Hiding Logic Improved
136 6:48p ⚖️ TypeScript diagnostics disabled, using IDE/linter instead
138 " 🔄 PageLevel 输入方式从单框改为逐字格子
139 " 🔄 谐音梗展示从 Label 改为动态 Block 节点
140 " ✅ PageLevel.prefab 布局位置微调
Access 179k tokens of past work via get_observations([IDs]) or mem-search skill.
</claude-mem-context>

9
assets/levels.meta Normal file
View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "0b928321-a809-4339-8af8-5a053aeda2d5",
"files": [],
"subMetas": {},
"userData": {}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
import { _decorator, Node, EditBox, instantiate, Vec3, Button, Label, Sprite, SpriteFrame, AudioClip, AudioSource, UITransform, Prefab } from 'cc';
import { _decorator, Node, EditBox, instantiate, Vec3, Button, Label, Sprite, SpriteFrame, AudioClip, AudioSource, Prefab } from 'cc';
import { BaseView } from 'db://assets/scripts/core/BaseView';
import { ViewManager } from 'db://assets/scripts/core/ViewManager';
import { StorageManager } from 'db://assets/scripts/utils/StorageManager';
@@ -28,6 +28,9 @@ export class PageLevel extends BaseView {
@property(Node)
inputLayout: Node | null = null;
@property(Node)
punchLayout: Node | null = null;
@property(Node)
submitButton: Node | null = null;
@@ -55,9 +58,6 @@ export class PageLevel extends BaseView {
@property(Label)
image2DescLabel: Label | null = null;
@property(Label)
punchlineLabel: Label | null = null;
@property(Node)
tipsItem1: Node | null = null;
@@ -103,6 +103,21 @@ export class PageLevel extends BaseView {
/** 当前创建的输入框节点数组 */
private _inputNodes: Node[] = [];
/** InputLayout 中默认放置的输入框模板节点 */
private _inputTemplateNode: Node | null = null;
/** 当前创建的包袱展示块节点数组 */
private _punchBlockNodes: Node[] = [];
/** punchLayout 中默认放置的展示块模板节点 */
private _punchBlockTemplateNode: Node | null = null;
/** 是否正在同步输入格内容,避免设置文本时重复触发事件 */
private _isSyncingInputText: boolean = false;
/** 最近一次自动提交的答案,避免填满后重复提交同一内容 */
private _lastAutoSubmittedAnswer: string = '';
/** 倒计时剩余秒数 */
private _countdown: number = 60;
@@ -185,6 +200,7 @@ export class PageLevel extends BaseView {
onViewDestroy(): void {
console.log('[PageLevel] onViewDestroy');
this.clearInputNodes();
this.clearPunchBlocks();
this.stopCountdown();
this._closePassModal();
this._stopStaminaRecoverTimer();
@@ -310,9 +326,9 @@ export class PageLevel extends BaseView {
// 显示解锁按钮(单个统一按钮)
this.showUnlockButton();
// 根据答案长度创建单个输入
// 根据答案字数创建输入
if (config.answer) {
this.createSingleInput(config.answer.length);
this.createInputBlocks(config.answer);
}
// 更新倒计时显示
@@ -328,99 +344,106 @@ export class PageLevel extends BaseView {
LevelDataManager.instance.preloadNextLevel(this.currentLevelIndex);
}
console.log(`[PageLevel] 初始化关卡 ${this.currentLevelIndex + 1}, 答案长度: ${config.answer?.length ?? 0}`);
console.log(`[PageLevel] 初始化关卡 ${this.currentLevelIndex + 1}, 答案长度: ${Array.from(config.answer ?? '').length}`);
}
/**
* 创建单个输入
* @param answerLength 答案长度,用于设置 placeholder 和宽度
* 根据答案字数创建输入
*/
private createSingleInput(answerLength: number): void {
if (!this.inputLayout || !this.inputTemplate) {
console.error('[PageLevel] inputLayout 或 inputTemplate 未设置');
private createInputBlocks(answer: string): void {
if (!this.inputLayout) {
console.error('[PageLevel] inputLayout 未设置');
return;
}
// 清理现有输入框
const chars = Array.from(answer);
const template = this.getInputTemplateNode();
if (!template) {
console.error('[PageLevel] InputLayout 下未找到默认 Input 节点');
return;
}
if (this.inputTemplate && this.inputTemplate !== template) {
this.inputTemplate.active = false;
}
this.clearInputNodes();
this.removeUnexpectedInputLayoutChildren(template);
this._lastAutoSubmittedAnswer = '';
// 隐藏模板节点
this.inputTemplate.active = false;
for (let i = 0; i < chars.length; i++) {
const inputNode = i === 0 ? template : instantiate(template);
inputNode.active = true;
inputNode.name = `Input_${i + 1}`;
inputNode.setPosition(PageLevel.ZERO_POS);
// 创建单个输入框
const inputNode = instantiate(this.inputTemplate);
inputNode.active = true;
inputNode.name = 'singleInput';
// 设置位置
inputNode.setPosition(PageLevel.ZERO_POS);
// 获取 EditBox 组件
const editBox = inputNode.getComponent(EditBox);
if (editBox) {
// 设置 placeholder 提示
editBox.placeholder = `(${answerLength}个字)`;
// 设置最大长度为答案长度
editBox.maxLength = answerLength;
// 清空输入内容
editBox.string = '';
// 监听事件
editBox.node.on(EditBox.EventType.TEXT_CHANGED, this.onInputTextChanged, this);
editBox.node.on(EditBox.EventType.EDITING_DID_ENDED, this.onInputEditingEnded, this);
}
// 动态调整输入框宽度
const uitransform = inputNode.getComponent(UITransform);
let inputWidth = 200;
if (uitransform) {
// 每个字符约 60px加上 padding
inputWidth = Math.min(600, Math.max(200, answerLength * 60 + 40));
uitransform.setContentSize(inputWidth, 100);
}
// 调整下划线宽度与输入框一致
const underLine = inputNode.getChildByName('UnderLine');
if (underLine) {
const underLineTransform = underLine.getComponent(UITransform);
if (underLineTransform) {
underLineTransform.setContentSize(inputWidth, underLineTransform.height);
const editBox = inputNode.getComponent(EditBox);
if (editBox) {
editBox.placeholder = '';
editBox.maxLength = chars.length;
editBox.string = '';
editBox.node.on(EditBox.EventType.TEXT_CHANGED, this.onInputTextChanged, this);
editBox.node.on(EditBox.EventType.EDITING_DID_ENDED, this.onInputEditingEnded, this);
}
if (inputNode.parent !== this.inputLayout) {
this.inputLayout.addChild(inputNode);
}
this._inputNodes.push(inputNode);
}
this.inputLayout.addChild(inputNode);
this._inputNodes.push(inputNode);
console.log(`[PageLevel] 创建单个输入框,答案长度: ${answerLength}`);
console.log(`[PageLevel] 创建输入格,答案长度: ${chars.length}`);
}
/**
* 清理所有输入框节点
*/
private clearInputNodes(): void {
const template = this.getInputTemplateNode();
for (const node of this._inputNodes) {
if (node.isValid) {
const editBox = node.getComponent(EditBox);
if (editBox) {
editBox.node.off(EditBox.EventType.TEXT_CHANGED, this.onInputTextChanged, this);
editBox.node.off(EditBox.EventType.EDITING_DID_ENDED, this.onInputEditingEnded, this);
editBox.string = '';
}
if (node === template) {
node.active = false;
} else {
node.removeFromParent();
node.destroy();
}
node.destroy();
}
}
this._inputNodes = [];
}
private getInputTemplateNode(): Node | null {
if (this._inputTemplateNode?.isValid) return this._inputTemplateNode;
this._inputTemplateNode = this.inputLayout?.children.find(node => !!node.getComponent(EditBox)) ?? this.inputTemplate ?? null;
return this._inputTemplateNode;
}
private removeUnexpectedInputLayoutChildren(template: Node): void {
if (!this.inputLayout) return;
for (const child of [...this.inputLayout.children]) {
if (child !== template) {
child.removeFromParent();
child.destroy();
}
}
}
/**
* 获取所有输入框的值
*/
getInputValues(): string[] {
if (this._inputNodes.length === 0) return [];
const editBox = this._inputNodes[0].getComponent(EditBox);
const str = (editBox?.string ?? '').trim();
return [str];
return this._inputNodes.map(node => (node.getComponent(EditBox)?.string ?? '').trim());
}
/**
@@ -428,8 +451,7 @@ export class PageLevel extends BaseView {
*/
getAnswer(): string {
if (this._inputNodes.length === 0) return '';
const editBox = this._inputNodes[0].getComponent(EditBox);
return (editBox?.string ?? '').trim();
return this.getInputValues().join('').trim();
}
// ========== EditBox 事件回调 ==========
@@ -437,8 +459,14 @@ export class PageLevel extends BaseView {
/**
* 输入框文本变化回调
*/
private onInputTextChanged(_editBox: EditBox): void {
console.log('[PageLevel] 输入内容变化');
private onInputTextChanged(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();
}
/**
@@ -448,6 +476,47 @@ export class PageLevel extends BaseView {
console.log('[PageLevel] 输入编辑结束');
}
private distributeInputText(startIndex: number, 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++) {
const editBox = this._inputNodes[i].getComponent(EditBox);
if (editBox) {
editBox.string = chars[i - startIndex] ?? '';
}
}
} finally {
this._isSyncingInputText = false;
}
}
private tryAutoSubmitAnswer(): void {
if (!this._currentConfig || this._isTransitioning) return;
const values = this.getInputValues();
const isFilled = values.length === Array.from(this._currentConfig.answer ?? '').length && values.every(value => value.length === 1);
if (!isFilled) {
this._lastAutoSubmittedAnswer = '';
return;
}
const answer = values.join('');
if (answer === this._lastAutoSubmittedAnswer) return;
this._lastAutoSubmittedAnswer = answer;
this.onSubmitAnswer();
}
// ========== IconSetting 按钮相关 ==========
/**
@@ -680,18 +749,74 @@ export class PageLevel extends BaseView {
}
/**
* 设置谐音梗说明(通关后展示,未通关时传 null 隐藏)
* 设置谐音梗说明(通关后逐字展示,未通关时传 null 隐藏)
*/
private setPunchline(punchline: string | null): void {
if (!this.punchlineLabel) return;
if (!this.punchLayout) return;
if (punchline) {
this.punchlineLabel.node.active = true;
this.punchlineLabel.string = punchline;
} else {
this.punchlineLabel.node.active = false;
this.punchlineLabel.string = '';
const chars = Array.from(punchline ?? '');
if (chars.length === 0) {
this.punchLayout.active = false;
this.clearPunchBlocks();
return;
}
const template = this.getPunchBlockTemplateNode();
if (!template) {
console.error('[PageLevel] punchLayout 下未找到默认 block 节点');
return;
}
this.clearPunchBlocks();
this.punchLayout.active = true;
for (let i = 0; i < chars.length; i++) {
const blockNode = i === 0 ? template : instantiate(template);
blockNode.active = true;
blockNode.name = `block_${i + 1}`;
blockNode.setPosition(PageLevel.ZERO_POS);
const label = this.getPunchBlockLabel(blockNode);
if (label) {
label.string = chars[i];
}
if (blockNode.parent !== this.punchLayout) {
this.punchLayout.addChild(blockNode);
}
this._punchBlockNodes.push(blockNode);
}
}
private clearPunchBlocks(): void {
const template = this.getPunchBlockTemplateNode();
for (const node of this._punchBlockNodes) {
if (node.isValid) {
const label = this.getPunchBlockLabel(node);
if (label) {
label.string = '';
}
if (node === template) {
node.active = false;
} else {
node.destroy();
}
}
}
this._punchBlockNodes = [];
}
private getPunchBlockTemplateNode(): Node | null {
if (this._punchBlockTemplateNode?.isValid) return this._punchBlockTemplateNode;
this._punchBlockTemplateNode = this.punchLayout?.children[0] ?? null;
return this._punchBlockTemplateNode;
}
private getPunchBlockLabel(blockNode: Node): Label | null {
return blockNode.getChildByName('Label')?.getComponent(Label) ?? blockNode.getComponent(Label);
}
// ========== 音效相关方法 ==========