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
@@ -19,6 +19,14 @@ const SENSITIVE_WORDS = {
'颠覆国家', '推翻政权', '分裂国家', '恐怖组织', '极端主义', '颠覆国家', '推翻政权', '分裂国家', '恐怖组织', '极端主义',
'反动', '暴乱', '煽动颠覆', '分裂势力', '恐怖袭击', '反动', '暴乱', '煽动颠覆', '分裂势力', '恐怖袭击',
'邪教组织', '法轮', '法轮功', '台独', '藏独', '疆独', '邪教组织', '法轮', '法轮功', '台独', '藏独', '疆独',
// Political figure names and common variants (homophone / split-char evasion)
'习近平', '刁近平', '习大大', '习主席', '习总',
'XiJinping', 'xijinping', '习近', '近平',
'李强', '王岐山', '栗战书', '汪洋', '韩正',
'李克强', '胡锦涛', '江泽民', '温家宝', '朱镕基',
'邓小平', '毛泽东', '周恩来', '刘少奇', '彭德怀',
'薄熙来', '周永康', '徐才厚', '郭伯雄', '令计划',
'孙政才', '赵乐际', '王沪宁', '丁薛祥', '蔡奇',
], ],
pornography: [ pornography: [
@@ -98,10 +106,17 @@ function checkText(text) {
const matchedWords = []; const matchedWords = [];
const categories = new Set(); const categories = new Set();
const lowerText = text.toLowerCase(); const lowerText = text.toLowerCase();
// Strip common evasion characters for split-char detection
const strippedText = text.replace(/[\s\u3000.,;:!?·…—\-_\|\\/~`@#$%^&*+=<>()\[\]{}""''「」『』【】()〈〕\u200b\u200c\u200d\ufeff]/g, '').toLowerCase();
for (const [category, words] of Object.entries(SENSITIVE_WORDS)) { for (const [category, words] of Object.entries(SENSITIVE_WORDS)) {
for (const word of words) { for (const word of words) {
if (lowerText.includes(word.toLowerCase())) { const lowerWord = word.toLowerCase();
if (lowerText.includes(lowerWord)) {
matchedWords.push(word);
categories.add(category);
} else if (word.length >= 2 && strippedText.includes(lowerWord)) {
// Split-char evasion detected
matchedWords.push(word); matchedWords.push(word);
categories.add(category); categories.add(category);
} }
@@ -120,7 +135,7 @@ function checkText(text) {
* @returns {string} * @returns {string}
*/ */
function getVersion() { function getVersion() {
return '2026-05-11-v1'; return '2026-05-12-v2';
} }
module.exports = { module.exports = {
+1 -1
View File
@@ -8,6 +8,7 @@ module.exports = {
// Common // Common
// ============================================================ // ============================================================
'common.back': '← Back', 'common.back': '← Back',
'common.cancel': 'Cancel',
'common.joinBtn': 'Join', 'common.joinBtn': 'Join',
'common.cannotConnect': 'Cannot connect to server', 'common.cannotConnect': 'Cannot connect to server',
'common.connectFailed': 'Connection failed', 'common.connectFailed': 'Connection failed',
@@ -208,7 +209,6 @@ module.exports = {
'settings.sound': 'Sound', 'settings.sound': 'Sound',
'settings.music': 'Music', 'settings.music': 'Music',
'settings.vibration': 'Vibration', 'settings.vibration': 'Vibration',
'settings.nickname': 'Display Name',
'settings.profile': 'Profile', 'settings.profile': 'Profile',
// ============================================================ // ============================================================
+1 -1
View File
@@ -8,6 +8,7 @@ module.exports = {
// Common // Common
// ============================================================ // ============================================================
'common.back': '← 返回', 'common.back': '← 返回',
'common.cancel': '取消',
'common.joinBtn': '加入', 'common.joinBtn': '加入',
'common.cannotConnect': '无法连接服务器', 'common.cannotConnect': '无法连接服务器',
'common.connectFailed': '连接失败', 'common.connectFailed': '连接失败',
@@ -208,7 +209,6 @@ module.exports = {
'settings.sound': '音效', 'settings.sound': '音效',
'settings.music': '音乐', 'settings.music': '音乐',
'settings.vibration': '振动', 'settings.vibration': '振动',
'settings.nickname': '显示名字',
'settings.profile': '个人资料', 'settings.profile': '个人资料',
// ============================================================ // ============================================================
+20 -2
View File
@@ -52,6 +52,13 @@ const FALLBACK_WORDS = [
// Politics // Politics
'颠覆国家', '推翻政权', '分裂国家', '恐怖组织', '极端主义', '颠覆国家', '推翻政权', '分裂国家', '恐怖组织', '极端主义',
'法轮', '法轮功', '台独', '藏独', '疆独', '法轮', '法轮功', '台独', '藏独', '疆独',
'习近平', '刁近平', '习大大', '习主席', '习总',
'XiJinping', 'xijinping', '习近', '近平',
'李强', '王岐山', '栗战书', '汪洋', '韩正',
'李克强', '胡锦涛', '江泽民', '温家宝', '朱镕基',
'邓小平', '毛泽东', '周恩来', '刘少奇', '彭德怀',
'薄熙来', '周永康', '徐才厚', '郭伯雄', '令计划',
'孙政才', '赵乐际', '王沪宁', '丁薛祥', '蔡奇',
// Pornography // Pornography
'色情', '淫秽', '裸体', '卖淫', '嫖娼', '色情', '淫秽', '裸体', '卖淫', '嫖娼',
'约炮', '援交', '一夜情', '黄色视频', '约炮', '援交', '一夜情', '黄色视频',
@@ -130,13 +137,24 @@ class ContentSecurityManager {
const startTime = Date.now(); const startTime = Date.now();
const lowerContent = content.toLowerCase(); const lowerContent = content.toLowerCase();
// Strip common evasion characters (punctuation, spaces, zero-width chars) for split-char detection
const strippedContent = content.replace(/[\s\u3000.,;:!?·…—\-_\|\\/~`@#$%^&*+=<>()\[\]{}""''「」『』【】()〈〕\u200b\u200c\u200d\ufeff]/g, '').toLowerCase();
const matchedWords = []; const matchedWords = [];
for (let i = 0; i < this._words.length; i++) { for (let i = 0; i < this._words.length; i++) {
const word = this._words[i]; const word = this._words[i];
if (word && lowerContent.includes(word.toLowerCase())) { if (!word) continue;
const lowerWord = word.toLowerCase();
// Direct match
if (lowerContent.includes(lowerWord)) {
matchedWords.push(word);
if (matchedWords.length >= 3) break;
continue;
}
// Split-char evasion match: check if word chars appear in order with evasion chars between
// Only for multi-char words (length >= 2)
if (word.length >= 2 && strippedContent.includes(lowerWord)) {
matchedWords.push(word); matchedWords.push(word);
// Early exit if we find violations (performance optimization)
if (matchedWords.length >= 3) break; if (matchedWords.length >= 3) break;
} }
} }
+229 -71
View File
@@ -44,6 +44,7 @@ const ProfileScene = {
// Current editing state // Current editing state
_editingField: null, // Which field is being edited _editingField: null, // Which field is being edited
_inputText: '', // Current input text _inputText: '', // Current input text
_originalText: '', // Original text before editing (for change detection)
_errorMessage: '', // Error message to display _errorMessage: '', // Error message to display
_isSubmitting: false, // Whether a submission is in progress _isSubmitting: false, // Whether a submission is in progress
_localViolation: false, // Whether local check found violation _localViolation: false, // Whether local check found violation
@@ -66,6 +67,7 @@ const ProfileScene = {
_descriptionRect: null, _descriptionRect: null,
_avatarRect: null, _avatarRect: null,
_saveBtnRect: null, _saveBtnRect: null,
_cancelBtnRect: null,
enter() { enter() {
this._editingField = null; this._editingField = null;
@@ -154,18 +156,15 @@ const ProfileScene = {
// Description field // Description field
this._renderDescriptionField(ctx); this._renderDescriptionField(ctx);
// Error message // Error message (only show when NOT editing to avoid overlap)
if (this._errorMessage) { if (this._errorMessage && !this._editingField) {
ctx.fillStyle = '#FF4444'; ctx.fillStyle = '#FF4444';
ctx.font = '12px Arial'; ctx.font = '12px Arial';
ctx.textAlign = 'center'; ctx.textAlign = 'center';
ctx.fillText(this._errorMessage, cx, SCREEN_HEIGHT - 140); ctx.fillText(this._errorMessage, cx, SCREEN_HEIGHT - 140);
} }
// Save button // Editing overlay (includes save/cancel buttons)
this._renderSaveButton(ctx);
// Editing overlay
if (this._editingField) { if (this._editingField) {
this._renderEditOverlay(ctx); this._renderEditOverlay(ctx);
} }
@@ -215,16 +214,13 @@ const ProfileScene = {
} }
}, },
// ============================================================
// Editing
// ============================================================
_startEditing(field, currentValue) { _startEditing(field, currentValue) {
const csm = GameGlobal.contentSecurityManager; const csm = GameGlobal.contentSecurityManager;
if (!csm || !csm.isInitialized()) return; if (!csm || !csm.isInitialized()) return;
this._editingField = field; this._editingField = field;
this._inputText = currentValue || ''; this._inputText = currentValue || '';
this._originalText = currentValue || '';
this._errorMessage = ''; this._errorMessage = '';
this._localViolation = false; this._localViolation = false;
@@ -234,55 +230,51 @@ const ProfileScene = {
if (field === FIELD.SIGNATURE) maxLength = SIGNATURE_MAX; if (field === FIELD.SIGNATURE) maxLength = SIGNATURE_MAX;
if (field === FIELD.DESCRIPTION) maxLength = DESCRIPTION_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({ wx.showKeyboard({
defaultValue: this._inputText, defaultValue: '',
maxLength, maxLength,
multiple: field === FIELD.DESCRIPTION, multiple: field === FIELD.DESCRIPTION,
confirmHold: false, confirmHold: false,
confirmType: 'done', confirmType: 'done',
}); });
// Reset _inputText so we know if user actually typed something
this._inputText = '';
// Listen for keyboard input // Listen for keyboard input
this._onKeyboardInput = (res) => { this._onKeyboardInput = (res) => {
this._inputText = res.value || '';
this._validateInput(field);
};
this._onKeyboardConfirm = (res) => {
// Ensure we have the final value on confirm
if (res && res.value !== undefined) {
this._inputText = res.value; this._inputText = res.value;
// 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._validateInput(field);
// Only auto-submit if no violations
if (!this._localViolation) {
this._handleSubmit();
} }
}; };
this._onKeyboardConfirm = () => { this._onKeyboardComplete = (res) => {
this._handleSubmit(); // Keyboard closed - get final value
if (res && res.value !== undefined) {
this._inputText = res.value;
this._validateInput(field);
}
}; };
wx.onKeyboardInput(this._onKeyboardInput); wx.onKeyboardInput(this._onKeyboardInput);
wx.onKeyboardConfirm(this._onKeyboardConfirm); wx.onKeyboardConfirm(this._onKeyboardConfirm);
wx.onKeyboardComplete(this._onKeyboardComplete);
// Initial validation to set correct button state
this._validateInput(field);
}, },
_stopEditing() { _stopEditing() {
@@ -294,10 +286,61 @@ const ProfileScene = {
wx.offKeyboardConfirm(this._onKeyboardConfirm); wx.offKeyboardConfirm(this._onKeyboardConfirm);
this._onKeyboardConfirm = null; this._onKeyboardConfirm = null;
} }
if (this._onKeyboardComplete) {
wx.offKeyboardComplete(this._onKeyboardComplete);
this._onKeyboardComplete = null;
}
wx.hideKeyboard(); wx.hideKeyboard();
this._editingField = null; 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() { async _handleSubmit() {
if (this._isSubmitting || this._localViolation) return; 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) { _renderEditOverlay(ctx) {
// Semi-transparent overlay at bottom // Place overlay at TOP of screen, so save/cancel buttons are NOT hidden by keyboard.
const overlayY = SCREEN_HEIGHT * 0.6; // The keyboard occupies the bottom portion of the screen when shown.
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; const overlayY = 0;
ctx.fillRect(0, overlayY, SCREEN_WIDTH, SCREEN_HEIGHT - overlayY); 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 // Field label
ctx.fillStyle = '#FFFFFF'; const fieldLabels = {
ctx.font = '14px Arial'; [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.textAlign = 'center';
ctx.textBaseline = 'middle'; 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 previewText = this._inputText || '';
const lines = this._wrapText(ctx, previewText, INPUT_WIDTH, 3); if (previewText) {
const lines = this._wrapText(ctx, previewText, SCREEN_WIDTH - 60, 2);
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
ctx.fillText(lines[i], CENTER_X, overlayY + 30 + i * 20); 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 // Character count
let maxLen = 200; let maxLen = 200;
if (this._editingField === FIELD.NICKNAME) maxLen = NICKNAME_MAX; if (this._editingField === FIELD.NICKNAME) maxLen = NICKNAME_MAX;
if (this._editingField === FIELD.SIGNATURE) maxLen = SIGNATURE_MAX; if (this._editingField === FIELD.SIGNATURE) maxLen = SIGNATURE_MAX;
ctx.fillStyle = '#888888'; ctx.fillStyle = '#888888';
ctx.font = '11px Arial'; 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 // Violation warning
if (this._localViolation) { if (this._localViolation && this._errorMessage) {
ctx.fillStyle = '#FF4444'; ctx.fillStyle = '#FF4444';
ctx.font = 'bold 12px Arial'; 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) { _handleEditOverlayTouch(x, y) {
// Save button // Save button - always allow click, _handleSubmit will validate
if (this._hitTest(x, y, this._saveBtnRect)) { if (this._hitTest(x, y, this._saveBtnRect)) {
if (!this._localViolation && !this._isSubmitting) { if (!this._isSubmitting) {
// Re-validate before submit (in case keyboard callbacks did not fire)
this._validateInput(this._editingField);
if (!this._localViolation) {
this._handleSubmit(); this._handleSubmit();
} }
// If validation fails, error message is now shown via _localViolation/_errorMessage
}
return; return;
} }
// Tap outside to cancel // Cancel button
if (this._hitTest(x, y, this._cancelBtnRect)) {
this._stopEditing(); 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; 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) { _wrapText(ctx, text, maxWidth, maxLines) {
const words = text.split(''); const words = text.split('');
const lines = []; const lines = [];
-112
View File
@@ -68,7 +68,6 @@ const SettingsScene = {
// Rows: nickname + 3 toggles. Distribute evenly between title and back btn. // Rows: nickname + 3 toggles. Distribute evenly between title and back btn.
const rows = [ const rows = [
{ type: 'nickname' },
{ type: 'toggle', key: 'soundEnabled', label: t('settings.sound'), icon: '🔊' }, { type: 'toggle', key: 'soundEnabled', label: t('settings.sound'), icon: '🔊' },
{ type: 'toggle', key: 'musicEnabled', label: t('settings.music'), icon: '🎵' }, { type: 'toggle', key: 'musicEnabled', label: t('settings.music'), icon: '🎵' },
{ type: 'toggle', key: 'vibrationEnabled', label: t('settings.vibration'), icon: '📳' }, { type: 'toggle', key: 'vibrationEnabled', label: t('settings.vibration'), icon: '📳' },
@@ -83,12 +82,8 @@ const SettingsScene = {
for (let i = 0; i < rows.length; i++) { for (let i = 0; i < rows.length; i++) {
const row = rows[i]; const row = rows[i];
const cy = firstCenterY + i * step; const cy = firstCenterY + i * step;
if (row.type === 'nickname') {
this._renderNicknameRow(ctx, cx, cy);
} else {
this._renderToggle(ctx, cx, cy, row); this._renderToggle(ctx, cx, cy, row);
} }
}
// Profile entry button (below the last toggle row) // Profile entry button (below the last toggle row)
const profileY = firstCenterY + rows.length * step; const profileY = firstCenterY + rows.length * step;
@@ -98,47 +93,6 @@ const SettingsScene = {
this._renderBackButton(ctx, cx, backCenterY); this._renderBackButton(ctx, cx, backCenterY);
}, },
_renderNicknameRow(ctx, cx, y) {
const w = SCREEN_WIDTH * 0.7;
const h = 50;
const x = cx - w / 2;
this._buttons['nickname'] = { x, y: y - h / 2, w, h };
// Background
ctx.fillStyle = '#1e1e3a';
ctx.fillRect(x, y - h / 2, w, h);
ctx.strokeStyle = '#333366';
ctx.lineWidth = 1;
ctx.strokeRect(x, y - h / 2, w, h);
// Icon + label (left)
ctx.fillStyle = COLORS.HUD_TEXT;
ctx.font = '16px Arial';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
const leftLabel = `👤 ${t('settings.nickname') || '显示名字'}`;
ctx.fillText(leftLabel, x + 15, y);
// Current value + chevron (right)
const profile = GameGlobal.playerProfile;
let shown = '';
if (profile) {
if (profile.granted && profile.nickname) {
shown = profile.truncate ? profile.truncate(profile.nickname, 5) : profile.nickname;
} else if (typeof profile.getDisplayName === 'function') {
const pid = (GameGlobal.networkManager && GameGlobal.networkManager.playerId) || '';
shown = profile.getDisplayName(pid);
}
}
if (!shown) shown = 'Tanker';
ctx.fillStyle = profile && profile.granted ? '#FFD700' : '#8899AA';
ctx.font = '13px Arial';
ctx.textAlign = 'right';
ctx.fillText(`${shown} `, x + w - 15, y);
},
_renderToggle(ctx, cx, y, toggle) { _renderToggle(ctx, cx, y, toggle) {
const w = SCREEN_WIDTH * 0.7; const w = SCREEN_WIDTH * 0.7;
const h = 50; const h = 50;
@@ -243,10 +197,6 @@ const SettingsScene = {
if (tx >= rect.x && tx <= rect.x + rect.w && ty >= rect.y && ty <= rect.y + rect.h) { if (tx >= rect.x && tx <= rect.x + rect.w && ty >= rect.y && ty <= rect.y + rect.h) {
if (key === 'back') { if (key === 'back') {
GameGlobal.sceneManager.switchTo(SCENE.MENU); GameGlobal.sceneManager.switchTo(SCENE.MENU);
} else if (key === 'nickname') {
// IMPORTANT: wx.getUserProfile must be called synchronously from a
// user tap handler; invoking it here is fine (touchstart is a tap).
this._requestNicknameAuth();
} else if (key === 'profile') { } else if (key === 'profile') {
const sm = GameGlobal.sceneManager; const sm = GameGlobal.sceneManager;
if (!sm._scenes.has(SCENE.PROFILE)) { if (!sm._scenes.has(SCENE.PROFILE)) {
@@ -264,68 +214,6 @@ const SettingsScene = {
} }
}, },
// ============================================================
// Nickname acquisition (moved from MenuScene)
// ============================================================
_requestNicknameAuth() {
const profile = GameGlobal.playerProfile;
if (!profile) return;
const onDone = (ok) => {
if (ok) {
try {
wx.showToast({
title: `欢迎 ${profile.nickname}`,
icon: 'none',
duration: 1500,
});
} catch (e) { /* ignore */ }
}
};
if (typeof profile.requestUserProfile === 'function') {
profile.requestUserProfile().then((ok) => {
if (ok) {
onDone(true);
} else {
this._promptManualNickname(onDone);
}
}).catch(() => this._promptManualNickname(onDone));
} else {
this._promptManualNickname(onDone);
}
},
_promptManualNickname(cb) {
try {
if (typeof wx === 'undefined' || typeof wx.showModal !== 'function') {
cb && cb(false);
return;
}
wx.showModal({
title: '设置昵称',
content: '输入在对战中显示的名字(最长16字)',
editable: true,
placeholderText: '例如:坏蹄子',
confirmText: '确定',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
const profile = GameGlobal.playerProfile;
const ok = profile && typeof profile.setManualNickname === 'function'
&& profile.setManualNickname(res.content || '');
cb && cb(!!ok);
} else {
cb && cb(false);
}
},
fail: () => cb && cb(false),
});
} catch (e) {
console.warn('[Settings] showModal failed:', e && e.message);
cb && cb(false);
}
},
}; };
module.exports = SettingsScene; module.exports = SettingsScene;
+17 -2
View File
@@ -19,6 +19,14 @@ const SENSITIVE_WORDS = {
'颠覆国家', '推翻政权', '分裂国家', '恐怖组织', '极端主义', '颠覆国家', '推翻政权', '分裂国家', '恐怖组织', '极端主义',
'反动', '暴乱', '煽动颠覆', '分裂势力', '恐怖袭击', '反动', '暴乱', '煽动颠覆', '分裂势力', '恐怖袭击',
'邪教组织', '法轮', '法轮功', '台独', '藏独', '疆独', '邪教组织', '法轮', '法轮功', '台独', '藏独', '疆独',
// Political figure names and common variants (homophone / split-char evasion)
'习近平', '刁近平', '习大大', '习主席', '习总',
'XiJinping', 'xijinping', '习近', '近平',
'李强', '王岐山', '栗战书', '汪洋', '韩正',
'李克强', '胡锦涛', '江泽民', '温家宝', '朱镕基',
'邓小平', '毛泽东', '周恩来', '刘少奇', '彭德怀',
'薄熙来', '周永康', '徐才厚', '郭伯雄', '令计划',
'孙政才', '赵乐际', '王沪宁', '丁薛祥', '蔡奇',
], ],
pornography: [ pornography: [
@@ -98,10 +106,17 @@ function checkText(text) {
const matchedWords = []; const matchedWords = [];
const categories = new Set(); const categories = new Set();
const lowerText = text.toLowerCase(); const lowerText = text.toLowerCase();
// Strip common evasion characters for split-char detection
const strippedText = text.replace(/[\s\u3000.,;:!?·…—\-_\|\\/~`@#$%^&*+=<>()\[\]{}""''「」『』【】()〈〕\u200b\u200c\u200d\ufeff]/g, '').toLowerCase();
for (const [category, words] of Object.entries(SENSITIVE_WORDS)) { for (const [category, words] of Object.entries(SENSITIVE_WORDS)) {
for (const word of words) { for (const word of words) {
if (lowerText.includes(word.toLowerCase())) { const lowerWord = word.toLowerCase();
if (lowerText.includes(lowerWord)) {
matchedWords.push(word);
categories.add(category);
} else if (word.length >= 2 && strippedText.includes(lowerWord)) {
// Split-char evasion detected
matchedWords.push(word); matchedWords.push(word);
categories.add(category); categories.add(category);
} }
@@ -120,7 +135,7 @@ function checkText(text) {
* @returns {string} * @returns {string}
*/ */
function getVersion() { function getVersion() {
return '2026-05-11-v1'; return '2026-05-12-v2';
} }
module.exports = { module.exports = {