feat: 支持动态创建输入框
This commit is contained in:
31
AGENTS.md
31
AGENTS.md
@@ -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
9
assets/levels.meta
Normal 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
@@ -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);
|
||||
}
|
||||
|
||||
// ========== 音效相关方法 ==========
|
||||
|
||||
Reference in New Issue
Block a user