新建项目

This commit is contained in:
2026-07-29 10:50:33 +08:00
parent bfd3841cbd
commit 4f69b859a8
49 changed files with 5565 additions and 0 deletions

116
pages/lobby/lobby.js Normal file
View File

@@ -0,0 +1,116 @@
const service = require("../../services/game-service");
const { GAME_MODES } = require("../../utils/constants");
Page({
data: {
code: "",
room: null,
modeName: "",
isHost: false,
showStartButton: false,
meReady: false,
canStart: false,
startHint: "",
errorMessage: "",
starting: false
},
onLoad(options) {
this.setData({ code: options.code || "" });
this.loadRoom(true);
},
onShow() {
if (this.data.code) this.loadRoom(false);
},
onPullDownRefresh() {
this.loadRoom(false);
wx.stopPullDownRefresh();
},
loadRoom(addBots) {
let room = service.getRoom(this.data.code);
if (!room) {
this.setData({ room: null, errorMessage: "房间不存在或本地数据已被清理" });
return;
}
if (addBots && room.players.length < Math.min(6, room.settings.maxPlayers)) {
room = service.addDemoPlayers(
room.code,
Math.min(6, room.settings.maxPlayers)
);
}
if (room.status !== "waiting") {
const target = room.status === "finished" ? "result" : "game";
wx.redirectTo({
url: `/pages/${target}/${target}?code=${room.code}`
});
return;
}
const me = room.players.find((player) => player.id === service.USER_ID);
const isHost = room.hostId === service.USER_ID;
const canOperate = isHost || room.code === "731204";
const enoughPlayers = room.players.length >= 4;
const seekersValid = room.settings.seekerCount < room.players.length;
const everyoneReady = room.players.every((player) => player.ready);
let startHint = "分配身份并开始";
if (!enoughPlayers) startHint = "至少需要 4 人";
else if (!seekersValid) startHint = "寻找者人数过多";
else if (!everyoneReady) startHint = "等待所有玩家准备";
this.setData({
room,
errorMessage: "",
modeName: GAME_MODES[room.settings.mode].name,
isHost,
showStartButton: canOperate,
meReady: me ? me.ready : false,
canStart: canOperate && enoughPlayers && seekersValid && everyoneReady,
startHint
});
},
toggleReady() {
try {
service.toggleReady(this.data.code);
wx.vibrateShort({ type: "light" });
this.loadRoom(false);
} catch (error) {
wx.showToast({ title: error.message, icon: "none" });
}
},
startGame() {
if (this.data.starting) return;
this.setData({ starting: true });
try {
service.startGame(this.data.code);
wx.redirectTo({
url: `/pages/game/game?code=${this.data.code}`
});
} catch (error) {
this.setData({ starting: false });
wx.showToast({ title: error.message, icon: "none" });
}
},
copyCode() {
wx.setClipboardData({
data: this.data.code,
success: () => wx.showToast({ title: "房间码已复制", icon: "success" })
});
},
backHome() {
service.clearCurrentRoom();
wx.reLaunch({ url: "/pages/home/home" });
},
onShareAppMessage() {
return {
title: `来加入「${this.data.room.name}`,
path: `/pages/join/join?code=${this.data.code}`
};
}
});

4
pages/lobby/lobby.json Normal file
View File

@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "游戏候场",
"enablePullDownRefresh": true
}

89
pages/lobby/lobby.wxml Normal file
View File

@@ -0,0 +1,89 @@
<view class="page lobby-page" wx:if="{{room}}">
<view class="room-head">
<view class="row between">
<view class="pill">{{modeName}}</view>
<view class="room-status"><view class="pulse"></view>等待加入</view>
</view>
<view class="room-name">{{room.name}}</view>
<view class="code-wrap" bindtap="copyCode">
<view>
<view class="code-label">房间码 · 点击复制</view>
<view class="room-code">{{room.code}}</view>
</view>
<button class="share-mini" open-type="share">邀请好友</button>
</view>
</view>
<view class="rule-strip">
<view>
<text class="rule-number">{{room.settings.durationMinutes}}</text>
<text class="rule-unit">分钟</text>
<view class="rule-name">游戏时间</view>
</view>
<view>
<text class="rule-number">{{room.settings.radiusMeters}}</text>
<text class="rule-unit">米</text>
<view class="rule-name">活动半径</view>
</view>
<view>
<text class="rule-number">{{room.settings.seekerCount}}</text>
<text class="rule-unit">人</text>
<view class="rule-name">寻找者</view>
</view>
</view>
<view class="row between player-heading">
<view class="section-label">玩家 {{room.players.length}} / {{room.settings.maxPlayers}}</view>
<view class="muted demo-note">已加入模拟队友</view>
</view>
<view class="player-list">
<view wx:for="{{room.players}}" wx:key="id" class="player-row">
<view class="player-avatar {{item.isHost ? 'host' : ''}}">{{item.avatar}}</view>
<view class="player-main">
<view class="player-name">
{{item.name}}
<text wx:if="{{item.isHost}}" class="host-tag">房主</text>
<text wx:if="{{item.isBot}}" class="bot-tag">模拟</text>
</view>
<view class="player-state">{{item.ready ? '已准备好' : '正在确认装备'}}</view>
</view>
<view class="ready-dot {{item.ready ? 'ready' : ''}}">
{{item.ready ? '✓' : '…'}}
</view>
</view>
</view>
<view class="tip-card">
<view class="tip-icon">!</view>
<view>
<view class="tip-title">开局前安全确认</view>
<view class="tip-copy">确保所有人定位可用、电量充足,并约定紧急集合点。抓捕只用口令确认,禁止身体接触。</view>
</view>
</view>
<view class="button-stack">
<button
wx:if="{{showStartButton}}"
class="primary-button"
loading="{{starting}}"
disabled="{{starting || !canStart}}"
bindtap="startGame"
>
{{startHint}}
</button>
<button
wx:else
class="{{meReady ? 'ghost-button' : 'secondary-button'}}"
bindtap="toggleReady"
>
{{meReady ? '取消准备' : '我准备好了'}}
</button>
</view>
</view>
<view class="state-page" wx:elif="{{errorMessage}}">
<view class="state-icon">!</view>
<view class="state-title">无法进入候场</view>
<view class="state-copy">{{errorMessage}}</view>
<button class="primary-button state-action" bindtap="backHome">返回首页</button>
</view>

