feat: 重构输入框为单输入框模式并添加震动反馈
- 将多输入框改为单个输入框,根据答案长度动态调整宽度 - 输入框 placeholder 显示答案字数提示 - 答案错误时触发微信小游戏震动反馈 - WxSDK 新增 vibrateShort/vibrateLong 方法 - 重构音效播放方法,提取公共 playSound 方法
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
BIN
assets/resources/images/pageLevel/ContentBg.png
Normal file
BIN
assets/resources/images/pageLevel/ContentBg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
134
assets/resources/images/pageLevel/ContentBg.png.meta
Normal file
134
assets/resources/images/pageLevel/ContentBg.png.meta
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user