Compare commits

...

2 Commits

Author SHA1 Message Date
richarjiang
394b8d2faf feat: 添加输入框编辑索引管理,优化输入文本分发逻辑 2026-05-13 08:29:12 +08:00
richarjiang
b8da554530 feat: 添加分享模式按钮文案支持,优化通关弹窗功能 2026-05-13 08:28:58 +08:00
3 changed files with 171 additions and 27 deletions

View File

@@ -40,7 +40,7 @@ Git 历史采用 Conventional Commits且摘要多为中文例如 `feat:
<claude-mem-context>
# Memory Context
# $CMEM mp-xieyingeng 2026-05-12 9:37pm GMT+8
# $CMEM mp-xieyingeng 2026-05-13 8:12am GMT+8
Legend: 🎯session 🔴bugfix 🟣feature 🔄refactor ✅change 🔵discovery ⚖decision
Format: ID TIME TYPE TITLE

View File

@@ -2,17 +2,20 @@ import { _decorator, Node, EditBox, instantiate, Vec3, Button, Label, Sprite, Sp
import { BaseView } from 'db://assets/scripts/core/BaseView';
import { ViewManager } from 'db://assets/scripts/core/ViewManager';
import { StaminaManager } from 'db://assets/scripts/utils/StaminaManager';
import { WxSDK } from 'db://assets/scripts/utils/WxSDK';
import { WxSDK, getUserProfile, type WxUserInfo } from 'db://assets/scripts/utils/WxSDK';
import { LevelDataManager } from 'db://assets/scripts/utils/LevelDataManager';
import { AuthManager } from 'db://assets/scripts/utils/AuthManager';
import { RuntimeLevelConfig } from 'db://assets/scripts/types/LevelTypes';
import { ToastManager } from 'db://assets/scripts/utils/ToastManager';
import { ShareManager } from 'db://assets/scripts/utils/ShareManager';
import { StorageManager } from 'db://assets/scripts/utils/StorageManager';
import { HttpUtil } from 'db://assets/scripts/utils/HttpUtil';
import { API_ENDPOINTS, API_TIMEOUT } from 'db://assets/scripts/config/ApiConfig';
import { PassModal } from 'db://assets/prefabs/PassModal';
import { WrongModal } from 'db://assets/prefabs/WrongModal';
import { TimeoutModal } from 'db://assets/prefabs/TimeoutModal';
import { CommonModal } from 'db://assets/prefabs/CommonModal';
import { StaminaInfo, NextLevelData, SubmitShareLevel } from 'db://assets/scripts/types/ApiTypes';
import { ApiEnvelope, StaminaInfo, NextLevelData, SubmitShareLevel } from 'db://assets/scripts/types/ApiTypes';
import { AchievementTitleManager } from 'db://assets/scripts/utils/AchievementTitleManager';
import { applyRoundedCorner } from 'db://assets/scripts/utils/roundedMaterial.utils';
const { ccclass, property } = _decorator;
@@ -72,6 +75,12 @@ export class PageLevel extends BaseView {
/** 分享模式只展示提示 1 时,固定放回 TipsLayout 顶部,避免 Layout 把它排到底部被下一题按钮遮挡 */
private static readonly SHARE_MODE_TIP1_Y = 120;
/** 分享模式底部按钮默认文案 */
private static readonly SHARE_NEXT_BUTTON_TEXT = '下一题';
/** 分享模式最后一题提交文案 */
private static readonly SHARE_SUBMIT_BUTTON_TEXT = '提交答案';
// ========== 节点引用 ==========
@property(Node)
inputLayout: Node | null = null;
@@ -225,6 +234,9 @@ export class PageLevel extends BaseView {
/** 最近一次自动提交的答案,避免填满后重复提交同一内容 */
private _lastAutoSubmittedAnswer: string = '';
/** 当前正在编辑的输入格索引 */
private _editingInputIndex: number = -1;
/** 倒计时剩余秒数 */
private _countdown: number = 60;
@@ -302,6 +314,9 @@ export class PageLevel extends BaseView {
/** 是否正在提交分享挑战结果 */
private _isSubmittingShareResult: boolean = false;
/** 本场分享挑战是否已经尝试拉取过用户头像昵称,避免结算流程重复弹窗 */
private _hasRequestedShareUserInfo: boolean = false;
/**
* 页面首次加载时调用
*/
@@ -318,6 +333,7 @@ export class PageLevel extends BaseView {
this._shareLevelIndex = 0;
this._shareSubmissions.clear();
this._isSubmittingShareResult = false;
this._hasRequestedShareUserInfo = false;
console.log('[PageLevel] 进入分享挑战模式');
} else {
// 从 AuthManager 获取首关数据(由 PageLoading → game-data 提供)
@@ -494,6 +510,7 @@ export class PageLevel extends BaseView {
// 设置关卡标题
this.updateTitleLevelLabel();
this.updatePkLevelProgressLabel();
this.updatePkNextLevelButtonText();
// 隐藏包袱答案,通关后再按 punchline 展示
this.hidePunchline();
@@ -645,28 +662,13 @@ export class PageLevel extends BaseView {
// ========== EditBox 事件回调 ==========
/**
* 输入框开始编辑时,把当前所有格子的内容合并到当前输入框里
*/
private onInputEditingBegan(editBox: EditBox): void {
if (this._isSyncingInputText) return;
const inputIndex = this._inputNodes.findIndex(node => node === editBox.node);
if (inputIndex < 0) return;
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;
}
this._editingInputIndex = inputIndex;
}
/**
@@ -682,22 +684,48 @@ export class PageLevel extends BaseView {
private onInputEditingEnded(editBox: EditBox): void {
if (this._isSyncingInputText) return;
const inputIndex = this._inputNodes.findIndex(node => node === editBox.node);
const inputIndex = this._editingInputIndex >= 0
? this._editingInputIndex
: this._inputNodes.findIndex(node => node === editBox.node);
if (inputIndex < 0) return;
this.distributeInputText(editBox.string);
this.applyInputTextToBlocks(editBox.string, inputIndex);
this._editingInputIndex = -1;
this.tryAutoSubmitAnswer();
}
private distributeInputText(text: string): void {
const chars = Array.from(text);
private applyInputTextToBlocks(text: string, inputIndex: number): void {
const chars = Array.from(text.trim());
if (chars.length <= 1) {
this.setInputBlockText(inputIndex, chars[0] ?? '');
return;
}
this.distributeInputText(text, inputIndex);
}
private setInputBlockText(index: number, text: string): void {
const editBox = this._inputNodes[index]?.getComponent(EditBox);
if (!editBox) return;
this._isSyncingInputText = true;
try {
editBox.string = Array.from(text.trim())[0] ?? '';
} finally {
this._isSyncingInputText = false;
}
}
private distributeInputText(text: string, startIndex: number = 0): void {
const chars = Array.from(text.trim());
const safeStartIndex = Math.max(0, Math.min(startIndex, this._inputNodes.length - 1));
this._isSyncingInputText = true;
try {
for (let i = 0; i < this._inputNodes.length; i++) {
for (let i = safeStartIndex; i < this._inputNodes.length; i++) {
const editBox = this._inputNodes[i].getComponent(EditBox);
if (editBox) {
editBox.string = chars[i] ?? '';
editBox.string = chars[i - safeStartIndex] ?? '';
}
}
} finally {
@@ -738,6 +766,10 @@ export class PageLevel extends BaseView {
this.onSubmitAnswer();
}
private normalizeAnswerForCompare(answer: string): string {
return answer.trim().toLocaleLowerCase();
}
// ========== IconSetting 按钮相关 ==========
/**
@@ -1115,6 +1147,21 @@ export class PageLevel extends BaseView {
: `${currentIndex}`;
}
private updatePkNextLevelButtonText(): void {
if (!this.pkNextLevelButton) {
return;
}
const label = this.pkNextLevelButton.getChildByName('Label')?.getComponent(Label);
if (!label) {
return;
}
label.string = this._isFinalShareLevel()
? PageLevel.SHARE_SUBMIT_BUTTON_TEXT
: PageLevel.SHARE_NEXT_BUTTON_TEXT;
}
private _refreshModeUI(): void {
const isPkMode = this._isShareMode;
@@ -1146,6 +1193,7 @@ export class PageLevel extends BaseView {
this.updateTitleLevelLabel();
this.updatePkLevelProgressLabel();
this.updatePkNextLevelButtonText();
}
private _refreshTipsModeUI(): void {
@@ -1711,7 +1759,7 @@ export class PageLevel extends BaseView {
const userAnswer = this.getAnswer();
console.log(`[PageLevel] 提交答案: ${userAnswer}, 正确答案: ${this._currentConfig.answer}`);
if (userAnswer === this._currentConfig.answer) {
if (this.normalizeAnswerForCompare(userAnswer) === this.normalizeAnswerForCompare(this._currentConfig.answer)) {
if (this._isShareMode) {
this._recordCurrentShareSubmission(userAnswer);
}
@@ -1885,11 +1933,20 @@ export class PageLevel extends BaseView {
passModal.setParams({
levelIndex: this.getDisplayLevelNumber(),
nextButtonText: this._isShareMode && this._isFinalShareLevel()
? PageLevel.SHARE_SUBMIT_BUTTON_TEXT
: undefined,
titleInfo,
previousTitleInfo
});
passModal.setCallbacks({
onNextLevel: () => {
if (this._isShareMode) {
this._closePassModal();
void this.goToNextLevel();
return;
}
this._showShareNextConfirmModal(() => {
this._closePassModal();
void this.goToNextLevel();
@@ -2131,9 +2188,12 @@ export class PageLevel extends BaseView {
return;
}
const isFinalShareLevel = this._isFinalShareLevel();
const modal = CommonModal.show(this.commonModalPrefab, {
title: '提示',
content: '还有时间\n确认进入下一题吗',
content: isFinalShareLevel
? '确认提交挑战答案吗?'
: '还有时间\n确认进入下一题吗',
buttonConfirm: '确认',
buttonCancel: '再想想',
zIndex: CommonModal.MODAL_Z_INDEX + 1,
@@ -2216,6 +2276,67 @@ export class PageLevel extends BaseView {
return totalLevels > 0 && this._shareLevelIndex >= totalLevels - 1;
}
private async _ensureShareParticipantUserInfo(): Promise<void> {
if (!this._isShareMode || this._hasRequestedShareUserInfo) {
return;
}
this._hasRequestedShareUserInfo = true;
const cachedUserInfo = StorageManager.getUserInfo();
if (cachedUserInfo && this._hasUsableUserInfo(cachedUserInfo)) {
await this._uploadShareParticipantUserInfo(cachedUserInfo);
return;
}
if (!WxSDK.isWechat()) {
console.log('[PageLevel] 非微信环境,跳过获取用户头像昵称');
return;
}
try {
const userInfo = await getUserProfile();
StorageManager.setUserInfo(userInfo);
await this._uploadShareParticipantUserInfo(userInfo);
} catch (err) {
console.warn('[PageLevel] 获取用户头像昵称失败,继续提交挑战:', err);
ToastManager.show('未授权头像昵称,将使用默认资料提交');
}
}
private _hasUsableUserInfo(userInfo: WxUserInfo): boolean {
return !!userInfo.avatarUrl?.trim()
&& !!userInfo.nickName?.trim()
&& userInfo.nickName !== '微信用户';
}
private async _uploadShareParticipantUserInfo(userInfo: WxUserInfo): Promise<void> {
const userId = AuthManager.instance.userId;
if (!userId) {
console.warn('[PageLevel] 用户未登录,跳过上传头像昵称');
return;
}
try {
const response = await HttpUtil.post<ApiEnvelope<unknown>>(
API_ENDPOINTS.USER_INFO,
{
userId,
avatarUrl: userInfo.avatarUrl,
nickName: userInfo.nickName,
},
API_TIMEOUT.DEFAULT,
);
if (response.success) {
console.log('[PageLevel] 分享挑战用户头像昵称上传成功');
} else {
console.warn('[PageLevel] 分享挑战用户头像昵称上传失败:', response.message);
}
} catch (err) {
console.warn('[PageLevel] 分享挑战用户头像昵称上传异常:', err);
}
}
private async _showShareEndPage(): Promise<void> {
if (this._isSubmittingShareResult) {
return;
@@ -2238,6 +2359,7 @@ export class PageLevel extends BaseView {
this._isSubmittingShareResult = true;
ToastManager.show('正在结算挑战...');
await this._ensureShareParticipantUserInfo();
const result = await ShareManager.instance.submitShareChallenge(payload);
this._isSubmittingShareResult = false;

View File

@@ -21,6 +21,8 @@ export interface PassModalTitleInfo {
interface PassModalParams {
levelIndex?: number;
/** 下一步按钮文案,不传时使用 prefab 默认文案 */
nextButtonText?: string;
titleInfo?: PassModalTitleInfo;
/**
* 通关前的称号信息。传入后,本次显示会把进度条从该起点动画到 titleInfo 的终点;
@@ -93,6 +95,9 @@ export class PassModal extends BaseModal {
/** 进度动画所绑定的对象,用于 Tween.stopAllByTarget */
private readonly _progressTweenTarget: { progress: number } = { progress: 0 };
/** 下一步按钮文案,为 null 时保留 prefab 默认值 */
private _nextButtonText: string | null = null;
/** 进度游标 0% 时的本地 X 坐标,使用 prefab 当前摆放位置作为起点 */
private _progressAnchorStartX: number | null = null;
@@ -107,6 +112,11 @@ export class PassModal extends BaseModal {
if (params?.titleInfo) {
this.setTitleInfo(params.titleInfo);
}
if (params && 'nextButtonText' in params) {
this._nextButtonText = params.nextButtonText ?? null;
this._applyNextButtonText();
}
}
/**
@@ -144,6 +154,7 @@ export class PassModal extends BaseModal {
super.onViewShow();
this._updateWidget();
this._refreshTitleView();
this._applyNextButtonText();
this._playSuccessSound();
this._playProgressAnimation();
}
@@ -234,6 +245,17 @@ export class PassModal extends BaseModal {
}
}
private _applyNextButtonText(): void {
if (!this.nextLevelButton || this._nextButtonText === null) {
return;
}
const label = this.nextLevelButton.getChildByName('Label')?.getComponent(Label);
if (label) {
label.string = this._nextButtonText;
}
}
private _applyProgressText(text: string | undefined): void {
if (this.progressLabel && text !== undefined) {
this.progressLabel.string = text;