221
pages/lobby/lobby.wxss Normal file
View File

@@ -0,0 +1,221 @@
.room-head {
padding: 30rpx;
background: #183f31;
border-radius: 34rpx;
color: #fff;
}
.room-status {
display: flex;
align-items: center;
gap: 10rpx;
color: rgba(255, 255, 255, 0.72);
font-size: 23rpx;
}
.pulse {
width: 14rpx;
height: 14rpx;
background: #d9ef72;
border-radius: 50%;
box-shadow: 0 0 0 8rpx rgba(217, 239, 114, 0.12);
}
.room-name {
margin-top: 28rpx;
font-size: 43rpx;
font-weight: 900;
overflow-wrap: anywhere;
}
.code-wrap {
display: flex;
align-items: flex-end;
justify-content: space-between;
margin-top: 34rpx;
padding-top: 26rpx;
border-top: 2rpx solid rgba(255, 255, 255, 0.12);
}
.code-label {
color: rgba(255, 255, 255, 0.6);
font-size: 21rpx;
}
.room-code {
margin-top: 5rpx;
color: #d9ef72;
font-size: 50rpx;
font-weight: 900;
letter-spacing: 7rpx;
}
.share-mini {
width: 180rpx;
height: 70rpx;
margin: 0;
background: #d9ef72;
color: #17231d;
font-size: 24rpx;
}
.rule-strip {
display: flex;
justify-content: space-around;
margin-top: 22rpx;
padding: 26rpx 10rpx;
background: #fffdf7;
border-radius: 26rpx;
text-align: center;
}
.rule-number {
font-size: 36rpx;
font-weight: 900;
}
.rule-unit {
margin-left: 4rpx;
color: #68726c;
font-size: 20rpx;
}
.rule-name {
margin-top: 6rpx;
color: #7c8580;
font-size: 21rpx;
}
.player-heading .section-label {
margin-bottom: 12rpx;
}
.demo-note {
margin-top: 26rpx;
font-size: 20rpx;
}
.player-list {
overflow: hidden;
background: #fffdf7;
border-radius: 28rpx;
}
.player-row {
display: flex;
align-items: center;
gap: 20rpx;
padding: 22rpx 26rpx;
border-bottom: 2rpx solid rgba(23, 35, 29, 0.07);
}
.player-row:last-child {
border-bottom: none;
}
.player-avatar {
display: flex;
align-items: center;
justify-content: center;
width: 66rpx;
height: 66rpx;
background: #e7eee8;
border-radius: 22rpx;
font-size: 25rpx;
font-weight: 800;
}
.player-avatar.host {
background: #d9ef72;
}
.player-main {
flex: 1;
min-width: 0;
}
.player-name {
overflow: hidden;
font-size: 27rpx;
font-weight: 800;
text-overflow: ellipsis;
white-space: nowrap;
}
.host-tag,
.bot-tag {
margin-left: 10rpx;
padding: 3rpx 10rpx;
background: #e7eee8;
border-radius: 99rpx;
color: #1d6b4f;
font-size: 18rpx;
}
.bot-tag {
background: #fff0df;
color: #9c531e;
}
.player-state {
margin-top: 6rpx;
color: #89918c;
font-size: 21rpx;
}
.ready-dot {
display: flex;
align-items: center;
justify-content: center;
width: 42rpx;
height: 42rpx;
background: #eef0ed;
border-radius: 50%;
color: #8b938e;
font-size: 22rpx;
}
.ready-dot.ready {
background: #1d6b4f;
color: #fff;
}
.tip-card {
display: flex;
gap: 20rpx;
margin-top: 26rpx;
padding: 24rpx;
background: #fff0df;
border-radius: 24rpx;
}
.tip-icon {
display: flex;
flex: 0 0 auto;
align-items: center;
justify-content: center;
width: 46rpx;
height: 46rpx;
background: #ef8f4a;
border-radius: 50%;
color: #fff;
font-weight: 900;
}
.tip-title {
color: #813f16;
font-size: 25rpx;
font-weight: 800;
}
.tip-copy {
margin-top: 8rpx;
color: #9c531e;
font-size: 22rpx;
line-height: 1.55;
}
button[disabled] {
background: #c8ceca;
color: #fff;
}