更新功能、刷新不立即解散房间、交互动画优化
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
// emit('score', n)、emit('end', {score}),带关卡的游戏另有 emit('progress', 解锁到的关卡)
|
||||
// 组队游戏(meta.coop,如饥荒):开始面板变状态机——模式选择/更衣室/联机房间
|
||||
import { ref, computed, shallowRef, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router'
|
||||
import http, { toast } from '../api/http'
|
||||
import { useUserStore } from '../stores/user'
|
||||
import { useStarveCoopStore } from '../stores/starveCoop'
|
||||
@@ -54,10 +54,22 @@ const buyingSkin = ref('') // 正在购买的皮肤(防连点)
|
||||
const joinCode = ref('') // 加入房间的邀请码输入
|
||||
const coopBusy = ref(false) // 连接/建房中
|
||||
const coopPlaying = ref(false) // 当前对局是否联机局(结束时不删单人存档)
|
||||
// ---- 饥荒页内顶部信息条:悬浮小条,自动隐藏,鼠标靠近顶部展示 ----
|
||||
const floatHead = computed(() => code === 'starve')
|
||||
const headShow = ref(false)
|
||||
function onHeadMouseMove(e) {
|
||||
if (!floatHead.value) return
|
||||
headShow.value = e.clientY <= 130
|
||||
}
|
||||
const playHeadClasses = computed(() => ({
|
||||
'play-head-float': floatHead.value,
|
||||
'play-head-hidden': floatHead.value && !headShow.value,
|
||||
}))
|
||||
|
||||
// ---- Mod 装载(注册表标记 mods 的游戏,如饥荒;勾选记本地) ----
|
||||
const modsAvail = meta?.mods ? modMeta() : []
|
||||
const selMods = ref([])
|
||||
const showModModal = ref(false)
|
||||
try {
|
||||
selMods.value = JSON.parse(localStorage.getItem('mods_' + code) || '[]').filter((id) => modsAvail.some((m) => m.id === id))
|
||||
} catch { selMods.value = [] }
|
||||
@@ -67,6 +79,10 @@ function toggleMod(id) {
|
||||
else selMods.value.push(id)
|
||||
localStorage.setItem('mods_' + code, JSON.stringify(selMods.value))
|
||||
}
|
||||
function clearMods() {
|
||||
selMods.value = []
|
||||
localStorage.setItem('mods_' + code, JSON.stringify([]))
|
||||
}
|
||||
// 勾选组合的得分系数(难度 mod 相乘,夹在 0.5~1.5)
|
||||
const modFactor = computed(() => {
|
||||
let f = 1
|
||||
@@ -356,6 +372,11 @@ function startCoopGame() {
|
||||
}
|
||||
// 房间座位视图数据(补上空位)
|
||||
const roomSeats = computed(() => coopStore.roomState?.seats || [])
|
||||
const overlayClasses = computed(() => ({
|
||||
scrollable: levelsMeta || (coopable && ['skins', 'solo', 'room'].includes(coopStage)),
|
||||
room: coopable && coopStage === 'room' && coopStore.inRoom,
|
||||
'room-entry': coopable && coopStage === 'room' && !coopStore.inRoom,
|
||||
}))
|
||||
const allReady = computed(() => {
|
||||
const occ = roomSeats.value.filter((s) => s.occupied)
|
||||
return occ.length >= 1 && occ.every((s) => s.ready)
|
||||
@@ -456,17 +477,58 @@ function again() {
|
||||
// 从全局邀请弹窗跳转而来:自动连 ws 并加入房间
|
||||
async function tryPendingJoin() {
|
||||
if (!coopable) return
|
||||
const pendingCode = sessionStorage.getItem('pending_join_code')
|
||||
let pendingCode = sessionStorage.getItem('pending_join_code')
|
||||
if (pendingCode) {
|
||||
sessionStorage.removeItem('pending_join_code')
|
||||
sessionStorage.removeItem('pending_join_game')
|
||||
} else {
|
||||
pendingCode = localStorage.getItem('starve_last_room_code')
|
||||
if (!pendingCode) return
|
||||
}
|
||||
if (!pendingCode) return
|
||||
sessionStorage.removeItem('pending_join_code')
|
||||
sessionStorage.removeItem('pending_join_game')
|
||||
coopBusy.value = true
|
||||
coopStage.value = 'room'
|
||||
try {
|
||||
if (coopStore.inRoom) coopStore.leaveRoom()
|
||||
await coopStore.connect()
|
||||
coopStage.value = 'room'
|
||||
joinCode.value = pendingCode
|
||||
coopStore.joinRoom(pendingCode)
|
||||
// 等待服务端自动重挂(30秒断线保护);重挂成功则无需再发 join_room
|
||||
await new Promise((resolve) => {
|
||||
if (coopStore.inRoom) return resolve(true)
|
||||
const t = setTimeout(() => {
|
||||
stopReattach?.()
|
||||
resolve(false)
|
||||
}, 800)
|
||||
const stopReattach = watch(() => coopStore.inRoom, (v) => {
|
||||
if (v) {
|
||||
clearTimeout(t)
|
||||
stopReattach?.()
|
||||
resolve(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
if (!coopStore.inRoom) coopStore.joinRoom(pendingCode)
|
||||
const ok = await new Promise((resolve) => {
|
||||
if (coopStore.inRoom) return resolve(true)
|
||||
const t = setTimeout(() => {
|
||||
stop?.()
|
||||
resolve(false)
|
||||
}, 3000)
|
||||
const stop = watch(() => coopStore.inRoom, (v) => {
|
||||
if (v) {
|
||||
clearTimeout(t)
|
||||
stop?.()
|
||||
resolve(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
if (!ok) {
|
||||
toast('自动回房失败,请手动创建或加入房间', 'info')
|
||||
try { localStorage.removeItem('starve_last_room_code') } catch (e) {}
|
||||
}
|
||||
} catch {
|
||||
toast('联机服务连接失败,请稍后再试')
|
||||
} finally {
|
||||
@@ -474,7 +536,20 @@ async function tryPendingJoin() {
|
||||
}
|
||||
}
|
||||
|
||||
// 正常导航离开(不是刷新)时清掉“自动回房”记忆;刷新不会触发路由离开守卫,因此记忆会保留
|
||||
onBeforeRouteLeave(() => {
|
||||
try { localStorage.removeItem('starve_last_room_code') } catch (e) {}
|
||||
})
|
||||
|
||||
// 兜底:房间状态里只要有邀请码,就写入自动回房记忆
|
||||
watch(() => coopStore.roomState?.code, (code) => {
|
||||
if (code) {
|
||||
try { localStorage.setItem('starve_last_room_code', code) } catch (e) {}
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('mousemove', onHeadMouseMove)
|
||||
// 宽屏游戏(如饥荒):给 body 打标解除全局 1200px 限宽
|
||||
if (meta?.wide) document.body.classList.add('wide-page')
|
||||
load()
|
||||
@@ -486,6 +561,7 @@ onMounted(() => {
|
||||
onBeforeUnmount(() => {
|
||||
document.body.classList.remove('wide-page')
|
||||
window.removeEventListener('nlg:pending-join', tryPendingJoin)
|
||||
window.removeEventListener('mousemove', onHeadMouseMove)
|
||||
clearInterval(inviteTickTimer)
|
||||
gameRef.value?.stop()
|
||||
// 离开游玩页断开联机连接(房间自动退出)
|
||||
@@ -496,7 +572,7 @@ onBeforeUnmount(() => {
|
||||
<template>
|
||||
<div v-if="detail" class="play-wrap" :class="{ 'play-fill': meta?.wide }">
|
||||
<!-- 顶部信息条 -->
|
||||
<div class="play-head panel">
|
||||
<div class="play-head panel" :class="playHeadClasses">
|
||||
<button class="btn btn-ghost btn-sm" @click="router.back()">← 返回</button>
|
||||
<GameIcon class="g-icon" :code="code" :icon="detail.game.icon" :size="34" />
|
||||
<div class="g-info">
|
||||
@@ -513,7 +589,7 @@ onBeforeUnmount(() => {
|
||||
<div class="stage panel">
|
||||
<component :is="gameComp" v-if="gameComp" ref="gameRef" @score="onScore" @end="onEnd" @progress="onProgress" @save="onSave" />
|
||||
<!-- 开始遮罩:带关卡的游戏显示关卡选择;组队游戏显示模式选择状态机;其余显示开始按钮 -->
|
||||
<div v-if="phase === 'ready'" class="overlay" :class="{ scrollable: levelsMeta || (coopable && ['skins', 'solo', 'room'].includes(coopStage)) }">
|
||||
<div v-if="phase === 'ready'" class="overlay" :class="overlayClasses">
|
||||
<!-- ============ 组队游戏(如饥荒):模式选择 / 单人 / 更衣室 / 房间 ============ -->
|
||||
<template v-if="coopable">
|
||||
<!-- 1. 模式选择 -->
|
||||
@@ -566,6 +642,11 @@ onBeforeUnmount(() => {
|
||||
<span v-if="m.factor !== 1" class="mp-factor" :class="{ up: m.factor > 1 }">×{{ m.factor }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<button v-if="modsAvail.length" class="btn btn-ghost mod-trigger" @click="showModModal = true">
|
||||
🧩 Mod 装载
|
||||
<span v-if="selMods.length" class="mod-trigger-count">{{ selMods.length }} 个已选</span>
|
||||
<span v-if="modFactor !== 1" class="mod-trigger-factor" :class="{ up: modFactor > 1 }">得分 ×{{ modFactor }}</span>
|
||||
</button>
|
||||
<template v-if="saveable && saveInfo.exists">
|
||||
<div class="save-card">
|
||||
<b>发现存档</b>
|
||||
@@ -654,6 +735,11 @@ onBeforeUnmount(() => {
|
||||
<span v-if="m.factor !== 1" class="mp-factor" :class="{ up: m.factor > 1 }">×{{ m.factor }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<button v-if="coopStore.isHost && modsAvail.length" class="btn btn-ghost mod-trigger" @click="showModModal = true">
|
||||
🧩 本局 Mod(房主设定)
|
||||
<span v-if="selMods.length" class="mod-trigger-count">{{ selMods.length }} 个已选</span>
|
||||
<span v-if="modFactor !== 1" class="mod-trigger-factor" :class="{ up: modFactor > 1 }">得分 ×{{ modFactor }}</span>
|
||||
</button>
|
||||
<!-- 房主邀请好友:在线好友一键发弹窗邀请,离线好友发私聊兜底 -->
|
||||
<div v-if="coopStore.isHost && friendList.length" class="friend-invite">
|
||||
<div class="fi-head">邀请好友({{ friendList.length }})· 同一人 1 分钟内不可重复</div>
|
||||
@@ -775,7 +861,40 @@ onBeforeUnmount(() => {
|
||||
<span v-else class="prop-buy">💰{{ p.price }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mod 选择模态框:卡片多选 -->
|
||||
<div v-if="showModModal" class="modal-mask" @click.self="showModModal = false">
|
||||
<div class="mod-modal panel">
|
||||
<button class="bm-close" @click="showModModal = false">✕</button>
|
||||
<h3>选择 Mod</h3>
|
||||
<p class="text-dim" style="font-size: 12px; margin-top: -4px">
|
||||
新开局时生效 · 已选 {{ selMods.length }} 个{{ modFactor !== 1 ? ` · 得分 ×${modFactor}` : '' }}
|
||||
</p>
|
||||
<div class="mod-card-grid">
|
||||
<div
|
||||
v-for="m in modsAvail"
|
||||
:key="m.id"
|
||||
class="mod-card"
|
||||
:class="{ on: selMods.includes(m.id) }"
|
||||
@click="toggleMod(m.id)"
|
||||
>
|
||||
<div class="mod-card-check">{{ selMods.includes(m.id) ? '✓' : '' }}</div>
|
||||
<div class="mod-card-name">{{ m.name }}</div>
|
||||
<div class="mod-card-desc">{{ m.desc }}</div>
|
||||
<div class="mod-card-foot">
|
||||
<span class="mod-card-tag">{{ m.tag }}</span>
|
||||
<span v-if="m.factor !== 1" class="mod-card-factor" :class="{ up: m.factor > 1 }">得分 ×{{ m.factor }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ov-btns">
|
||||
<button class="btn btn-ghost" @click="clearMods">清空</button>
|
||||
<button class="btn" @click="showModModal = false">确认</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 未拥有提示 -->
|
||||
<div v-else-if="phase === 'locked'" class="locked panel">
|
||||
<div style="font-size: 50px">🔒</div>
|
||||
@@ -1324,4 +1443,218 @@ onBeforeUnmount(() => {
|
||||
font-size: 12px;
|
||||
color: var(--text-dim, #9a9ac4);
|
||||
}
|
||||
|
||||
/* ---- Mod 改为卡片多选模态框:隐藏旧的平铺列表,触发按钮 + 卡片模态框 ---- */
|
||||
.mod-pick {
|
||||
display: none;
|
||||
}
|
||||
.mod-trigger {
|
||||
width: min(420px, 92%);
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.mod-trigger-count {
|
||||
font-size: 12px;
|
||||
color: var(--primary-2);
|
||||
font-weight: 700;
|
||||
}
|
||||
.mod-trigger-factor {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #67d18f;
|
||||
}
|
||||
.mod-trigger-factor.up {
|
||||
color: #e0a63c;
|
||||
}
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(3px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 200;
|
||||
padding: 20px;
|
||||
}
|
||||
.mod-modal {
|
||||
position: relative;
|
||||
width: min(680px, 100%);
|
||||
max-height: 84vh;
|
||||
overflow: auto;
|
||||
padding: 20px 18px 18px;
|
||||
}
|
||||
.bm-close {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 12px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--text-dim);
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.bm-close:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
.mod-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 10px;
|
||||
margin: 14px 0;
|
||||
}
|
||||
.mod-card {
|
||||
position: relative;
|
||||
background: var(--bg-card);
|
||||
border: 1.5px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.mod-card:hover {
|
||||
border-color: var(--primary);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.mod-card.on {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 12px color-mix(in srgb, var(--accent) 35%, transparent);
|
||||
background: color-mix(in srgb, var(--accent) 10%, var(--bg-card));
|
||||
}
|
||||
.mod-card-check {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
}
|
||||
.mod-card.on .mod-card-check {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.mod-card-name {
|
||||
font-weight: 800;
|
||||
font-size: 14px;
|
||||
padding-right: 26px;
|
||||
}
|
||||
.mod-card-desc {
|
||||
font-size: 12px;
|
||||
color: var(--text-dim);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.mod-card-foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: auto;
|
||||
}
|
||||
.mod-card-tag {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.mod-card-factor {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #67d18f;
|
||||
margin-left: auto;
|
||||
}
|
||||
.mod-card-factor.up {
|
||||
color: #e0a63c;
|
||||
}
|
||||
/* ---- 邀请好友移到右侧悬浮栏,不挤占中间 ---- */
|
||||
.friend-invite {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 16px;
|
||||
bottom: 16px;
|
||||
width: 260px;
|
||||
max-height: none;
|
||||
height: auto;
|
||||
z-index: 6;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.friend-invite {
|
||||
position: static;
|
||||
width: min(440px, 96%);
|
||||
height: auto;
|
||||
max-height: 220px;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
.overlay.room {
|
||||
padding-right: 280px;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.overlay.room {
|
||||
padding-right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 饥荒页内顶部信息条:悬浮小条,自动隐藏,鼠标靠近顶部展示 ---- */
|
||||
.play-head.play-head-float {
|
||||
position: fixed;
|
||||
top: 40px;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
z-index: 149;
|
||||
transition: transform 0.25s ease;
|
||||
margin: 0;
|
||||
}
|
||||
.play-head.play-head-float.play-head-hidden {
|
||||
transform: translateY(calc(-100% - 40px));
|
||||
}
|
||||
/* ---- 创建/加入房间入口:卡片式排版 ---- */
|
||||
.overlay.room-entry .room-entry-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
padding: 16px 18px;
|
||||
}
|
||||
.overlay.room-entry .ov-btns {
|
||||
width: min(360px, 92%);
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
padding: 14px 16px;
|
||||
justify-content: center;
|
||||
}
|
||||
.overlay.room-entry .ov-btns .btn-lg {
|
||||
width: 100%;
|
||||
}
|
||||
.overlay.room-entry .join-row {
|
||||
width: min(360px, 92%);
|
||||
justify-content: center;
|
||||
}
|
||||
.overlay.room-entry .join-row {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 12px 14px;
|
||||
gap: 10px;
|
||||
}
|
||||
.overlay.room-entry .join-input {
|
||||
width: 180px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
/* 选择组队(创建/加入)和房间面板:垂直居中,不要贴顶 */
|
||||
.overlay.room,
|
||||
.overlay.room-entry {
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user