Files
cg eaa0d7101f feat(chat): 添加私信功能和WebSocket实时通信
- 集成WebSocket管理器,支持心跳、自动重连、消息队列
- 实现私信相关API接口,包括会话列表、历史消息、已读标记等
- 添加聊天页面和会话列表页面,支持实时消息收发
- 配置地图权限和腾讯地图SDK用于酒局定位功能
- 修改应用名称为"喝酒了么"并更新应用描述
- 在App.vue中初始化WebSocket连接和私信功能
- 添加详细的私信模块接口文档和Mock数据
- 优化单图片动态高度计算和预加载逻辑
2026-07-21 21:27:01 +08:00

1337 lines
38 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view :class="themeClass" class="page-container games-page">
<!-- 顶部导航 -->
<view class="games-nav" :style="{ paddingTop: navPaddingTop }">
<view class="nav-back" @click="goBack">
<text class="nav-back-icon"></text>
</view>
<text class="nav-title">{{ currentGame === 'hub' ? '酒桌小游戏' : gameMeta[currentGame].name }}</text>
<view class="nav-placeholder"></view>
</view>
<!-- ===== 游戏大厅 ===== -->
<view v-if="currentGame === 'hub'" class="game-hub">
<view class="hub-header">
<text class="hub-deco">🎲</text>
<view class="hub-title-wrap">
<text class="hub-title">今晚喝不喝</text>
<text class="hub-sub">游戏说了算 · 愿赌服输</text>
</view>
</view>
<view class="hub-grid">
<view
class="hub-card"
v-for="(g, i) in gameList"
:key="g.id"
:class="'hub-card-' + g.id"
:style="'animation-delay:' + (i * 0.08) + 's'"
@click="enterGame(g.id)"
>
<view class="hub-card-glow"></view>
<text class="hub-card-emoji">{{ g.emoji }}</text>
<text class="hub-card-name">{{ g.name }}</text>
<text class="hub-card-desc">{{ g.desc }}</text>
<view class="hub-card-players">
<text>{{ g.players }}</text>
</view>
</view>
</view>
<view class="hub-tip">
<text class="hub-tip-icon">🍺</text>
<text class="hub-tip-text">小酌怡情微醺即止酒后不开车</text>
</view>
</view>
<!-- ===== 数字炸弹 ===== -->
<view v-if="currentGame === 'bomb'" class="game-stage">
<view class="bomb-arena">
<view class="bomb-range-card" :class="{ 'bomb-boom': bombBoom }">
<text class="bomb-emoji">{{ bombBoom ? '💥' : '💣' }}</text>
<view class="bomb-range-row">
<text class="bomb-num text-num">{{ bombMin }}</text>
<view class="bomb-fuse">
<view class="bomb-fuse-line" :style="{ width: bombFuseWidth }"></view>
</view>
<text class="bomb-num text-num">{{ bombMax }}</text>
</view>
<text class="bomb-hint">{{ bombBoom ? 'boom!踩中炸弹了!' : '在范围内猜一个数字,踩中就喝' }}</text>
</view>
<view v-if="bombBoom" class="result-banner result-drink">
<text class="result-emoji">🍻</text>
<text class="result-text">喝酒一杯闷</text>
</view>
<view v-else-if="bombSafe" class="result-banner result-safe">
<text class="result-emoji">😎</text>
<text class="result-text">安全过关下一位</text>
</view>
<view class="bomb-history" v-if="bombGuesses.length">
<view class="bomb-guess-tag" v-for="(g, i) in bombGuesses" :key="i" :class="g.dir === 'high' ? 'tag-high' : 'tag-low'">
<text>{{ g.val }} {{ g.dir === 'high' ? '↓' : '↑' }}</text>
</view>
</view>
<view class="bomb-input-area" v-if="!bombBoom && !bombSafe">
<input
class="bomb-input text-num"
type="number"
v-model="bombInput"
:placeholder="bombMin + ' ~ ' + bombMax"
confirm-type="done"
@confirm="bombGuess"
/>
<view class="btn-amber bomb-go-btn" @click="bombGuess">
<text>拆弹</text>
</view>
</view>
<view class="game-actions" v-if="bombBoom || bombSafe">
<view class="btn-amber game-restart-btn" @click="bombReset">
<text>再来一轮</text>
</view>
</view>
</view>
<view class="game-rule-card">
<text class="rule-title">📖 玩法</text>
<text class="rule-text">系统随机埋一个 1~100 的数字炸弹轮流猜数范围不断缩小踩中炸弹的人喝酒范围越小心跳越快</text>
</view>
</view>
<!-- ===== 猜拳大战 ===== -->
<view v-if="currentGame === 'rps'" class="game-stage">
<view class="rps-arena">
<view class="rps-score-row">
<view class="rps-score-item">
<text class="rps-score-label"></text>
<text class="rps-score-num text-num">{{ rpsWin }}</text>
</view>
<text class="rps-vs">VS</text>
<view class="rps-score-item">
<text class="rps-score-label">酒神AI</text>
<text class="rps-score-num text-num">{{ rpsLose }}</text>
</view>
</view>
<view class="rps-battle">
<view class="rps-hand rps-hand-left" :class="{ 'hand-shake': rpsShaking, 'hand-reveal': !rpsShaking && rpsRound > 0 }">
<text class="rps-hand-emoji">{{ rpsShaking ? '✊' : rpsPlayerHand }}</text>
</view>
<view class="rps-clash" v-if="rpsResult">
<text class="rps-clash-text">{{ rpsResult === 'win' ? '⚡' : rpsResult === 'lose' ? '💫' : '🤝' }}</text>
</view>
<view class="rps-hand rps-hand-right" :class="{ 'hand-shake': rpsShaking, 'hand-reveal': !rpsShaking && rpsRound > 0 }">
<text class="rps-hand-emoji">{{ rpsShaking ? '✊' : rpsAiHand }}</text>
</view>
</view>
<view v-if="rpsResult === 'lose'" class="result-banner result-drink">
<text class="result-emoji">🍺</text>
<text class="result-text">输了喝酒</text>
</view>
<view v-else-if="rpsResult === 'win'" class="result-banner result-safe">
<text class="result-emoji">🎉</text>
<text class="result-text">赢了对方喝</text>
</view>
<view v-else-if="rpsResult === 'draw'" class="result-banner result-draw">
<text class="result-emoji">🔄</text>
<text class="result-text">平局再来</text>
</view>
<view class="rps-picker" v-if="!rpsShaking">
<view
class="rps-choice"
v-for="c in rpsChoices"
:key="c.key"
:class="{ 'choice-active': rpsLastPick === c.key }"
@click="rpsPlay(c.key)"
>
<text class="rps-choice-emoji">{{ c.emoji }}</text>
<text class="rps-choice-name">{{ c.name }}</text>
</view>
</view>
<view class="rps-countdown" v-else>
<text class="rps-countdown-text">{{ rpsCountdown }}</text>
</view>
</view>
<view class="game-rule-card">
<text class="rule-title">📖 玩法</text>
<text class="rule-text">和酒神AI猜拳三局定胜负不存在的——输一局喝一杯赢一局对方喝平局一起抿一口</text>
</view>
</view>
<!-- ===== 骰子大话 ===== -->
<view v-if="currentGame === 'dice'" class="game-stage">
<view class="dice-arena">
<view class="dice-cup" :class="{ 'cup-shaking': diceShaking }" @click="diceRoll">
<view class="dice-pair">
<view class="die" :class="{ 'die-rolling': diceShaking }">
<view class="die-grid">
<view class="die-cell" v-for="p in 9" :key="p">
<view class="die-pip" v-if="dicePips(diceA).indexOf(p) > -1"></view>
</view>
</view>
</view>
<view class="die die-2" :class="{ 'die-rolling': diceShaking }">
<view class="die-grid">
<view class="die-cell" v-for="p in 9" :key="p">
<view class="die-pip" v-if="dicePips(diceB).indexOf(p) > -1"></view>
</view>
</view>
</view>
</view>
<text class="dice-sum text-num" v-if="!diceShaking && diceRound > 0">{{ diceA + diceB }}</text>
</view>
<view v-if="diceRule && !diceShaking" class="dice-rule-result" :class="diceRule.type === 'drink' ? 'result-drink' : diceRule.type === 'safe' ? 'result-safe' : 'result-fun'">
<text class="dice-rule-emoji">{{ diceRule.emoji }}</text>
<view class="dice-rule-text-wrap">
<text class="dice-rule-name">{{ diceRule.name }}</text>
<text class="dice-rule-desc">{{ diceRule.desc }}</text>
</view>
</view>
<view class="dice-history" v-if="diceHistory.length">
<view class="dice-history-item" v-for="(h, i) in diceHistory" :key="i">
<text class="dice-history-val text-num">{{ h.a }}+{{ h.b }}</text>
<text class="dice-history-rule">{{ h.rule }}</text>
</view>
</view>
<view class="btn-amber dice-roll-btn" @click="diceRoll" :class="{ 'btn-disabled': diceShaking }">
<text>{{ diceShaking ? '摇骰中…' : '🎲 摇骰子' }}</text>
</view>
</view>
<view class="game-rule-card">
<text class="rule-title">📖 玩法</text>
<text class="rule-text">轮流摇两颗骰子豹子全桌敬你一杯7点安全过关双数你喝一杯单数指定一人喝点数4 干杯</text>
</view>
</view>
<!-- ===== 命运转盘老虎机式 ===== -->
<view v-if="currentGame === 'wheel'" class="game-stage">
<view class="wheel-arena">
<view class="slot-machine">
<view class="slot-glow" :class="{ 'slot-glow-on': wheelSpinning }"></view>
<view class="slot-window">
<view class="slot-strip" :style="{ transform: 'translateY(' + slotOffset + 'rpx)', transition: slotTransition }">
<view
class="slot-item"
v-for="(s, i) in slotStrip"
:key="i"
:class="{ 'slot-item-active': !wheelSpinning && wheelResult && i === slotTarget }"
>
<view class="slot-item-emoji-box">
<text class="slot-item-emoji">{{ s.emoji }}</text>
</view>
<text class="slot-item-label">{{ s.label }}</text>
</view>
</view>
<view class="slot-mask-top"></view>
<view class="slot-mask-bottom"></view>
<view class="slot-indicator">
<text class="slot-ind-arrow"></text>
<text class="slot-ind-arrow slot-ind-arrow-r"></text>
</view>
</view>
</view>
<view v-if="wheelResult && !wheelSpinning" class="result-banner" :class="wheelResult.type === 'drink' ? 'result-drink' : wheelResult.type === 'safe' ? 'result-safe' : 'result-fun'">
<text class="result-emoji">{{ wheelResult.emoji }}</text>
<text class="result-text">{{ wheelResult.label }}愿赌服输</text>
</view>
<view class="btn-amber slot-spin-btn" :class="{ 'btn-disabled': wheelSpinning }" @click="wheelSpin">
<text>{{ wheelSpinning ? '命运转动中…' : '🎰 转一转' }}</text>
</view>
</view>
<view class="game-rule-card">
<text class="rule-title">📖 玩法</text>
<text class="rule-text">点击按钮转动命运滚轮停下来时中间高亮的那格就是你的命运不许耍赖</text>
</view>
</view>
<!-- 返回大厅浮动按钮 -->
<view v-if="currentGame !== 'hub'" class="back-hub-btn" @click="currentGame = 'hub'">
<text>🏠 返回大厅</text>
</view>
</view>
</template>
<script>
import themeMixin from '../../common/theme-mixin'
export default {
mixins: [themeMixin],
data() {
let navPaddingTop = '20px'
try {
const menuBtn = uni.getMenuButtonBoundingClientRect()
navPaddingTop = menuBtn.top + 'px'
} catch (e) {}
return {
navPaddingTop,
currentGame: 'hub',
gameList: [
{ id: 'bomb', emoji: '💣', name: '数字炸弹', desc: '猜中即喝,心跳加速', players: '2人+' },
{ id: 'rps', emoji: '✊', name: '猜拳大战', desc: '挑战酒神AI,输了干杯', players: '1人+' },
{ id: 'dice', emoji: '🎲', name: '骰子大话', desc: '豹子通杀,七点保命', players: '2人+' },
{ id: 'wheel', emoji: '🎡', name: '命运转盘', desc: '转到什么喝什么', players: '3人+' }
],
gameMeta: {
bomb: { name: '💣 数字炸弹' },
rps: { name: '✊ 猜拳大战' },
dice: { name: '🎲 骰子大话' },
wheel: { name: '🎡 命运转盘' }
},
// --- 数字炸弹 ---
bombTarget: 0,
bombMin: 1,
bombMax: 100,
bombInput: '',
bombGuesses: [],
bombBoom: false,
bombSafe: false,
// --- 猜拳 ---
rpsChoices: [
{ key: 'rock', emoji: '✊', name: '石头' },
{ key: 'scissors', emoji: '✌️', name: '剪刀' },
{ key: 'paper', emoji: '🖐', name: '布' }
],
rpsPlayerHand: '❔',
rpsAiHand: '❔',
rpsShaking: false,
rpsCountdown: 3,
rpsResult: '',
rpsWin: 0,
rpsLose: 0,
rpsRound: 0,
rpsLastPick: '',
rpsTimer: null,
// --- 骰子 ---
diceA: 1,
diceB: 1,
diceShaking: false,
diceRound: 0,
diceRule: null,
diceHistory: [],
// --- 转盘(老虎机式) ---
wheelSegments: [
{ label: '喝一杯', emoji: '🍺', type: 'drink' },
{ label: '真心话', emoji: '💬', type: 'fun' },
{ label: '免一杯', emoji: '🛡️', type: 'safe' },
{ label: '大冒险', emoji: '🔥', type: 'fun' },
{ label: '干一杯', emoji: '🥂', type: 'drink' },
{ label: '指定喝', emoji: '👉', type: 'drink' },
{ label: '表演节目', emoji: '🎤', type: 'fun' },
{ label: '双倍快乐', emoji: '🍻', type: 'drink' }
],
slotOffset: -120,
slotTransition: 'none',
slotTarget: -1,
wheelSpinning: false,
wheelResult: null
}
},
computed: {
bombFuseWidth() {
const range = this.bombMax - this.bombMin
const pct = Math.max(8, Math.min(100, range))
return pct + '%'
},
// 老虎机滚动条:重复4轮方便滚动动画
slotStrip() {
const arr = []
for (let r = 0; r < 4; r++) {
this.wheelSegments.forEach(s => arr.push(s))
}
return arr
}
},
onLoad() {
this.bombReset()
},
onUnload() {
if (this.rpsTimer) clearInterval(this.rpsTimer)
},
onShareAppMessage() {
return {
title: '酒桌小游戏 - 今晚谁喝?来 battle',
path: '/pages/games/games'
}
},
methods: {
goBack() {
if (this.currentGame !== 'hub') {
this.currentGame = 'hub'
} else {
uni.navigateBack({ delta: 1 })
}
},
enterGame(id) {
this.currentGame = id
if (id === 'bomb') this.bombReset()
if (id === 'rps') this.rpsResetRound()
if (id === 'dice') { this.diceRule = null; this.diceHistory = []; this.diceRound = 0 }
if (id === 'wheel') { this.wheelResult = null }
},
/* ========== 数字炸弹 ========== */
bombReset() {
this.bombTarget = Math.floor(Math.random() * 100) + 1
this.bombMin = 1
this.bombMax = 100
this.bombInput = ''
this.bombGuesses = []
this.bombBoom = false
this.bombSafe = false
},
bombGuess() {
const val = parseInt(this.bombInput)
if (isNaN(val) || val < this.bombMin || val > this.bombMax) {
uni.showToast({ title: `请输入 ${this.bombMin}~${this.bombMax} 的数字`, icon: 'none' })
return
}
if (val === this.bombTarget) {
this.bombBoom = true
uni.vibrateShort({ type: 'heavy' })
return
}
this.bombGuesses.push({ val, dir: val > this.bombTarget ? 'high' : 'low' })
if (val > this.bombTarget) {
this.bombMax = val - 1
} else {
this.bombMin = val + 1
}
this.bombInput = ''
uni.vibrateShort({ type: 'light' })
// 范围缩到1时安全过关
if (this.bombMin === this.bombMax && this.bombMin !== this.bombTarget) {
this.bombSafe = true
}
},
/* ========== 猜拳大战 ========== */
rpsResetRound() {
this.rpsPlayerHand = '❔'
this.rpsAiHand = '❔'
this.rpsResult = ''
this.rpsShaking = false
},
rpsPlay(key) {
if (this.rpsShaking) return
this.rpsLastPick = key
this.rpsResult = ''
this.rpsShaking = true
this.rpsCountdown = 3
this.rpsRound++
uni.vibrateShort({ type: 'light' })
this.rpsTimer = setInterval(() => {
this.rpsCountdown--
if (this.rpsCountdown <= 0) {
clearInterval(this.rpsTimer)
this.rpsTimer = null
this.rpsReveal(key)
}
}, 500)
},
rpsReveal(playerKey) {
const keys = ['rock', 'scissors', 'paper']
const emojis = { rock: '✊', scissors: '✌️', paper: '🖐' }
const aiKey = keys[Math.floor(Math.random() * 3)]
this.rpsPlayerHand = emojis[playerKey]
this.rpsAiHand = emojis[aiKey]
this.rpsShaking = false
if (playerKey === aiKey) {
this.rpsResult = 'draw'
} else if (
(playerKey === 'rock' && aiKey === 'scissors') ||
(playerKey === 'scissors' && aiKey === 'paper') ||
(playerKey === 'paper' && aiKey === 'rock')
) {
this.rpsResult = 'win'
this.rpsWin++
} else {
this.rpsResult = 'lose'
this.rpsLose++
uni.vibrateShort({ type: 'heavy' })
}
},
/* ========== 骰子大话 ========== */
// 骰子点数对应的 pip 位置(3x3 九宫格,1~9 从左到右、从上到下)
dicePips(val) {
const map = {
1: [5],
2: [1, 9],
3: [1, 5, 9],
4: [1, 3, 7, 9],
5: [1, 3, 5, 7, 9],
6: [1, 3, 4, 6, 7, 9]
}
return map[val] || []
},
diceRoll() {
if (this.diceShaking) return
this.diceShaking = true
this.diceRule = null
this.diceRound++
uni.vibrateShort({ type: 'medium' })
// 摇动期间快速随机跳面
const flicker = setInterval(() => {
this.diceA = Math.floor(Math.random() * 6) + 1
this.diceB = Math.floor(Math.random() * 6) + 1
}, 80)
setTimeout(() => {
clearInterval(flicker)
this.diceA = Math.floor(Math.random() * 6) + 1
this.diceB = Math.floor(Math.random() * 6) + 1
this.diceShaking = false
this.diceApplyRule()
uni.vibrateShort({ type: 'heavy' })
}, 1200)
},
diceApplyRule() {
const sum = this.diceA + this.diceB
let rule
if (this.diceA === this.diceB) {
rule = { emoji: '👑', name: '豹子!', desc: '天选之人!全桌敬你一杯', type: 'safe' }
} else if (sum === 7) {
rule = { emoji: '🛡️', name: '七点保命', desc: '幸运7!安全过关', type: 'safe' }
} else if (sum <= 4) {
rule = { emoji: '💀', name: '天崩开局', desc: '点数太小,直接干杯!', type: 'drink' }
} else if (sum % 2 === 0) {
rule = { emoji: '🍺', name: '双数喝一杯', desc: '愿赌服输,一杯下肚', type: 'drink' }
} else {
rule = { emoji: '👉', name: '单数点名', desc: '指定一人喝一杯,就你了!', type: 'fun' }
}
this.diceRule = rule
this.diceHistory.unshift({ a: this.diceA, b: this.diceB, rule: rule.name })
if (this.diceHistory.length > 5) this.diceHistory.pop()
},
/* ========== 命运转盘(老虎机式) ========== */
wheelSpin() {
if (this.wheelSpinning) return
this.wheelSpinning = true
this.wheelResult = null
this.slotTarget = -1
uni.vibrateShort({ type: 'medium' })
const count = this.wheelSegments.length
const segH = 120 // 每格高度 rpx
const targetIndex = Math.floor(Math.random() * count)
// 目标位置在第3轮(索引 2*count + targetIndex
const target = count * 2 + targetIndex
this.slotTarget = target
// 先瞬间回到起始位
this.slotTransition = 'none'
this.slotOffset = -segH // 显示第2个在中间
setTimeout(() => {
// 滚动到目标,中间项对齐窗口中心
this.slotTransition = 'transform 3s cubic-bezier(0.12, 0.75, 0.18, 1)'
this.slotOffset = -(target * segH) + segH // 让 target 居中(窗口显示3格,中间格偏移=1格)
}, 50)
setTimeout(() => {
this.wheelSpinning = false
this.wheelResult = this.wheelSegments[targetIndex]
uni.vibrateShort({ type: 'heavy' })
}, 3200)
}
}
}
</script>
<style lang="scss" scoped>
.games-page {
padding-bottom: calc(env(safe-area-inset-bottom) + 140rpx);
}
/* === 顶部导航 === */
.games-nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 $sp-lg;
height: 88rpx;
position: sticky;
top: 0;
z-index: 50;
background: $bg-base;
}
.nav-back {
width: 64rpx; height: 64rpx;
display: flex; align-items: center; justify-content: center;
border-radius: 50%;
background: $bg-elevated;
transition: transform $duration-fast $ease-out;
&:active { transform: scale(0.88); }
}
.nav-back-icon {
font-size: 44rpx;
color: $text-primary;
font-weight: $fw-bold;
margin-top: -6rpx;
}
.nav-title {
font-size: $fs-lg;
font-weight: $fw-bold;
color: $text-primary;
}
.nav-placeholder { width: 64rpx; }
/* === 游戏大厅 === */
.game-hub {
padding: $sp-xl $sp-lg 0;
}
.hub-header {
display: flex;
align-items: center;
gap: $sp-lg;
margin-bottom: $sp-xl;
}
.hub-deco {
font-size: 88rpx;
animation: hub-float 3s ease-in-out infinite;
}
@keyframes hub-float {
0%, 100% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-12rpx) rotate(8deg); }
}
.hub-title {
display: block;
font-size: $fs-2xl;
font-weight: $fw-black;
color: $text-primary;
letter-spacing: -1rpx;
}
.hub-sub {
display: block;
font-size: $fs-sm;
color: $text-tertiary;
margin-top: 8rpx;
}
.hub-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: $sp-md;
}
.hub-card {
position: relative;
padding: $sp-xl $sp-lg;
border-radius: $radius-xl;
background: $bg-card;
border: 1rpx solid var(--border-faint, rgba(255,255,255,0.08));
overflow: hidden;
animation: card-enter 0.5s $ease-out both;
transition: transform $duration-fast $ease-out, box-shadow $duration-fast $ease-out;
&:active {
transform: scale(0.95);
box-shadow: $shadow-amber;
}
}
@keyframes card-enter {
from { opacity: 0; transform: translateY(40rpx) scale(0.92); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.hub-card-glow {
position: absolute;
width: 180rpx; height: 180rpx;
top: -60rpx; right: -60rpx;
border-radius: 50%;
pointer-events: none;
}
.hub-card-bomb .hub-card-glow { background: radial-gradient(circle, rgba(255,107,107,0.14) 0%, transparent 70%); }
.hub-card-rps .hub-card-glow { background: radial-gradient(circle, rgba(232,168,56,0.14) 0%, transparent 70%); }
.hub-card-dice .hub-card-glow { background: radial-gradient(circle, rgba(94,198,160,0.14) 0%, transparent 70%); }
.hub-card-wheel .hub-card-glow { background: radial-gradient(circle, rgba(139,133,184,0.16) 0%, transparent 70%); }
.hub-card-emoji {
display: block;
font-size: 72rpx;
margin-bottom: $sp-md;
}
.hub-card-name {
display: block;
font-size: $fs-lg;
font-weight: $fw-bold;
color: $text-primary;
margin-bottom: 8rpx;
}
.hub-card-desc {
display: block;
font-size: $fs-xs;
color: $text-tertiary;
line-height: $lh-normal;
}
.hub-card-players {
display: inline-flex;
margin-top: $sp-md;
padding: 4rpx 16rpx;
border-radius: $radius-full;
background: $bg-elevated;
text {
font-size: 18rpx;
color: $text-secondary;
}
}
.hub-tip {
display: flex;
align-items: center;
justify-content: center;
gap: $sp-sm;
margin-top: $sp-2xl;
padding: $sp-md;
border-radius: $radius-lg;
background: rgba(232,168,56,0.06);
border: 1rpx dashed var(--border-dashed, rgba(255,255,255,0.12));
}
.hub-tip-icon { font-size: 28rpx; }
.hub-tip-text { font-size: $fs-xs; color: $text-tertiary; }
/* === 游戏舞台通用 === */
.game-stage {
padding: $sp-xl $sp-lg 0;
animation: stage-in 0.4s $ease-out both;
}
@keyframes stage-in {
from { opacity: 0; transform: translateY(30rpx); }
to { opacity: 1; transform: translateY(0); }
}
.game-rule-card {
margin-top: $sp-xl;
padding: $sp-lg;
border-radius: $radius-lg;
background: $bg-card;
border: 1rpx solid var(--border-faint, rgba(255,255,255,0.08));
}
.rule-title {
display: block;
font-size: 30rpx;
font-weight: $fw-bold;
color: $text-secondary;
margin-bottom: $sp-sm;
}
.rule-text {
font-size: 30rpx;
color: $text-tertiary;
line-height: $lh-loose;
}
.result-banner {
display: flex;
align-items: center;
justify-content: center;
gap: $sp-sm;
padding: $sp-lg;
border-radius: $radius-lg;
margin-top: $sp-lg;
animation: result-pop 0.4s $ease-bounce both;
&.result-drink {
background: linear-gradient(135deg, rgba(255,107,107,0.14), rgba(232,168,56,0.10));
border: 1rpx solid rgba(255,107,107,0.25);
}
&.result-safe {
background: linear-gradient(135deg, rgba(94,198,160,0.14), rgba(94,198,160,0.05));
border: 1rpx solid rgba(94,198,160,0.25);
}
&.result-fun {
background: linear-gradient(135deg, rgba(139,133,184,0.16), rgba(139,133,184,0.05));
border: 1rpx solid rgba(139,133,184,0.28);
}
&.result-draw {
background: $bg-elevated;
border: 1rpx solid var(--border-subtle, rgba(255,255,255,0.10));
}
}
@keyframes result-pop {
from { opacity: 0; transform: scale(0.7); }
to { opacity: 1; transform: scale(1); }
}
.result-emoji { font-size: 44rpx; }
.result-text { font-size: $fs-lg; font-weight: $fw-bold; color: $text-primary; }
.game-actions {
display: flex;
justify-content: center;
margin-top: $sp-xl;
}
.game-restart-btn {
min-width: 320rpx;
text-align: center;
}
.btn-amber {
display: flex;
align-items: center;
justify-content: center;
padding: $sp-md $sp-xl;
border-radius: $radius-full;
background: linear-gradient(135deg, var(--amber, #E8A838), var(--amber-deep, #C47F17));
box-shadow: var(--shadow-amber, 0 8rpx 32rpx rgba(232,168,56,0.25));
transition: transform $duration-fast $ease-out, opacity $duration-fast;
text { color: var(--text-on-amber, #0B0B14); font-size:40rpx; font-weight: $fw-bold; }
&:active { transform: scale(0.94); }
&.btn-disabled { opacity: 0.5; pointer-events: none; }
}
/* === 数字炸弹 === */
.bomb-arena { padding-top: $sp-md; }
.bomb-range-card {
text-align: center;
padding: $sp-2xl $sp-xl;
border-radius: $radius-xl;
background: $bg-card;
border: 1rpx solid var(--border-faint, rgba(255,255,255,0.08));
transition: all $duration-base;
&.bomb-boom {
border-color: rgba(255,107,107,0.4);
background: linear-gradient(160deg, rgba(255,107,107,0.10), $bg-card);
animation: boom-shake 0.5s ease-out;
}
}
@keyframes boom-shake {
0%, 100% { transform: translateX(0); }
20% { transform: translateX(-14rpx) rotate(-1deg); }
40% { transform: translateX(12rpx) rotate(1deg); }
60% { transform: translateX(-8rpx); }
80% { transform: translateX(6rpx); }
}
.bomb-emoji {
display: block;
font-size: 96rpx;
margin-bottom: $sp-lg;
animation: bomb-pulse 1.6s ease-in-out infinite;
}
@keyframes bomb-pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.12); }
}
.bomb-range-row {
display: flex;
align-items: center;
justify-content: center;
gap: $sp-lg;
}
.bomb-num {
font-size: $fs-hero;
font-weight: $fw-black;
color: $text-primary;
line-height: 1;
}
.bomb-fuse {
width: 160rpx;
height: 8rpx;
border-radius: 4rpx;
background: $bg-elevated;
overflow: hidden;
}
.bomb-fuse-line {
height: 100%;
border-radius: 4rpx;
background: linear-gradient(90deg, var(--mint, #5EC6A0), var(--amber, #E8A838), var(--coral, #FF6B6B));
transition: width 0.4s $ease-out;
}
.bomb-hint {
display: block;
margin-top: $sp-lg;
font-size: $fs-sm;
color: $text-tertiary;
}
.bomb-history {
display: flex;
flex-wrap: wrap;
gap: $sp-sm;
margin-top: $sp-lg;
justify-content: center;
}
.bomb-guess-tag {
padding: 8rpx 20rpx;
border-radius: $radius-full;
animation: tag-in 0.3s $ease-bounce both;
text { font-size: $fs-sm; font-weight: $fw-medium; }
&.tag-high {
background: rgba(255,107,107,0.12);
text { color: var(--coral, #FF6B6B); }
}
&.tag-low {
background: rgba(94,198,160,0.12);
text { color: var(--mint, #5EC6A0); }
}
}
@keyframes tag-in {
from { opacity: 0; transform: scale(0.5); }
to { opacity: 1; transform: scale(1); }
}
.bomb-input-area {
display: flex;
gap: $sp-md;
margin-top: $sp-xl;
}
.bomb-input {
flex: 1;
height: 96rpx;
padding: 0 $sp-xl;
border-radius: $radius-full;
background: var(--bg-input, rgba(255,255,255,0.08));
border: 1rpx solid var(--border-subtle, rgba(255,255,255,0.10));
color: $text-primary;
font-size: $fs-xl;
text-align: center;
font-weight: $fw-bold;
}
.bomb-go-btn {
min-width: 180rpx;
height: 96rpx;
}
/* === 猜拳大战 === */
.rps-arena { padding-top: $sp-md; }
.rps-score-row {
display: flex;
align-items: center;
justify-content: center;
gap: $sp-xl;
margin-bottom: $sp-xl;
}
.rps-score-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4rpx;
}
.rps-score-label { font-size: $fs-xs; color: $text-tertiary; }
.rps-score-num { font-size: $fs-2xl; font-weight: $fw-black; color: $amber; }
.rps-vs { font-size: $fs-sm; font-weight: $fw-black; color: $text-tertiary; letter-spacing: 2rpx; }
.rps-battle {
display: flex;
align-items: center;
justify-content: center;
gap: $sp-xl;
padding: $sp-xl 0;
}
.rps-hand {
width: 180rpx; height: 180rpx;
display: flex; align-items: center; justify-content: center;
border-radius: 50%;
background: $bg-card;
border: 2rpx solid var(--border-faint, rgba(255,255,255,0.08));
box-shadow: $shadow-md;
}
.rps-hand-emoji { font-size: 80rpx; }
.hand-shake {
animation: hand-shake 0.5s ease-in-out infinite;
}
@keyframes hand-shake {
0%, 100% { transform: translateY(0) rotate(0deg); }
25% { transform: translateY(-20rpx) rotate(-6deg); }
75% { transform: translateY(10rpx) rotate(4deg); }
}
.hand-reveal {
animation: hand-pop 0.35s $ease-bounce both;
}
@keyframes hand-pop {
from { transform: scale(0.6) rotate(-10deg); }
to { transform: scale(1) rotate(0deg); }
}
.rps-clash-text { font-size: 56rpx; animation: clash-flash 0.6s ease-out both; }
@keyframes clash-flash {
0% { opacity: 0; transform: scale(2.4); }
100% { opacity: 1; transform: scale(1); }
}
.rps-picker {
display: flex;
justify-content: center;
gap: $sp-lg;
margin-top: $sp-lg;
}
.rps-choice {
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-sm;
padding: $sp-lg $sp-xl;
border-radius: $radius-xl;
background: $bg-card;
border: 2rpx solid var(--border-faint, rgba(255,255,255,0.08));
transition: all $duration-fast $ease-out;
&:active { transform: scale(0.9); }
&.choice-active {
border-color: rgba(232,168,56,0.5);
background: rgba(232,168,56,0.08);
box-shadow: var(--shadow-amber, 0 8rpx 32rpx rgba(232,168,56,0.25));
}
}
.rps-choice-emoji { font-size: 56rpx; }
.rps-choice-name { font-size: $fs-xs; color: $text-secondary; font-weight: $fw-medium; }
.rps-countdown {
display: flex;
justify-content: center;
margin-top: $sp-lg;
}
.rps-countdown-text {
font-size: $fs-hero;
font-weight: $fw-black;
color: $amber;
animation: count-zoom 0.5s $ease-out both;
}
@keyframes count-zoom {
from { opacity: 0; transform: scale(1.8); }
to { opacity: 1; transform: scale(1); }
}
/* === 骰子大话 === */
.dice-arena {
display: flex;
flex-direction: column;
align-items: center;
padding-top: $sp-md;
}
.dice-cup {
position: relative;
width: 400rpx; height: 320rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: $sp-lg;
border-radius: $radius-xl;
background: $bg-card;
border: 1rpx solid var(--border-faint, rgba(255,255,255,0.08));
box-shadow: $shadow-md;
transition: box-shadow $duration-base;
&.cup-shaking {
animation: cup-shake 0.15s linear infinite;
box-shadow: var(--shadow-amber, 0 8rpx 32rpx rgba(232,168,56,0.25));
}
}
@keyframes cup-shake {
0%, 100% { transform: rotate(0deg) translateX(0); }
25% { transform: rotate(-2deg) translateX(-6rpx); }
75% { transform: rotate(2deg) translateX(6rpx); }
}
.dice-pair {
display: flex;
gap: $sp-lg;
}
.die {
width: 132rpx; height: 132rpx;
display: flex;
align-items: center; justify-content: center;
border-radius: $radius-md;
background: linear-gradient(160deg, #FBF7EF, #EAE2D3);
box-shadow: inset 0 4rpx 8rpx rgba(255,255,255,0.7), inset 0 -6rpx 12rpx rgba(0,0,0,0.12), $shadow-sm;
transition: transform $duration-base $ease-bounce;
&.die-rolling { animation: die-spin 0.3s linear infinite; }
}
@keyframes die-spin {
0% { transform: rotate(0deg) scale(1); }
50% { transform: rotate(180deg) scale(0.85); }
100% { transform: rotate(360deg) scale(1); }
}
.die-grid {
display: flex;
flex-wrap: wrap;
width: 96rpx;
height: 96rpx;
}
.die-cell {
width: 32rpx;
height: 32rpx;
display: flex;
align-items: center;
justify-content: center;
}
.die-pip {
width: 18rpx;
height: 18rpx;
border-radius: 50%;
background: #241B10;
box-shadow: inset 0 2rpx 3rpx rgba(0,0,0,0.5);
}
.dice-sum {
font-size: $fs-2xl;
font-weight: $fw-black;
color: $amber;
}
.dice-rule-result {
display: flex;
align-items: center;
gap: $sp-md;
width: 100%;
margin-top: $sp-lg;
padding: $sp-lg;
border-radius: $radius-lg;
animation: result-pop 0.4s $ease-bounce both;
&.result-drink {
background: linear-gradient(135deg, rgba(255,107,107,0.12), rgba(255,107,107,0.04));
border: 1rpx solid rgba(255,107,107,0.22);
}
&.result-safe {
background: linear-gradient(135deg, rgba(94,198,160,0.12), rgba(94,198,160,0.04));
border: 1rpx solid rgba(94,198,160,0.22);
}
&.result-fun {
background: linear-gradient(135deg, rgba(139,133,184,0.14), rgba(139,133,184,0.04));
border: 1rpx solid rgba(139,133,184,0.24);
}
}
.dice-rule-emoji { font-size: 48rpx; }
.dice-rule-name { display: block; font-size: $fs-base; font-weight: $fw-bold; color: $text-primary; }
.dice-rule-desc { display: block; font-size: $fs-xs; color: $text-tertiary; margin-top: 4rpx; }
.dice-history {
display: flex;
gap: $sp-sm;
margin-top: $sp-lg;
flex-wrap: wrap;
justify-content: center;
}
.dice-history-item {
display: flex;
align-items: center;
gap: 8rpx;
padding: 6rpx 16rpx;
border-radius: $radius-full;
background: $bg-elevated;
}
.dice-history-val { font-size: $fs-xs; font-weight: $fw-bold; color: $text-secondary; }
.dice-history-rule { font-size: 18rpx; color: $text-tertiary; }
.dice-roll-btn {
margin-top: $sp-xl;
min-width: 360rpx;
height: 96rpx;
}
/* === 命运转盘(老虎机) === */
.wheel-arena {
display: flex;
flex-direction: column;
align-items: center;
padding-top: $sp-md;
}
.slot-machine {
position: relative;
width: 100%;
display: flex;
justify-content: center;
}
.slot-glow {
position: absolute;
top: -30rpx; left: 50%;
transform: translateX(-50%);
width: 480rpx; height: 480rpx;
border-radius: 50%;
background: radial-gradient(circle, rgba(232,168,56,0.10) 0%, transparent 70%);
pointer-events: none;
opacity: 0.5;
transition: opacity $duration-base;
&.slot-glow-on {
opacity: 1;
animation: glow-pulse 0.8s ease-in-out infinite;
}
}
@keyframes glow-pulse {
0%, 100% { transform: translateX(-50%) scale(1); }
50% { transform: translateX(-50%) scale(1.08); }
}
.slot-window {
position: relative;
width: 520rpx;
height: 360rpx;
border-radius: $radius-xl;
background: $bg-card;
border: 2rpx solid var(--border-subtle, rgba(255,255,255,0.10));
box-shadow: $shadow-lg;
overflow: hidden;
}
.slot-strip {
position: absolute;
left: 0; right: 0;
top: 0;
width: 100%;
will-change: transform;
}
.slot-item {
display: flex;
align-items: center;
justify-content: center;
gap: $sp-sm;
height: 120rpx;
border-bottom: 1rpx solid var(--border-micro, rgba(255,255,255,0.05));
transition: background $duration-base;
&.slot-item-active {
background: rgba(232,168,56,0.10);
}
}
.slot-item-emoji-box {
width: 52rpx;
height: 52rpx;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
flex-shrink: 0;
}
.slot-item-emoji { font-size: 40rpx; line-height: 1; }
.slot-item-label { font-size: $fs-base; font-weight: $fw-bold; color: $text-primary; letter-spacing: 2rpx; }
.slot-mask-top, .slot-mask-bottom {
position: absolute;
left: 0; right: 0;
height: 120rpx;
pointer-events: none;
z-index: 5;
}
.slot-mask-top {
top: 0;
background: linear-gradient(to bottom, var(--bg-card, #1A1A28) 20%, transparent);
opacity: 0.85;
}
.slot-mask-bottom {
bottom: 0;
background: linear-gradient(to top, var(--bg-card, #1A1A28) 20%, transparent);
opacity: 0.85;
}
.slot-indicator {
position: absolute;
top: 50%;
left: 0; right: 0;
transform: translateY(-50%);
display: flex;
justify-content: space-between;
padding: 0 $sp-sm;
pointer-events: none;
z-index: 10;
}
.slot-ind-arrow {
font-size: 28rpx;
color: var(--amber, #E8A838);
text-shadow: 0 0 12rpx rgba(232,168,56,0.6);
}
.slot-spin-btn {
margin-top: $sp-xl;
min-width: 360rpx;
height: 96rpx;
}
/* === 返回大厅按钮 === */
.back-hub-btn {
position: fixed;
bottom: calc(env(safe-area-inset-bottom) + 48rpx);
left: 50%;
transform: translateX(-50%);
padding: $sp-md $sp-2xl;
border-radius: $radius-full;
background: $bg-elevated;
border: 1rpx solid var(--border-subtle, rgba(255,255,255,0.10));
box-shadow: $shadow-md;
z-index: 60;
transition: transform $duration-fast $ease-out;
&:active { transform: translateX(-50%) scale(0.92); }
text { font-size: $fs-sm; font-weight: $fw-medium; color: $text-secondary; }
}
</style>