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)}`