diff --git a/pages-game/game/minesweeper/play.scss b/pages-game/game/minesweeper/play.scss index 8f2c441..4c3f453 100644 --- a/pages-game/game/minesweeper/play.scss +++ b/pages-game/game/minesweeper/play.scss @@ -442,6 +442,27 @@ margin-bottom: $spacing-xs; display: flex; gap: $spacing-sm; + + // 最新日志高亮动画(第一个元素是最新的) + &:first-child { + animation: logHighlight 1s ease-out; + background: rgba($brand-primary, 0.1); + padding: 4rpx 8rpx; + border-radius: 4rpx; + margin-left: -8rpx; + margin-right: -8rpx; + } +} + +@keyframes logHighlight { + 0% { + background: rgba($brand-primary, 0.3); + transform: scale(1.02); + } + 100% { + background: rgba($brand-primary, 0.1); + transform: scale(1); + } } .log-time { @@ -969,6 +990,16 @@ border: 1px solid $border-dark; // 确保格子始终保持正方形 aspect-ratio: 1 / 1; + // 阻止长按缩放 + touch-action: none; + user-select: none; + -webkit-user-select: none; + -webkit-touch-callout: none; + // 额外的防护 + -webkit-tap-highlight-color: transparent; + outline: none; + // 禁用任何手势 + overscroll-behavior: none; } .grid-cell { @@ -983,6 +1014,16 @@ aspect-ratio: 1 / 1; width: 100%; height: auto; + // 阻止长按弹出菜单和缩放 + touch-action: none; + user-select: none; + -webkit-user-select: none; + -webkit-touch-callout: none; + // 额外的防护 + -webkit-tap-highlight-color: transparent; + outline: none; + // 禁用任何手势 + overscroll-behavior: none; &:active { transform: scale(0.95); @@ -1000,6 +1041,12 @@ border: 2rpx dashed rgba($accent-cyan, 0.5); } + // 观察者旗帜标记样式 + &.has-flag { + border: 2rpx solid rgba($color-warning, 0.6); + background: rgba($color-warning, 0.1); + } + // --- 新增特效样式 --- &.explosion { animation: explode 0.5s ease-out; @@ -1184,6 +1231,33 @@ font-weight: 900; } + // 观察者旗帜图标 + .spectator-flag { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + animation: flagWave 2s ease-in-out infinite; + + .flag-icon { + font-size: 36rpx; + filter: drop-shadow(0 2rpx 4rpx rgba(0, 0, 0, 0.3)); + } + } + + @keyframes flagWave { + 0%, 100% { + transform: rotate(0deg); + } + 25% { + transform: rotate(5deg); + } + 75% { + transform: rotate(-5deg); + } + } + .magnifier-mark { display: flex; justify-content: center; @@ -1281,6 +1355,15 @@ overflow: hidden; text-overflow: ellipsis; + // 最新日志高亮动画(第一个元素是最新的) + &:first-child { + animation: gameLogHighlight 0.8s ease-out; + background: rgba($brand-primary, 0.15); + padding: 2rpx 6rpx; + border-radius: 4rpx; + font-weight: 600; + } + &.log-system { color: $accent-cyan; } @@ -1290,6 +1373,17 @@ } } +@keyframes gameLogHighlight { + 0% { + background: rgba($brand-primary, 0.4); + transform: scale(1.05); + } + 100% { + background: rgba($brand-primary, 0.15); + transform: scale(1); + } +} + // ===================================== // 弹窗 (全屏遮罩) diff --git a/pages-game/game/minesweeper/play.vue b/pages-game/game/minesweeper/play.vue index b1c086f..e41d245 100644 --- a/pages-game/game/minesweeper/play.vue +++ b/pages-game/game/minesweeper/play.vue @@ -157,18 +157,22 @@ - 💣 @@ -188,6 +192,11 @@ 🔍 + + + 🚩 + + {{ l.text }} @@ -335,6 +344,8 @@ export default { showResultModal: false, onlineCount: 0, onlineCountInterval: null, + spectatorFlags: {}, // 观察者模式的旗帜标记 { cellIndex: true } + longPressTimer: null, // 长按定时器 // Timers matchInterval: null, turnInterval: null, @@ -407,11 +418,6 @@ export default { this.showResultModal = true; } }, - logs() { - this.$nextTick(() => { - this.logsScrollTop = 99999 + Math.random(); - }); - }, 'gameState.currentTurnIndex'() { this.resetTurnTimer(); }, @@ -422,7 +428,11 @@ export default { onLoad(options) { this.fetchGameConfig(); const { game_token, nakama_server, nakama_key, match_id, is_spectator, uid } = options; - if (is_spectator) this.isSpectator = true; + if (is_spectator) { + this.isSpectator = true; + // 观察者模式:禁用页面缩放 + this.disablePageZoom(); + } if (match_id) this.matchId = match_id; if (uid) this.stableUserId = uid; @@ -436,6 +446,33 @@ export default { this.cleanup(); }, methods: { + disablePageZoom() { + // 观察者模式:禁用页面缩放和长按菜单 + // 注意:这些操作在 H5 环境下有效 + // #ifdef H5 + document.addEventListener('contextmenu', (e) => { + e.preventDefault(); + e.stopPropagation(); + return false; + }); + + // 禁用双指缩放 + document.addEventListener('gesturestart', (e) => { + e.preventDefault(); + }); + + document.addEventListener('gesturechange', (e) => { + e.preventDefault(); + }); + + document.addEventListener('gestureend', (e) => { + e.preventDefault(); + }); + // #endif + + // 微信小程序环境下,通过页面配置禁用缩放 + // 这些需要在 pages.json 中配置 + }, async fetchGameConfig() { try { const res = await new Promise((resolve, reject) => { @@ -464,10 +501,19 @@ export default { }, addLog(type, content) { const now = new Date(); - const timeStr = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0'); + const timeStr = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0') + ':' + now.getSeconds().toString().padStart(2, '0'); const id = Date.now() + Math.random().toString(); - this.logs.push({ id, type, content, time: timeStr }); - if (this.logs.length > 50) this.logs.shift(); + + // 插入到数组头部,最新的在最上面 + this.logs.unshift({ id, type, content, time: timeStr }); + + // 增加日志存储限制到 100 条,让用户能看到更多历史 + if (this.logs.length > 100) this.logs.pop(); + + // 确保滚动到顶部显示最新日志 + this.$nextTick(() => { + this.logsScrollTop = 0; + }); }, spawnLabel(x, y, text, type, cellIndex, targetUserId) { const id = Date.now() + Math.random().toString(); @@ -839,7 +885,10 @@ export default { }, handleCellClick(idx) { // 前置条件检查 - if (this.isSpectator) return; + if (this.isSpectator) { + // 观察者模式:不处理普通点击,只处理长按 + return; + } if (!this.gameState?.gameStarted) { uni.showToast({ title: '游戏尚未开始', icon: 'none' }); return; @@ -850,10 +899,73 @@ export default { return; } if (this.gameState.grid[idx].revealed) return; - + // 发送移动指令 nakamaManager.sendMatchState(this.matchId, 3, JSON.stringify({ index: idx })); }, + // 观察者模式:长按插旗 + handleCellLongPress(idx) { + // 只有观察者才能长按插旗 + if (!this.isSpectator) return; + if (!this.gameState?.grid) return; + if (this.gameState.grid[idx].revealed) return; + + // 切换旗帜状态 + if (this.spectatorFlags[idx]) { + this.$delete(this.spectatorFlags, idx); + this.addLog('system', `🚩 移除了位置 ${idx} 的旗帜`); + } else { + this.$set(this.spectatorFlags, idx, true); + this.addLog('system', `🚩 在位置 ${idx} 插上了旗帜`); + } + }, + // 触摸开始 - 用于检测长按 + handleCellTouchStart(idx, event) { + // 只有观察者才处理长按插旗 + if (!this.isSpectator) return; + + // 阻止默认行为,防止长按弹出菜单(仅观察者) + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + + // 清除之前的定时器 + if (this.longPressTimer) { + clearTimeout(this.longPressTimer); + } + + // 设置长按定时器(800ms) + this.longPressTimer = setTimeout(() => { + // 添加震动反馈 + // #ifndef MP-ALIPAY + uni.vibrateShort(); + // #endif + this.handleCellLongPress(idx); + this.longPressTimer = null; + }, 800); + }, + // 触摸结束 - 取消长按 + handleCellTouchEnd(event) { + // 只有观察者需要处理 + if (!this.isSpectator) return; + + if (this.longPressTimer) { + clearTimeout(this.longPressTimer); + this.longPressTimer = null; + } + }, + // 触摸移动 - 取消长按 + handleCellTouchMove(event) { + // 只有观察者需要处理 + if (!this.isSpectator) return; + + // 如果手指移动了,取消长按 + if (this.longPressTimer) { + clearTimeout(this.longPressTimer); + this.longPressTimer = null; + } + }, refreshAndPlayAgain() { uni.removeStorageSync('minesweeper_last_match_id'); uni.navigateBack(); @@ -873,6 +985,13 @@ export default { clearInterval(this.matchInterval); clearInterval(this.turnInterval); clearInterval(this.onlineCountInterval); + + // 清理长按定时器 + if (this.longPressTimer) { + clearTimeout(this.longPressTimer); + this.longPressTimer = null; + } + nakamaManager.disconnect(); }, async fetchOnlineCount() { diff --git a/pages-user/coupons/index.vue b/pages-user/coupons/index.vue index 11596a9..e541d4d 100644 --- a/pages-user/coupons/index.vue +++ b/pages-user/coupons/index.vue @@ -79,7 +79,7 @@ - {{ item.rules || '全场通用' }} + {{ formatRules(item.rules) }} @@ -164,6 +164,18 @@ function formatValue(val) { return (Number(val) / 100).toFixed(0) } +// 格式化优惠券规则描述中的分转为元 +function formatRules(rules) { + if (!rules) return '全场通用' + // 将"XXX分"替换为"¥X.XX元"格式 + return rules.replace(/(\d+)分/g, (match, p1) => { + const yuan = (Number(p1) / 100).toFixed(2) + // 去掉末尾的.00 + const formatted = yuan.endsWith('.00') ? yuan.slice(0, -3) : yuan + return `¥${formatted}元` + }) +} + // 格式化有效期 function formatExpiry(item) { // 后端返回的字段是 valid_end diff --git a/pages.json b/pages.json index 1a29685..364925c 100644 --- a/pages.json +++ b/pages.json @@ -184,6 +184,12 @@ "navigationStyle": "default", "navigationBarTitleText": "扫雷对战", "disableScroll": true, + "mp-weixin": { + "disableSwipeBack": true + }, + "h5": { + "titleNView": false + }, "app-plus": { "bounce": "none" } diff --git a/pages/mine/index.vue b/pages/mine/index.vue index a2a9d47..24e0714 100644 --- a/pages/mine/index.vue +++ b/pages/mine/index.vue @@ -268,7 +268,7 @@ - {{ item.rules || '全场通用' }} + {{ formatCouponRules(item.rules) }} @@ -1135,6 +1135,16 @@ export default { formatCouponValue(val) { return (Number(val) / 100).toFixed(0) }, + formatCouponRules(rules) { + if (!rules) return '全场通用' + // 将"XXX分"替换为"¥X.XX元"格式 + return rules.replace(/(\d+)分/g, (match, p1) => { + const yuan = (Number(p1) / 100).toFixed(2) + // 去掉末尾的.00 + const formatted = yuan.endsWith('.00') ? yuan.slice(0, -3) : yuan + return `¥${formatted}元` + }) + }, formatCouponExpiry(item) { if (!item.end_time) return '长期有效' return `有效期至 ${this.formatDate(item.end_time)}`