This commit is contained in:
jakciehan
2026-05-12 08:03:21 +08:00
parent d263c7bf48
commit c4bd390478
7 changed files with 292 additions and 198 deletions
+233 -75
View File
@@ -44,6 +44,7 @@ const ProfileScene = {
// Current editing state
_editingField: null, // Which field is being edited
_inputText: '', // Current input text
_originalText: '', // Original text before editing (for change detection)
_errorMessage: '', // Error message to display
_isSubmitting: false, // Whether a submission is in progress
_localViolation: false, // Whether local check found violation
@@ -66,6 +67,7 @@ const ProfileScene = {
_descriptionRect: null,
_avatarRect: null,
_saveBtnRect: null,
_cancelBtnRect: null,
enter() {
this._editingField = null;
@@ -154,18 +156,15 @@ const ProfileScene = {
// Description field
this._renderDescriptionField(ctx);
// Error message
if (this._errorMessage) {
// Error message (only show when NOT editing to avoid overlap)
if (this._errorMessage && !this._editingField) {
ctx.fillStyle = '#FF4444';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.fillText(this._errorMessage, cx, SCREEN_HEIGHT - 140);
}
// Save button
this._renderSaveButton(ctx);
// Editing overlay
// Editing overlay (includes save/cancel buttons)
if (this._editingField) {
this._renderEditOverlay(ctx);
}
@@ -215,16 +214,13 @@ const ProfileScene = {
}
},
// ============================================================
// Editing
// ============================================================
_startEditing(field, currentValue) {
const csm = GameGlobal.contentSecurityManager;
if (!csm || !csm.isInitialized()) return;
this._editingField = field;
this._inputText = currentValue || '';
this._originalText = currentValue || '';
this._errorMessage = '';
this._localViolation = false;
@@ -234,55 +230,51 @@ const ProfileScene = {
if (field === FIELD.SIGNATURE) maxLength = SIGNATURE_MAX;
if (field === FIELD.DESCRIPTION) maxLength = DESCRIPTION_MAX;
// Show keyboard
// Show keyboard with empty defaultValue, so the native "Done" button highlights
// as soon as user types any character. The original value is preserved in
// _originalText and shown in the overlay preview for reference.
wx.showKeyboard({
defaultValue: this._inputText,
defaultValue: '',
maxLength,
multiple: field === FIELD.DESCRIPTION,
confirmHold: false,
confirmType: 'done',
});
// Reset _inputText so we know if user actually typed something
this._inputText = '';
// Listen for keyboard input
this._onKeyboardInput = (res) => {
this._inputText = res.value;
this._inputText = res.value || '';
this._validateInput(field);
};
// Real-time local sensitive word check
if (this._inputText) {
const localCheck = csm.checkLocalText(this._inputText);
if (localCheck.hasViolation) {
this._localViolation = true;
this._errorMessage = '内容包含违规信息,请修改';
} else {
this._localViolation = false;
this._errorMessage = '';
}
// Length validation
if (field === FIELD.NICKNAME) {
if (this._inputText.length < NICKNAME_MIN) {
this._errorMessage = `昵称至少需要${NICKNAME_MIN}个字符`;
this._localViolation = true;
} else if (this._inputText.length > NICKNAME_MAX) {
this._errorMessage = `昵称不能超过${NICKNAME_MAX}个字符`;
this._localViolation = true;
}
} else if (field === FIELD.SIGNATURE && this._inputText.length > SIGNATURE_MAX) {
this._errorMessage = `签名不能超过${SIGNATURE_MAX}个字符`;
this._localViolation = true;
} else if (field === FIELD.DESCRIPTION && this._inputText.length > DESCRIPTION_MAX) {
this._errorMessage = `描述不能超过${DESCRIPTION_MAX}个字符`;
this._localViolation = true;
}
this._onKeyboardConfirm = (res) => {
// Ensure we have the final value on confirm
if (res && res.value !== undefined) {
this._inputText = res.value;
}
this._validateInput(field);
// Only auto-submit if no violations
if (!this._localViolation) {
this._handleSubmit();
}
};
this._onKeyboardConfirm = () => {
this._handleSubmit();
this._onKeyboardComplete = (res) => {
// Keyboard closed - get final value
if (res && res.value !== undefined) {
this._inputText = res.value;
this._validateInput(field);
}
};
wx.onKeyboardInput(this._onKeyboardInput);
wx.onKeyboardConfirm(this._onKeyboardConfirm);
wx.onKeyboardComplete(this._onKeyboardComplete);
// Initial validation to set correct button state
this._validateInput(field);
},
_stopEditing() {
@@ -294,10 +286,61 @@ const ProfileScene = {
wx.offKeyboardConfirm(this._onKeyboardConfirm);
this._onKeyboardConfirm = null;
}
if (this._onKeyboardComplete) {
wx.offKeyboardComplete(this._onKeyboardComplete);
this._onKeyboardComplete = null;
}
wx.hideKeyboard();
this._editingField = null;
},
_validateInput(field) {
const csm = GameGlobal.contentSecurityManager;
const text = this._inputText || '';
// Reset violation state first
this._localViolation = false;
this._errorMessage = '';
// Empty check
if (!text || text.trim().length === 0) {
this._localViolation = true;
this._errorMessage = '内容不能为空';
return;
}
// Sensitive word check
if (csm && csm.isInitialized()) {
const localCheck = csm.checkLocalText(text);
if (localCheck.hasViolation) {
this._localViolation = true;
this._errorMessage = '内容包含违规信息,请修改';
return;
}
}
// Length validation
if (field === FIELD.NICKNAME) {
if (text.length < NICKNAME_MIN) {
this._localViolation = true;
this._errorMessage = `昵称至少需要${NICKNAME_MIN}个字符`;
} else if (text.length > NICKNAME_MAX) {
this._localViolation = true;
this._errorMessage = `昵称不能超过${NICKNAME_MAX}个字符`;
}
} else if (field === FIELD.SIGNATURE) {
if (text.length > SIGNATURE_MAX) {
this._localViolation = true;
this._errorMessage = `签名不能超过${SIGNATURE_MAX}个字符`;
}
} else if (field === FIELD.DESCRIPTION) {
if (text.length > DESCRIPTION_MAX) {
this._localViolation = true;
this._errorMessage = `描述不能超过${DESCRIPTION_MAX}个字符`;
}
}
},
async _handleSubmit() {
if (this._isSubmitting || this._localViolation) return;
@@ -478,66 +521,167 @@ const ProfileScene = {
}
},
_renderSaveButton(ctx) {
const rect = this._saveBtnRect;
const isActive = this._editingField && !this._localViolation && !this._isSubmitting;
ctx.fillStyle = isActive ? '#4a90d9' : '#555555';
ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
ctx.fillStyle = '#FFFFFF';
ctx.font = 'bold 14px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(this._isSubmitting ? '审核中...' : t('profile.save'), rect.x + rect.w / 2, rect.y + rect.h / 2);
},
_renderEditOverlay(ctx) {
// Semi-transparent overlay at bottom
const overlayY = SCREEN_HEIGHT * 0.6;
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fillRect(0, overlayY, SCREEN_WIDTH, SCREEN_HEIGHT - overlayY);
// Place overlay at TOP of screen, so save/cancel buttons are NOT hidden by keyboard.
// The keyboard occupies the bottom portion of the screen when shown.
const overlayY = 0;
const overlayH = Math.min(SCREEN_HEIGHT * 0.4, 220);
ctx.fillStyle = 'rgba(0, 0, 0, 0.92)';
ctx.fillRect(0, overlayY, SCREEN_WIDTH, overlayH);
// Current input text preview
ctx.fillStyle = '#FFFFFF';
ctx.font = '14px Arial';
// Field label
const fieldLabels = {
[FIELD.NICKNAME]: t('profile.nickname'),
[FIELD.SIGNATURE]: t('profile.signature'),
[FIELD.DESCRIPTION]: t('profile.description'),
};
ctx.fillStyle = '#AAAAAA';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('编辑 ' + (fieldLabels[this._editingField] || ''), CENTER_X, overlayY + 14);
// Input text preview area
const previewBoxY = overlayY + 30;
const previewBoxH = 50;
ctx.fillStyle = 'rgba(255, 255, 255, 0.08)';
ctx.fillRect(20, previewBoxY, SCREEN_WIDTH - 40, previewBoxH);
ctx.strokeStyle = '#4a90d9';
ctx.lineWidth = 1;
ctx.strokeRect(20, previewBoxY, SCREEN_WIDTH - 40, previewBoxH);
// Current input text
ctx.fillStyle = '#FFFFFF';
ctx.font = '15px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const previewText = this._inputText || '';
const lines = this._wrapText(ctx, previewText, INPUT_WIDTH, 3);
for (let i = 0; i < lines.length; i++) {
ctx.fillText(lines[i], CENTER_X, overlayY + 30 + i * 20);
if (previewText) {
const lines = this._wrapText(ctx, previewText, SCREEN_WIDTH - 60, 2);
for (let i = 0; i < lines.length; i++) {
ctx.fillText(lines[i], CENTER_X, previewBoxY + 16 + i * 18);
}
} else {
// Placeholder
ctx.fillStyle = '#888888';
ctx.font = '13px Arial';
ctx.fillText(
this._originalText ? `当前: ${this._originalText}(在下方键盘输入新内容)` : '请在下方键盘输入',
CENTER_X,
previewBoxY + previewBoxH / 2
);
}
// Character count
let maxLen = 200;
if (this._editingField === FIELD.NICKNAME) maxLen = NICKNAME_MAX;
if (this._editingField === FIELD.SIGNATURE) maxLen = SIGNATURE_MAX;
ctx.fillStyle = '#888888';
ctx.font = '11px Arial';
ctx.fillText(`${this._inputText.length}/${maxLen}`, CENTER_X, overlayY + 100);
ctx.textAlign = 'right';
ctx.fillText(`${this._inputText.length}/${maxLen}`, SCREEN_WIDTH - 24, previewBoxY + previewBoxH + 12);
// Violation warning
if (this._localViolation) {
if (this._localViolation && this._errorMessage) {
ctx.fillStyle = '#FF4444';
ctx.font = 'bold 12px Arial';
ctx.fillText('⚠ ' + this._errorMessage, CENTER_X, overlayY + 120);
ctx.textAlign = 'center';
ctx.fillText('⚠ ' + this._errorMessage, CENTER_X, previewBoxY + previewBoxH + 12);
}
// Buttons row at bottom of overlay
const btnRowY = overlayY + overlayH - BTN_HEIGHT - 12;
const btnGap = 12;
const btnW = (SCREEN_WIDTH - 40 - btnGap) / 2;
// Cancel button (left)
this._cancelBtnRect = {
x: 20,
y: btnRowY,
w: btnW,
h: BTN_HEIGHT,
};
// Save button (right)
this._saveBtnRect = {
x: 20 + btnW + btnGap,
y: btnRowY,
w: btnW,
h: BTN_HEIGHT,
};
const btnR = 6;
// Cancel button render
ctx.fillStyle = '#3a3a3a';
this._drawRoundRect(ctx, this._cancelBtnRect.x, this._cancelBtnRect.y, this._cancelBtnRect.w, this._cancelBtnRect.h, btnR);
ctx.fill();
ctx.strokeStyle = '#666666';
ctx.lineWidth = 1;
this._drawRoundRect(ctx, this._cancelBtnRect.x, this._cancelBtnRect.y, this._cancelBtnRect.w, this._cancelBtnRect.h, btnR);
ctx.stroke();
ctx.fillStyle = '#CCCCCC';
ctx.font = '14px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(t('common.cancel') || '取消', this._cancelBtnRect.x + this._cancelBtnRect.w / 2, this._cancelBtnRect.y + this._cancelBtnRect.h / 2);
// Save button render: always highlighted unless submitting
const isActive = !this._isSubmitting;
if (isActive) {
ctx.shadowColor = '#00CC66';
ctx.shadowBlur = 16;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.fillStyle = '#00CC66';
} else {
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
ctx.fillStyle = '#3a3a3a';
}
this._drawRoundRect(ctx, this._saveBtnRect.x, this._saveBtnRect.y, this._saveBtnRect.w, this._saveBtnRect.h, btnR);
ctx.fill();
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
if (isActive) {
ctx.strokeStyle = '#33FF99';
ctx.lineWidth = 2;
} else {
ctx.strokeStyle = '#555555';
ctx.lineWidth = 1;
}
this._drawRoundRect(ctx, this._saveBtnRect.x, this._saveBtnRect.y, this._saveBtnRect.w, this._saveBtnRect.h, btnR);
ctx.stroke();
ctx.fillStyle = isActive ? '#FFFFFF' : '#666666';
ctx.font = 'bold 15px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(this._isSubmitting ? '审核中...' : t('profile.save'), this._saveBtnRect.x + this._saveBtnRect.w / 2, this._saveBtnRect.y + this._saveBtnRect.h / 2);
},
_handleEditOverlayTouch(x, y) {
// Save button
// Save button - always allow click, _handleSubmit will validate
if (this._hitTest(x, y, this._saveBtnRect)) {
if (!this._localViolation && !this._isSubmitting) {
this._handleSubmit();
if (!this._isSubmitting) {
// Re-validate before submit (in case keyboard callbacks did not fire)
this._validateInput(this._editingField);
if (!this._localViolation) {
this._handleSubmit();
}
// If validation fails, error message is now shown via _localViolation/_errorMessage
}
return;
}
// Tap outside to cancel
this._stopEditing();
// Cancel button
if (this._hitTest(x, y, this._cancelBtnRect)) {
this._stopEditing();
return;
}
// Ignore taps elsewhere inside the overlay (do NOT auto-cancel)
},
// ============================================================
@@ -548,6 +692,20 @@ const ProfileScene = {
return rect && x >= rect.x && x <= rect.x + rect.w && y >= rect.y && y <= rect.y + rect.h;
},
_drawRoundRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.arcTo(x + w, y, x + w, y + r, r);
ctx.lineTo(x + w, y + h - r);
ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
ctx.lineTo(x + r, y + h);
ctx.arcTo(x, y + h, x, y + h - r, r);
ctx.lineTo(x, y + r);
ctx.arcTo(x, y, x + r, y, r);
ctx.closePath();
},
_wrapText(ctx, text, maxWidth, maxLines) {
const words = text.split('');
const lines = [];