feat: 重构输入框为单输入框模式并添加震动反馈

- 将多输入框改为单个输入框,根据答案长度动态调整宽度
- 输入框 placeholder 显示答案字数提示
- 答案错误时触发微信小游戏震动反馈
- WxSDK 新增 vibrateShort/vibrateLong 方法
- 重构音效播放方法,提取公共 playSound 方法
This commit is contained in:
richarjiang
2026-03-14 19:04:48 +08:00
parent 71a38c1fe5
commit c9fbc5212a
5 changed files with 786 additions and 434 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,8 @@
import { _decorator, Node, EditBox, instantiate, Vec3, Button, Label, Sprite, SpriteFrame, AudioClip, AudioSource } from 'cc'; import { _decorator, Node, EditBox, instantiate, Vec3, Button, Label, Sprite, SpriteFrame, AudioClip, AudioSource, UITransform } from 'cc';
import { BaseView } from 'db://assets/scripts/core/BaseView'; import { BaseView } from 'db://assets/scripts/core/BaseView';
import { ViewManager } from 'db://assets/scripts/core/ViewManager'; import { ViewManager } from 'db://assets/scripts/core/ViewManager';
import { StorageManager } from 'db://assets/scripts/utils/StorageManager'; import { StorageManager } from 'db://assets/scripts/utils/StorageManager';
import { WxSDK } from 'db://assets/scripts/utils/WxSDK';
const { ccclass, property } = _decorator; const { ccclass, property } = _decorator;
/** /**
@@ -168,25 +169,21 @@ export class PageLevel extends BaseView {
this.showUnlockButton(2); this.showUnlockButton(2);
this.showUnlockButton(3); this.showUnlockButton(3);
// 根据答案长度创建输入框 // 根据答案长度创建单个输入框
const inputCount = config.answer.length; const answerLength = config.answer.length;
this.createInputs(inputCount); this.createSingleInput(answerLength);
// 隐藏提交按钮
if (this.submitButton) {
this.submitButton.active = false;
}
// 更新倒计时显示 // 更新倒计时显示
this.updateClockLabel(); this.updateClockLabel();
console.log(`[PageLevel] 初始化关卡 ${this.currentLevelIndex + 1}, 答案长度: ${inputCount}`); console.log(`[PageLevel] 初始化关卡 ${this.currentLevelIndex + 1}, 答案长度: ${answerLength}`);
} }
/** /**
* 动态创建输入框 * 创建单个输入框
* @param answerLength 答案长度,用于设置 placeholder 和宽度
*/ */
private createInputs(count: number): void { private createSingleInput(answerLength: number): void {
if (!this.inputLayout || !this.inputTemplate) { if (!this.inputLayout || !this.inputTemplate) {
console.error('[PageLevel] inputLayout 或 inputTemplate 未设置'); console.error('[PageLevel] inputLayout 或 inputTemplate 未设置');
return; return;
@@ -198,31 +195,53 @@ export class PageLevel extends BaseView {
// 隐藏模板节点 // 隐藏模板节点
this.inputTemplate.active = false; this.inputTemplate.active = false;
// 创建指定数量的输入框 // 创建单个输入框
for (let i = 0; i < count; i++) { const inputNode = instantiate(this.inputTemplate);
const inputNode = instantiate(this.inputTemplate); inputNode.active = true;
inputNode.active = true; inputNode.name = 'singleInput';
inputNode.name = `input${i}`;
// 设置位置Layout 会自动排列) // 设置位置
inputNode.setPosition(new Vec3(0, 0, 0)); inputNode.setPosition(new Vec3(0, 0, 0));
// 获取 EditBox 组件并监听事件 // 获取 EditBox 组件
const editBox = inputNode.getComponent(EditBox); const editBox = inputNode.getComponent(EditBox);
if (editBox) { if (editBox) {
// 清空输入内容 // 设置 placeholder 提示
editBox.string = ''; editBox.placeholder = `(${answerLength}个字)`;
// 监听文本变化事件
editBox.node.on(EditBox.EventType.TEXT_CHANGED, this.onInputTextChanged, this);
// 监听编辑结束事件
editBox.node.on(EditBox.EventType.EDITING_DID_ENDED, this.onInputEditingEnded, this);
}
this.inputLayout.addChild(inputNode); // 设置最大长度为答案长度
this._inputNodes.push(inputNode); 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);
} }
console.log(`[PageLevel] 创建了 ${count} 个输入框`); // 动态调整输入框宽度
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);
}
}
this.inputLayout.addChild(inputNode);
this._inputNodes.push(inputNode);
console.log(`[PageLevel] 创建单个输入框,答案长度: ${answerLength}`);
} }
/** /**
@@ -237,110 +256,39 @@ export class PageLevel extends BaseView {
this._inputNodes = []; this._inputNodes = [];
} }
/**
* 检查所有输入框是否都已填写
*/
private checkAllInputsFilled(): void {
let allFilled = true;
for (const node of this._inputNodes) {
const editBox = node.getComponent(EditBox);
if (!editBox || editBox.string.trim() === '') {
allFilled = false;
break;
}
}
// 根据填写状态显示/隐藏提交按钮
if (this.submitButton) {
this.submitButton.active = allFilled;
}
console.log(`[PageLevel] 检查输入状态: ${allFilled ? '全部已填写' : '未全部填写'}`);
}
/** /**
* 获取所有输入框的值 * 获取所有输入框的值
*/ */
getInputValues(): string[] { getInputValues(): string[] {
const values: string[] = []; if (this._inputNodes.length === 0) return [];
for (const node of this._inputNodes) { const editBox = this._inputNodes[0].getComponent(EditBox);
const editBox = node.getComponent(EditBox); const str = (editBox?.string ?? '').trim();
// 只取第一个字符,去除空格 return [str];
const str = (editBox?.string ?? '').trim();
values.push(str.charAt(0));
}
return values;
} }
/** /**
* 获取拼接后的答案字符串 * 获取拼接后的答案字符串
*/ */
getAnswer(): string { getAnswer(): string {
return this.getInputValues().join(''); if (this._inputNodes.length === 0) return '';
const editBox = this._inputNodes[0].getComponent(EditBox);
return (editBox?.string ?? '').trim();
} }
// ========== EditBox 事件回调 ========== // ========== EditBox 事件回调 ==========
/** 是否正在处理输入(防止递归) */
private _isHandlingInput: boolean = false;
/** /**
* 输入框文本变化回调 * 输入框文本变化回调
*/ */
private onInputTextChanged(editBox: EditBox): void { private onInputTextChanged(_editBox: EditBox): void {
// 防止递归调用 console.log('[PageLevel] 输入内容变化');
if (this._isHandlingInput) return;
// 处理多字符输入,自动分配到后续输入框
this.handleMultiCharInput(editBox);
this.checkAllInputsFilled();
}
/**
* 处理多字符输入,自动分配到后续输入框
*/
private handleMultiCharInput(editBox: EditBox): void {
const text = editBox.string;
if (text.length <= 1) return;
// 找到当前输入框的索引
const currentIndex = this._inputNodes.findIndex(node => node.getComponent(EditBox) === editBox);
if (currentIndex === -1) return;
// 标记正在处理输入
this._isHandlingInput = true;
// 保留第一个字符在当前输入框
const firstChar = text[0];
const remainingChars = text.slice(1);
// 设置当前输入框只保留第一个字符
editBox.string = firstChar;
// 将剩余字符分配到后续输入框
for (let i = 0; i < remainingChars.length; i++) {
const nextIndex = currentIndex + 1 + i;
if (nextIndex < this._inputNodes.length) {
const nextEditBox = this._inputNodes[nextIndex].getComponent(EditBox);
if (nextEditBox) {
// 只在目标输入框为空时填入
if (nextEditBox.string === '') {
nextEditBox.string = remainingChars[i];
}
}
}
}
// 处理完成
this._isHandlingInput = false;
} }
/** /**
* 输入框编辑结束回调 * 输入框编辑结束回调
*/ */
private onInputEditingEnded(_editBox: EditBox): void { private onInputEditingEnded(_editBox: EditBox): void {
this.checkAllInputsFilled(); console.log('[PageLevel] 输入编辑结束');
} }
// ========== IconSetting 按钮相关 ========== // ========== IconSetting 按钮相关 ==========
@@ -533,41 +481,34 @@ export class PageLevel extends BaseView {
// ========== 音效相关方法 ========== // ========== 音效相关方法 ==========
/**
* 播放音效(通用方法)
*/
private playSound(clip: AudioClip | null): void {
if (!clip) return;
const audioSource = this.node.getComponent(AudioSource);
audioSource?.playOneShot(clip);
}
/** /**
* 播放点击音效 * 播放点击音效
*/ */
private playClickSound(): void { private playClickSound(): void {
if (this.clickAudio) { this.playSound(this.clickAudio);
// 使用 audioSource 组件播放一次性音效
const audioSource = this.node.getComponent(AudioSource);
if (audioSource) {
audioSource.playOneShot(this.clickAudio);
}
}
} }
/** /**
* 播放成功音效 * 播放成功音效
*/ */
private playSuccessSound(): void { private playSuccessSound(): void {
if (this.successAudio) { this.playSound(this.successAudio);
const audioSource = this.node.getComponent(AudioSource);
if (audioSource) {
audioSource.playOneShot(this.successAudio);
}
}
} }
/** /**
* 播放失败音效 * 播放失败音效
*/ */
private playFailSound(): void { private playFailSound(): void {
if (this.failAudio) { this.playSound(this.failAudio);
const audioSource = this.node.getComponent(AudioSource);
if (audioSource) {
audioSource.playOneShot(this.failAudio);
}
}
} }
// ========== 倒计时相关方法 ========== // ========== 倒计时相关方法 ==========
@@ -719,6 +660,9 @@ export class PageLevel extends BaseView {
// 播放失败音效 // 播放失败音效
this.playFailSound(); this.playFailSound();
// 触发手机震动
WxSDK.vibrateLong();
} }
/** /**

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,134 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "a34c93d2-cae0-42d4-a2eb-c3155052ad20",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "a34c93d2-cae0-42d4-a2eb-c3155052ad20@6c48a",
"displayName": "ContentBg",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "a34c93d2-cae0-42d4-a2eb-c3155052ad20",
"isUuid": true,
"visible": false,
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"f9941": {
"importer": "sprite-frame",
"uuid": "a34c93d2-cae0-42d4-a2eb-c3155052ad20@f9941",
"displayName": "ContentBg",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 304,
"height": 404,
"rawWidth": 304,
"rawHeight": 404,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-152,
-202,
0,
152,
-202,
0,
-152,
202,
0,
152,
202,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
0,
404,
304,
404,
0,
0,
304,
0
],
"nuv": [
0,
0,
1,
0,
0,
1,
1,
1
],
"minPos": [
-152,
-202,
0
],
"maxPos": [
152,
202,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "a34c93d2-cae0-42d4-a2eb-c3155052ad20@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "a34c93d2-cae0-42d4-a2eb-c3155052ad20@6c48a"
}
}

View File

@@ -145,4 +145,43 @@ export class WxSDK {
console.log('[WxSDK] 分享功能初始化完成'); console.log('[WxSDK] 分享功能初始化完成');
} }
// ==================== 震动相关 ====================
/**
* 触发短震动15ms
* 用于轻量级反馈,如按钮点击
*/
static vibrateShort(): void {
const wxApi = WxSDK.getWx();
if (!wxApi) return;
wxApi.vibrateShort({
type: 'medium',
success: () => {
console.log('[WxSDK] 短震动成功');
},
fail: (err: any) => {
console.warn('[WxSDK] 短震动失败', err);
}
});
}
/**
* 触发长震动400ms
* 用于重要反馈,如错误提示
*/
static vibrateLong(): void {
const wxApi = WxSDK.getWx();
if (!wxApi) return;
wxApi.vibrateLong({
success: () => {
console.log('[WxSDK] 长震动成功');
},
fail: (err: any) => {
console.warn('[WxSDK] 长震动失败', err);
}
});
}
} }