commit
This commit is contained in:
@@ -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 = {
|
||||||
|
|||||||
+2
-2
@@ -7,7 +7,8 @@ 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',
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
+2
-2
@@ -7,7 +7,8 @@ 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': '个人资料',
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
@@ -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
@@ -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 = [];
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
Reference in New Issue
Block a user