first commit

This commit is contained in:
ytc1012
2026-02-04 16:11:55 +08:00
commit 0f3ee050dc
165 changed files with 25795 additions and 0 deletions

19
miniprogram/app.js Normal file
View File

@@ -0,0 +1,19 @@
// app.js
App({
onLaunch() {
if (!wx.cloud) {
console.error('请使用 2.2.3 或以上的基础库以使用云能力')
} else {
wx.cloud.init({
// env 参数说明:
// env 参数决定接下来小程序发起的云开发调用wx.cloud.xxx会默认请求到哪个云环境的资源
// 此处请填入环境 ID, 环境 ID 可打开云控制台查看
// 如不填则使用默认环境(第一个创建的环境)
env: 'cloud1-5gcxzw9bb70ac4fe',
traceUser: true,
})
}
this.globalData = {}
}
})

25
miniprogram/app.json Normal file
View File

@@ -0,0 +1,25 @@
{
"pages": [
"pages/index/index",
"pages/create-room/create-room",
"pages/room/room",
"pages/result/result"
],
"window": {
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#fff",
"navigationBarTitleText": "我们去哪聚",
"navigationBarTextStyle": "black"
},
"sitemapLocation": "sitemap.json",
"permission": {
"scope.userLocation": {
"desc": "你的位置信息将用于计算最佳会面点"
}
},
"requiredPrivateInfos": [
"getLocation",
"chooseLocation"
],
"lazyCodeLoading": "requiredComponents"
}

22
miniprogram/app.wxss Normal file
View File

@@ -0,0 +1,22 @@
/**app.wxss**/
.container {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 200rpx 0;
box-sizing: border-box;
}
button {
background: initial;
}
button:focus{
outline: 0;
}
button::after{
border: none;
}

View File

@@ -0,0 +1,179 @@
const app = getApp()
const db = wx.cloud.database()
Page({
data: {
name: '',
keywords: ['咖啡馆', '餐厅', '公园', '商场', '电影院', 'KTV', '酒吧', '火锅', '烧烤', '其他'],
keyword: '咖啡馆',
meetTime: '',
meetDate: '',
meetTimeOnly: '',
requirements: '',
userInfo: {
avatarUrl: '',
nickName: ''
}
},
onLoad() {
// 生成默认聚会名称
const randomNum = Math.floor(Math.random() * 9000) + 1000
this.setData({
name: `聚会-${randomNum}`
})
},
onNameInput(e) {
this.setData({
name: e.detail.value
})
},
onSelectKeyword(e) {
this.setData({
keyword: e.currentTarget.dataset.keyword
})
},
onTimeInput(e) {
this.setData({
meetTime: e.detail.value
})
},
onDateChange(e) {
const date = e.detail.value
this.setData({
meetDate: date
})
this.updateMeetTime()
},
onTimeChange(e) {
const time = e.detail.value
this.setData({
meetTimeOnly: time
})
this.updateMeetTime()
},
updateMeetTime() {
const { meetDate, meetTimeOnly } = this.data
if (meetDate && meetTimeOnly) {
this.setData({
meetTime: `${meetDate} ${meetTimeOnly}`
})
} else if (meetDate) {
this.setData({
meetTime: meetDate
})
}
},
onRequirementsInput(e) {
this.setData({
requirements: e.detail.value
})
},
onChooseAvatar(e) {
const { avatarUrl } = e.detail
this.setData({
'userInfo.avatarUrl': avatarUrl
})
},
onNicknameChange(e) {
const nickName = e.detail.value
this.setData({
'userInfo.nickName': nickName
})
},
onSubmit() {
const { name, keyword, meetTime, requirements, userInfo } = this.data
if (!name.trim()) {
wx.showToast({
title: '请输入聚会名称',
icon: 'none'
})
return
}
// 检查是否填写了昵称
if (!userInfo.nickName || !userInfo.nickName.trim()) {
wx.showToast({
title: '请输入你的昵称',
icon: 'none'
})
return
}
this.createRoom(name, keyword, meetTime, requirements)
},
onCancel() {
wx.navigateBack()
},
createRoom(name, keywords, meetTime, requirements) {
wx.showLoading({ title: '创建中...', mask: true })
wx.cloud.callFunction({
name: 'createRoom',
data: {
name,
meetTime,
keywords,
requirements: requirements || '',
userInfo: this.data.userInfo
},
success: res => {
wx.hideLoading()
if (res.result.success) {
const roomId = res.result.roomId
// 保存到本地存储
let myRooms = wx.getStorageSync('myRooms') || []
myRooms = myRooms.filter(item => item.roomId !== roomId)
myRooms.unshift({
roomId,
name,
meetTime,
keywords
})
if (myRooms.length > 2) {
myRooms = myRooms.slice(0, 2)
}
wx.setStorageSync('myRooms', myRooms)
// 更新全局roomList
let roomList = wx.getStorageSync('roomList') || []
roomList = roomList.filter(item => item.roomId !== roomId)
roomList.unshift({
roomId,
name,
meetTime,
keywords
})
if (roomList.length > 5) {
roomList = roomList.slice(0, 5)
}
wx.setStorageSync('roomList', roomList)
wx.redirectTo({
url: `/pages/room/room?roomId=${roomId}&isCreator=true`
})
} else {
wx.showToast({ title: res.result.msg || '创建失败', icon: 'none' })
}
},
fail: err => {
wx.hideLoading()
console.error('云函数调用失败', err)
wx.showToast({ title: '网络错误', icon: 'none' })
}
})
}
})

View File

@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "创建聚会"
}

View File

@@ -0,0 +1,69 @@
<view class="container">
<form bindsubmit="onSubmit">
<view class="form-section">
<view class="form-item">
<view class="label">聚会名称</view>
<input class="input" placeholder="请输入聚会名称" value="{{name}}" bindinput="onNameInput" />
</view>
<view class="form-item">
<view class="label">聚会地点类型</view>
<view class="keywords-grid">
<view
class="keyword-item {{keyword === item ? 'active' : ''}}"
wx:for="{{keywords}}"
wx:key="index"
bindtap="onSelectKeyword"
data-keyword="{{item}}"
>
{{item}}
</view>
</view>
</view>
<view class="form-item">
<view class="label">聚会时间(可选)</view>
<view class="time-picker-row">
<picker mode="date" value="{{meetDate}}" bindchange="onDateChange" class="picker">
<view class="picker-value">
{{meetDate || '选择日期'}}
</view>
</picker>
<picker mode="time" value="{{meetTimeOnly}}" bindchange="onTimeChange" class="picker">
<view class="picker-value">
{{meetTimeOnly || '选择时间'}}
</view>
</picker>
</view>
</view>
<view class="form-item">
<view class="label">特殊需求(可选)</view>
<textarea
class="textarea"
placeholder="如需要停车位、环境安静、有Wi-Fi等"
value="{{requirements}}"
bindinput="onRequirementsInput"
maxlength="200"
show-confirm-bar="{{false}}"
/>
<view class="char-count">{{requirements.length}}/200</view>
</view>
<view class="form-item">
<view class="label">你的信息</view>
<view class="user-info-section">
<button class="avatar-wrapper" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
<image class="avatar" src="{{userInfo.avatarUrl || '/images/default-avatar.png'}}" mode="aspectFill"></image>
</button>
<input type="nickname" class="nickname-input" placeholder="请输入昵称" value="{{userInfo.nickName}}" bindchange="onNicknameChange" />
</view>
</view>
</view>
<view class="btn-area">
<button class="submit-btn" form-type="submit">创建聚会</button>
<button class="cancel-btn" bindtap="onCancel">取消</button>
</view>
</form>
</view>

View File

@@ -0,0 +1,156 @@
.container {
min-height: 100vh;
background-color: #f6f7f9;
padding: 0;
align-items: stretch;
width: 100%;
}
.form-section {
margin-top: 40rpx;
padding: 0 80rpx;
}
.form-item {
background-color: transparent;
padding: 20rpx 0;
border-radius: 0;
margin-bottom: 30rpx;
box-sizing: border-box;
width: 100%;
}
.label {
font-size: 28rpx;
color: #333;
margin-bottom: 20rpx;
font-weight: 500;
}
.input {
width: 100%;
font-size: 32rpx;
color: #333;
border: none;
outline: none;
}
.time-picker-row {
display: flex;
gap: 20rpx;
}
.picker {
flex: 1;
background-color: #fff;
border-radius: 8rpx;
padding: 20rpx;
border: 1rpx solid #e5e5e5;
}
.picker-value {
font-size: 32rpx;
color: #333;
text-align: center;
}
.textarea {
width: 100%;
min-height: 120rpx;
font-size: 32rpx;
color: #333;
background-color: #fff;
border-radius: 8rpx;
padding: 20rpx;
border: 1rpx solid #e5e5e5;
box-sizing: border-box;
}
.char-count {
font-size: 24rpx;
color: #999;
text-align: right;
margin-top: 10rpx;
}
.keywords-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16rpx;
}
.keyword-item {
background-color: #f5f5f5;
color: #666;
padding: 20rpx;
border-radius: 12rpx;
font-size: 28rpx;
text-align: center;
transition: all 0.3s;
}
.keyword-item.active {
background-color: #07c160;
color: white;
}
.btn-area {
margin-top: 60rpx;
padding: 0 80rpx;
}
.submit-btn {
background-color: #07c160;
color: white;
font-size: 36rpx;
padding: 24rpx 0;
border-radius: 50rpx;
width: 100%;
margin-bottom: 20rpx;
}
.cancel-btn {
background-color: #f5f5f5;
color: #666;
font-size: 36rpx;
padding: 24rpx 0;
border-radius: 50rpx;
width: 100%;
}
.user-info-section {
display: flex;
align-items: center;
gap: 20rpx;
background-color: #fff;
padding: 20rpx;
border-radius: 12rpx;
border: 1rpx solid #e5e5e5;
}
.avatar-wrapper {
padding: 0;
margin: 0;
border: none;
background: none;
line-height: 1;
}
.avatar-wrapper::after {
border: none;
}
.avatar {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
background-color: #f0f0f0;
}
.nickname-input {
flex: 1;
font-size: 32rpx;
color: #333;
border: none;
outline: none;
}

View File

@@ -0,0 +1,234 @@
const app = getApp()
const db = wx.cloud.database()
Page({
data: {
myRooms: [],
joinedRooms: [],
userOpenId: ''
},
onLoad: function () {
// 先从本地加载
const myRooms = wx.getStorageSync('myRooms') || []
this.setData({ myRooms })
// 获取用户openid后加载完整数据
this.getUserOpenId()
},
onShow: function () {
// 每次显示页面时重新加载聚会列表
if (this.data.userOpenId) {
this.loadRooms()
} else {
// 先显示本地数据,异步加载完整数据
this.getUserOpenId()
}
},
async getUserOpenId() {
try {
// 调用云函数获取 openid
const res = await wx.cloud.callFunction({
name: 'getOpenId'
})
if (res.result && res.result.openid) {
this.setData({ userOpenId: res.result.openid })
this.loadRooms()
}
} catch (err) {
console.error('获取openid失败', err)
}
},
async loadRooms() {
try {
const userOpenId = this.data.userOpenId
if (!userOpenId) return
const _ = db.command
// 查询我创建的聚会
const myRoomsRes = await db.collection('rooms')
.where({
_openid: userOpenId
})
.orderBy('createdAt', 'desc')
.limit(5)
.get()
// 查询我参与的聚会members中有我但不是我创建的
const joinedRoomsRes = await db.collection('rooms')
.where({
_openid: _.neq(userOpenId),
'members.openid': userOpenId
})
.orderBy('createdAt', 'desc')
.limit(5)
.get()
// 转换数据格式
const myRooms = myRoomsRes.data.map(room => ({
roomId: room._id,
name: room.name,
meetTime: room.meetTime
}))
const joinedRooms = joinedRoomsRes.data.map(room => ({
roomId: room._id,
name: room.name,
meetTime: room.meetTime
}))
// 更新到本地存储
const allRooms = [...myRooms, ...joinedRooms]
const uniqueRooms = this.getUniqueRooms(allRooms)
wx.setStorageSync('roomList', uniqueRooms)
wx.setStorageSync('myRooms', myRooms.slice(0, 2))
this.setData({
myRooms: myRooms.slice(0, 2),
joinedRooms: joinedRooms.slice(0, 2)
})
} catch (err) {
console.error('加载聚会列表失败', err)
}
},
getUniqueRooms(rooms) {
const unique = {}
rooms.forEach(room => {
unique[room.roomId] = room
})
// 使用 Object.keys + map 代替 Object.values
return Object.keys(unique).map(key => unique[key])
},
onJoinRoom(e) {
const roomId = e.currentTarget.dataset.roomid
wx.navigateTo({
url: `/pages/room/room?roomId=${roomId}&isCreator=true`
})
},
onDeleteRoom(e) {
const roomId = e.currentTarget.dataset.roomid
wx.showModal({
title: '删除聚会',
content: '确定要删除这个聚会吗?',
confirmText: '删除',
confirmColor: '#ff4d4f',
success: (res) => {
if (res.confirm) {
this.deleteRoom(roomId)
}
}
})
},
async deleteRoom(roomId) {
wx.showLoading({ title: '删除中...' })
try {
// 调用云函数删除聚会
const res = await wx.cloud.callFunction({
name: 'deleteRoom',
data: { roomId }
})
wx.hideLoading()
if (res.result && res.result.success) {
// 从本地存储中移除
let myRooms = wx.getStorageSync('myRooms') || []
myRooms = myRooms.filter(item => item.roomId !== roomId)
wx.setStorageSync('myRooms', myRooms)
let roomList = wx.getStorageSync('roomList') || []
roomList = roomList.filter(item => item.roomId !== roomId)
wx.setStorageSync('roomList', roomList)
// 更新页面数据
this.setData({
myRooms
})
wx.showToast({ title: '删除成功', icon: 'success' })
} else {
wx.showToast({ title: res.result?.msg || '删除失败', icon: 'none' })
}
} catch (err) {
wx.hideLoading()
console.error('删除聚会失败', err)
wx.showToast({ title: '网络错误', icon: 'none' })
}
},
onCreateRoom() {
wx.navigateTo({
url: '/pages/create-room/create-room'
})
},
createRoom(name, keywords, meetTime) {
wx.showLoading({ title: '创建中...', mask: true })
wx.cloud.callFunction({
name: 'createRoom',
data: {
name,
meetTime,
keywords,
requirements: ''
},
success: res => {
wx.hideLoading()
if (res.result.success) {
const roomId = res.result.roomId
// 保存到本地存储
let myRooms = wx.getStorageSync('myRooms') || []
myRooms = myRooms.filter(item => item.roomId !== roomId)
myRooms.unshift({
roomId,
name,
meetTime,
keywords
})
if (myRooms.length > 2) {
myRooms = myRooms.slice(0, 2)
}
wx.setStorageSync('myRooms', myRooms)
// 更新全局roomList
let roomList = wx.getStorageSync('roomList') || []
roomList = roomList.filter(item => item.roomId !== roomId)
roomList.unshift({
roomId,
name,
meetTime,
keywords
})
if (roomList.length > 5) {
roomList = roomList.slice(0, 5)
}
wx.setStorageSync('roomList', roomList)
wx.navigateTo({
url: `/pages/room/room?roomId=${roomId}&isCreator=true`
})
} else {
wx.showToast({ title: res.result.msg || '创建失败', icon: 'none' })
}
},
fail: err => {
wx.hideLoading()
console.error('云函数调用失败', err)
wx.showToast({ title: '网络错误', icon: 'none' })
}
})
}
})

View File

@@ -0,0 +1,4 @@
{
"usingComponents": {},
"navigationBarTitleText": "我们去哪聚"
}

View File

@@ -0,0 +1,55 @@
<view class="container">
<view class="header">
<view class="title">我们去哪聚</view>
<view class="subtitle">找到你们的完美聚会点</view>
<button class="primary-btn" bindtap="onCreateRoom">发起聚会</button>
</view>
<!-- 我发起的聚会 -->
<view class="room-section" wx:if="{{myRooms.length > 0}}">
<view class="section-title">我发起的</view>
<block wx:for="{{myRooms}}" wx:key="roomId">
<view class="room-item">
<view class="room-info" bindtap="onJoinRoom" data-roomid="{{item.roomId}}">
<view class="room-name">{{item.name}}</view>
<view class="room-meta">
<text wx:if="{{item.meetTime}}">{{item.meetTime}}</text>
<text wx:else>时间未定</text>
</view>
</view>
<view class="room-actions">
<view class="room-arrow">→</view>
<view class="delete-btn" catchtap="onDeleteRoom" data-roomid="{{item.roomId}}">
删除
</view>
</view>
</view>
</block>
</view>
<!-- 我参与的聚会 -->
<view class="room-section" wx:if="{{joinedRooms.length > 0}}">
<view class="section-title">我参与的</view>
<block wx:for="{{joinedRooms}}" wx:key="roomId">
<view class="room-item" bindtap="onJoinRoom" data-roomid="{{item.roomId}}">
<view class="room-info">
<view class="room-name">{{item.name}}</view>
<view class="room-meta">
<text wx:if="{{item.meetTime}}">{{item.meetTime}}</text>
<text wx:else>时间未定</text>
</view>
</view>
<view class="room-arrow">→</view>
</view>
</block>
</view>
<!-- 无聚会提示 -->
<view class="empty-tip" wx:if="{{myRooms.length === 0 && joinedRooms.length === 0}}">
<view class="empty-text">还没有聚会,快去创建一个吧</view>
</view>
<view class="desc-area">
<view class="desc">分享给好友,共同寻找中间点</view>
</view>
</view>

View File

@@ -0,0 +1,114 @@
.container {
display: flex;
flex-direction: column;
height: 100vh;
padding: 0 40rpx;
background-color: #f6f7f9;
overflow: hidden;
}
.header {
padding: 60rpx 0 40rpx;
text-align: center;
}
.title {
font-size: 48rpx;
font-weight: bold;
color: #333;
margin-bottom: 16rpx;
}
.subtitle {
font-size: 32rpx;
color: #666;
margin-bottom: 40rpx;
}
.room-section {
margin-bottom: 20rpx;
}
.section-title {
font-size: 28rpx;
color: #999;
margin-bottom: 20rpx;
}
.room-item {
display: flex;
align-items: center;
justify-content: space-between;
background-color: #fff;
padding: 24rpx;
border-radius: 20rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.04);
}
.room-info {
flex: 1;
}
.room-name {
font-size: 32rpx;
font-weight: 500;
color: #333;
margin-bottom: 8rpx;
}
.room-meta {
font-size: 24rpx;
color: #666;
}
.room-actions {
display: flex;
align-items: center;
gap: 10rpx;
}
.room-arrow {
font-size: 40rpx;
color: #07c160;
}
.delete-btn {
font-size: 24rpx;
color: #ff4d4f;
padding: 12rpx 24rpx;
border-radius: 8rpx;
background-color: #fff1f0;
}
.empty-tip {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.empty-text {
font-size: 28rpx;
color: #999;
}
.desc-area {
padding-bottom: 40rpx;
}
.primary-btn {
background-color: #07c160;
color: white;
font-size: 36rpx;
padding: 20rpx 0;
border-radius: 50rpx;
width: 100%;
margin-bottom: 20rpx;
}
.desc {
font-size: 24rpx;
color: #999;
text-align: center;
}

View File

@@ -0,0 +1,128 @@
const app = getApp()
const db = wx.cloud.database()
Page({
data: {
roomId: '',
center: { latitude: 39.9, longitude: 116.4 },
markers: [],
recommendations: [],
scale: 13,
selectedIndex: -1 // 当前选中的索引
},
onLoad: function (options) {
this.setData({ roomId: options.roomId })
this.fetchResult()
},
async fetchResult() {
wx.showLoading({ title: '加载结果...' })
try {
const res = await db.collection('rooms').doc(this.data.roomId).get()
const data = res.data
if (data && data.result && data.result.success) {
const result = data.result
const center = result.center
const recommendations = result.recommendations || []
// 构建 Markers
const markers = []
// 中心点 Marker
markers.push({
id: 0,
latitude: center.lat,
longitude: center.lng,
iconPath: '',
width: 0,
height: 0,
callout: {
content: '最佳中心点',
display: 'ALWAYS',
padding: 8,
borderRadius: 4,
bgColor: '#FF6B6B',
color: '#FFFFFF',
fontSize: 12
}
})
// 推荐点 Markers
recommendations.forEach((place, index) => {
markers.push({
id: index + 1, // ID 从 1 开始
latitude: place.location.lat,
longitude: place.location.lng,
iconPath: '',
width: 0,
height: 0,
callout: {
content: `${index + 1}`, // 默认只显示序号
display: 'ALWAYS',
padding: 6,
borderRadius: 4,
bgColor: '#4CAF50',
color: '#FFFFFF',
fontSize: 11
}
})
})
this.setData({
center: { latitude: center.lat, longitude: center.lng },
recommendations: recommendations,
markers: markers
})
} else {
wx.showToast({ title: '暂无结果', icon: 'none' })
}
wx.hideLoading()
} catch (err) {
console.error(err)
wx.hideLoading()
wx.showToast({ title: '加载失败', icon: 'none' })
}
},
onListTap(e) {
const index = e.currentTarget.dataset.index
const place = this.data.recommendations[index]
// 如果点击已选中的,取消选中
const newSelectedIndex = this.data.selectedIndex === index ? -1 : index
// 更新选中状态
this.setData({ selectedIndex: newSelectedIndex })
// 更新地图标记的 callout
const markers = this.data.markers
markers.forEach((marker, markerIndex) => {
if (marker.id > 0) { // 推荐点 MarkerID 从 1 开始)
const placeIndex = marker.id - 1
if (newSelectedIndex === placeIndex) {
// 选中的地点显示名称
markers[markerIndex].callout.content = `${index + 1}. ${place.name}`
markers[markerIndex].callout.bgColor = '#FF6B6B' // 红色背景
markers[markerIndex].callout.fontSize = 12
} else {
// 未选中的地点只显示序号
markers[markerIndex].callout.content = `${placeIndex + 1}`
markers[markerIndex].callout.bgColor = '#4CAF50' // 绿色背景
markers[markerIndex].callout.fontSize = 11
}
}
})
// 更新 markers 数据
this.setData({ markers })
// 移动地图到选中地点
if (newSelectedIndex >= 0) {
this.setData({
center: { latitude: place.location.lat, longitude: place.location.lng }
})
}
}
})

View File

@@ -0,0 +1,4 @@
{
"usingComponents": {},
"navigationBarTitleText": "推荐结果"
}

View File

@@ -0,0 +1,36 @@
<view class="container">
<map
id="mapResult"
class="map"
latitude="{{center.latitude}}"
longitude="{{center.longitude}}"
scale="{{scale}}"
markers="{{markers}}"
bindmarkertap="onMarkerTap"
>
</map>
<!-- 底部推荐列表 -->
<scroll-view scroll-y class="rec-list">
<block wx:for="{{recommendations}}" wx:key="id">
<view class="rec-card {{selectedIndex === index ? 'selected' : ''}}" bindtap="onListTap" data-index="{{index}}">
<view class="rec-header">
<view class="rec-rank {{selectedIndex === index ? 'selected' : ''}}">{{index+1}}</view>
<view class="rec-info-right">
<view class="rec-name">{{item.name}}</view>
<view class="rec-info">
<text class="rating">{{item.rating}}分</text>
<text class="divider">|</text>
<text class="distance">{{item.distance}}米</text>
</view>
</view>
</view>
<view class="rec-address">{{item.address}}</view>
<view class="rec-tags" wx:if="{{item.tags && item.tags.length}}">
<text class="tag" wx:for="{{item.tags}}" wx:key="*this" wx:for-item="tag">{{tag}}</text>
</view>
<view class="rec-reason" wx:if="{{item.reason}}">{{item.reason}}</view>
</view>
</block>
</scroll-view>
</view>

View File

@@ -0,0 +1,141 @@
.container {
height: 100vh;
display: flex;
flex-direction: column;
padding: 0 !important;
box-sizing: border-box;
align-items: stretch !important;
justify-content: flex-start !important;
}
.map {
flex: 1;
width: 100%;
}
.rec-list {
background-color: #fff;
padding: 20rpx;
height: 60vh;
box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.1);
box-sizing: border-box;
}
.rec-card {
display: flex;
flex-direction: column;
background-color: #f9f9f9;
margin-bottom: 16rpx;
padding: 20rpx;
border-radius: 12rpx;
box-sizing: border-box;
transition: all 0.3s;
border: 2rpx solid transparent;
}
.rec-card.selected {
background-color: #fff5f5;
border-color: #FF6B6B;
box-shadow: 0 4rpx 12rpx rgba(255, 107, 107, 0.15);
}
.rec-card:active {
background-color: #f0f0f0;
}
.rec-header {
display: flex;
align-items: flex-start;
margin-bottom: 12rpx;
}
.rec-rank {
width: 44rpx;
height: 44rpx;
background-color: #667eea;
color: white;
font-size: 24rpx;
font-weight: bold;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16rpx;
flex-shrink: 0;
}
.rec-rank.selected {
background-color: #FF6B6B;
transform: scale(1.1);
}
.rec-info-right {
flex: 1;
min-width: 0;
}
.rec-name {
font-size: 30rpx;
font-weight: bold;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: 8rpx;
}
.rec-info {
display: flex;
align-items: center;
font-size: 24rpx;
}
.rec-info .rating {
color: #ff9800;
font-weight: 500;
}
.rec-info .divider {
color: #ddd;
margin: 0 8rpx;
}
.rec-info .distance {
color: #666;
}
.rec-address {
font-size: 24rpx;
color: #666;
margin-bottom: 10rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.rec-tags {
display: flex;
flex-wrap: wrap;
margin-bottom: 10rpx;
}
.tag {
font-size: 22rpx;
background-color: #e8f7f0;
color: #07c160;
padding: 4rpx 10rpx;
border-radius: 8rpx;
margin-right: 8rpx;
margin-bottom: 6rpx;
}
.rec-reason {
background-color: #fffbf0;
color: #d97a00;
padding: 12rpx;
border-radius: 8rpx;
font-size: 24rpx;
line-height: 1.5;
}

View File

@@ -0,0 +1,610 @@
const app = getApp()
const db = wx.cloud.database()
Page({
data: {
roomId: '',
roomName: '',
roomTime: '',
keywords: '',
keywordOptions: ['咖啡馆', '餐厅', '公园', '商场', '电影院', 'KTV', '酒吧', '火锅', '烧烤', '其他'],
keywordIndex: 0,
requirements: '',
members: [],
readyCount: 0,
hasResult: false,
currentUserOpenId: '',
isCreator: false,
calculating: false,
editingRequirements: false,
meetDate: '',
meetTimeOnly: '',
hasJoined: false,
userInfo: {
avatarUrl: '',
nickName: ''
},
showUserInfoModal: false,
previousStatus: '' // 新增:记录上一次的状态,用于判断是否刚完成计算
},
onLoad: function (options) {
const roomId = options.roomId
const isCreator = options.isCreator === 'true'
this.setData({
roomId,
isCreator,
// 如果是创建者,已经在创建时加入了房间
hasJoined: isCreator
})
// 直接开始监听房间,不需要先加入
this.startWatch()
},
onUnload: function () {
if (this.watcher) {
this.watcher.close()
}
},
async joinGroup() {
// 显示用户信息填写弹窗
this.setData({ showUserInfoModal: true })
},
onChooseAvatar(e) {
const { avatarUrl } = e.detail
this.setData({
'userInfo.avatarUrl': avatarUrl
})
},
onNicknameChange(e) {
const nickName = e.detail.value
this.setData({
'userInfo.nickName': nickName
})
},
onCancelUserInfo() {
this.setData({ showUserInfoModal: false })
},
async onConfirmUserInfo() {
const { userInfo } = this.data
// 检查是否填写了昵称
if (!userInfo.nickName || !userInfo.nickName.trim()) {
wx.showToast({
title: '请输入你的昵称',
icon: 'none'
})
return
}
// 关闭弹窗
this.setData({ showUserInfoModal: false })
// 调用云函数加入
wx.showLoading({ title: '加入房间...' })
try {
const res = await wx.cloud.callFunction({
name: 'joinRoom',
data: {
roomId: this.data.roomId,
userInfo: userInfo
}
})
wx.hideLoading()
if (!res.result.success) {
wx.showModal({ title: '错误', content: res.result.msg, showCancel: false })
return false
}
this.setData({ hasJoined: true })
// 加入成功后,继续添加位置
if (this.pendingAddLocation) {
this.pendingAddLocation()
this.pendingAddLocation = null
}
return true
} catch (err) {
console.error(err)
wx.hideLoading()
wx.showToast({ title: '加入失败', icon: 'none' })
return false
}
},
startWatch() {
// 先获取当前用户的 openid
this.getCurrentUserOpenId()
this.watcher = db.collection('rooms').doc(this.data.roomId).watch({
onChange: snapshot => {
if (snapshot.docs && snapshot.docs.length > 0) {
const roomData = snapshot.docs[0]
const members = roomData.members || []
const readyCount = members.filter(m => m.location).length
const hasResult = roomData.result && roomData.result.success && roomData.status === 'calculated'
const isCalculating = roomData.status === 'calculating'
const currentStatus = roomData.status
const roomTime = roomData.meetTime || ''
const [date, time] = roomTime.includes(' ') ? roomTime.split(' ') : [roomTime, '']
const keyword = roomData.keywords || '咖啡馆'
const keywordIndex = this.data.keywordOptions.indexOf(keyword)
// 检查当前用户是否已经在成员列表中
const currentUserOpenId = this.data.currentUserOpenId
const hasJoined = currentUserOpenId && members.some(m => m.openid === currentUserOpenId)
// 判断是否刚完成计算(从 calculating 变为 calculated
const justFinishedCalculating = this.data.previousStatus === 'calculating' && currentStatus === 'calculated'
this.setData({
members,
readyCount,
hasResult,
calculating: isCalculating, // 同步云端计算状态
roomName: roomData.name || '未命名聚会',
roomTime: roomTime,
meetDate: date,
meetTimeOnly: time,
keywords: keyword,
keywordIndex: keywordIndex >= 0 ? keywordIndex : 0,
requirements: roomData.requirements || '',
hasJoined: hasJoined || this.data.hasJoined,
previousStatus: currentStatus // 更新状态记录
})
// 只有刚完成计算时才自动跳转到结果页
if (justFinishedCalculating && hasResult) {
wx.navigateTo({
url: `/pages/result/result?roomId=${this.data.roomId}`
})
}
}
},
onError: err => {
console.error('Watch Error', err)
}
})
},
async getCurrentUserOpenId() {
try {
const res = await wx.cloud.callFunction({
name: 'getOpenId'
})
if (res.result && res.result.openid) {
this.setData({ currentUserOpenId: res.result.openid })
}
} catch (err) {
console.error('获取openid失败', err)
}
},
onAddLocation() {
if (!this.data.roomId) return
// 定义添加位置的函数
const doAddLocation = () => {
wx.chooseLocation({
success: (res) => {
const { latitude, longitude, address, name } = res
wx.showLoading({ title: '提交位置...' })
wx.cloud.callFunction({
name: 'updateLocation',
data: {
roomId: this.data.roomId,
location: {
lng: longitude,
lat: latitude,
address: address || '地图选点',
name: name || '我的位置'
}
},
success: res => {
wx.hideLoading()
if (res.result.success) {
wx.showToast({ title: '已更新', icon: 'success' })
} else {
wx.showToast({ title: res.result.msg || '更新失败', icon: 'none' })
}
},
fail: err => {
wx.hideLoading()
console.error(err)
wx.showToast({ title: '网络错误', icon: 'none' })
}
})
},
fail: (err) => {
if (err.errMsg.indexOf('cancel') === -1) {
wx.showToast({ title: '选择位置失败', icon: 'none' })
}
}
})
}
// 如果还没加入房间,先加入
if (!this.data.hasJoined) {
// 保存待执行的操作
this.pendingAddLocation = doAddLocation
this.joinGroup()
} else {
doAddLocation()
}
},
onShareAppMessage() {
return {
title: '快来填位置,我们去找个中间点聚会!',
path: `/pages/room/room?roomId=${this.data.roomId}`
}
},
// 添加测试成员(仅用于开发测试)
onAddTestMember() {
wx.showModal({
title: '添加测试成员',
content: '请输入成员名称',
editable: true,
placeholderText: '测试成员',
success: (res) => {
if (res.confirm && res.content) {
const nickName = res.content.trim() || '测试成员'
wx.showLoading({ title: '添加中...' })
wx.cloud.callFunction({
name: 'addTestMember',
data: {
roomId: this.data.roomId,
nickName: nickName
},
success: res => {
wx.hideLoading()
if (res.result.success) {
wx.showToast({ title: '添加成功', icon: 'success' })
} else {
wx.showToast({ title: res.result.msg || '添加失败', icon: 'none' })
}
},
fail: err => {
wx.hideLoading()
console.error(err)
wx.showToast({ title: '网络错误', icon: 'none' })
}
})
}
}
})
},
// 为测试成员添加/修改位置
onAddMemberLocation(e) {
const { openid, nickname, istest } = e.currentTarget.dataset
// 检查是否是测试成员,如果不是则不处理
if (!istest) {
return
}
wx.chooseLocation({
success: (res) => {
const { latitude, longitude, address, name } = res
wx.showLoading({ title: '提交中...' })
wx.cloud.callFunction({
name: 'updateMemberLocation',
data: {
roomId: this.data.roomId,
memberOpenid: openid,
location: {
lng: longitude,
lat: latitude,
address: address || '地图选点',
name: name || '选定位置'
}
},
success: res => {
wx.hideLoading()
if (res.result.success) {
wx.showToast({ title: `${nickname}位置已更新`, icon: 'success' })
} else {
wx.showToast({ title: res.result.msg || '更新失败', icon: 'none' })
}
},
fail: err => {
wx.hideLoading()
console.error(err)
wx.showToast({ title: '网络错误', icon: 'none' })
}
})
},
fail: (err) => {
if (err.errMsg.indexOf('cancel') === -1) {
wx.showToast({ title: '选择位置失败', icon: 'none' })
}
}
})
},
onCalculate() {
// 前端防抖:检查是否正在计算
if (this.data.calculating) {
wx.showToast({
title: '正在计算中,请稍候...',
icon: 'none',
duration: 2000
})
return
}
const membersWithLocation = this.data.members.filter(m => m.location).length
if (membersWithLocation < 2) {
wx.showToast({
title: '至少需要2人添加位置才能计算',
icon: 'none',
duration: 2000
})
return
}
// 设置计算状态,禁用按钮
this.setData({ calculating: true })
wx.showLoading({ title: '计算最佳地点...' })
wx.cloud.callFunction({
name: 'calculateMeetSpot',
data: { roomId: this.data.roomId },
success: res => {
wx.hideLoading()
this.setData({ calculating: false })
// 处理后端返回的状态锁信息
if (res.result.isCalculating) {
wx.showToast({
title: res.result.msg || '正在计算中',
icon: 'none',
duration: 2000
})
return
}
// 处理重复计算拦截
if (res.result.isDuplicate) {
wx.showModal({
title: '提示',
content: res.result.msg || '成员位置未变化,无需重复计算',
confirmText: '查看结果',
cancelText: '知道了',
success: (modalRes) => {
if (modalRes.confirm) {
// 跳转到结果页
wx.navigateTo({
url: `/pages/result/result?roomId=${this.data.roomId}`
})
}
}
})
return
}
if (!res.result.success) {
wx.showModal({
title: '计算失败',
content: res.result.msg,
showCancel: false
})
}
// 成功后依靠 watch 自动跳转,或者这里手动跳转也可以
},
fail: err => {
wx.hideLoading()
this.setData({ calculating: false })
console.error(err)
wx.showToast({ title: '调用失败', icon: 'none' })
}
})
},
onViewResult() {
if (!this.data.hasResult) {
wx.showToast({ title: '暂无计算结果', icon: 'none' })
return
}
wx.navigateTo({
url: `/pages/result/result?roomId=${this.data.roomId}`
})
},
onEditRoomName() {
wx.showModal({
title: '修改聚会名称',
content: '请输入新的聚会名称',
editable: true,
placeholderText: this.data.roomName,
success: (res) => {
if (res.confirm && res.content.trim()) {
const newName = res.content.trim()
this.updateRoomInfo({ name: newName })
}
}
})
},
onEditRoomTime() {
// 不需要做任何事,交给 picker 组件处理
},
onDateChange(e) {
const date = e.detail.value
this.setData({ meetDate: date })
this.updateMeetTime()
},
onTimeChange(e) {
const time = e.detail.value
this.setData({ meetTimeOnly: time })
this.updateMeetTime()
},
updateMeetTime() {
const { meetDate, meetTimeOnly } = this.data
let newTime = ''
if (meetDate && meetTimeOnly) {
newTime = `${meetDate} ${meetTimeOnly}`
} else if (meetDate) {
newTime = meetDate
}
if (newTime) {
this.setData({ roomTime: newTime })
this.updateRoomInfo({ meetTime: newTime })
}
},
onKeywordChange(e) {
const index = e.detail.value
const keyword = this.data.keywordOptions[index]
this.setData({
keywordIndex: index,
keywords: keyword
})
this.updateRoomInfo({ keywords: keyword })
},
onEditKeywords() {
// 已改用 picker此方法不再需要
},
onEditRequirements() {
this.setData({ editingRequirements: true })
},
onRequirementsInput(e) {
this.setData({ requirements: e.detail.value })
},
onRequirementsBlur() {
this.setData({ editingRequirements: false })
this.updateRoomInfo({ requirements: this.data.requirements })
},
onRequirementsConfirm() {
this.setData({ editingRequirements: false })
this.updateRoomInfo({ requirements: this.data.requirements })
},
updateRoomInfo(updateData) {
wx.showLoading({ title: '更新中...' })
wx.cloud.callFunction({
name: 'updateRoomInfo',
data: {
roomId: this.data.roomId,
...updateData
},
success: res => {
wx.hideLoading()
if (res.result && res.result.success) {
wx.showToast({ title: '更新成功', icon: 'success' })
} else {
wx.showToast({ title: res.result?.msg || '更新失败', icon: 'none' })
}
},
fail: err => {
wx.hideLoading()
console.error('更新失败', err)
wx.showToast({ title: '网络错误', icon: 'none' })
}
})
},
// 退出聚会(普通成员)
onLeaveRoom() {
wx.showModal({
title: '确认退出',
content: '确定要退出这个聚会吗?',
success: (res) => {
if (res.confirm) {
this.removeMember()
}
}
})
},
// 移除成员(创建者权限)
onRemoveMember(e) {
const { openid, nickname } = e.currentTarget.dataset
// 不能移除测试成员(测试成员应该用专门的删除逻辑)
const member = this.data.members.find(m => m.openid === openid)
if (member && member.isTestMember) {
wx.showToast({ title: '请使用删除功能移除测试成员', icon: 'none' })
return
}
wx.showModal({
title: '移除成员',
content: `确定要移除 ${nickname} 吗?`,
success: (res) => {
if (res.confirm) {
this.removeMember(openid, nickname)
}
}
})
},
// 执行移除操作
removeMember(memberOpenid = null, nickName = null) {
wx.showLoading({ title: '处理中...' })
wx.cloud.callFunction({
name: 'removeMember',
data: {
roomId: this.data.roomId,
memberOpenid: memberOpenid // 不传则退出自己
},
success: res => {
wx.hideLoading()
if (res.result.success) {
const msg = memberOpenid ? `已移除 ${nickName}` : res.result.msg
wx.showToast({
title: msg,
icon: 'success',
duration: 2000
})
// 如果是退出自己,返回首页
if (!memberOpenid) {
setTimeout(() => {
wx.navigateBack({ delta: 1 })
}, 2000)
}
} else {
wx.showModal({
title: '操作失败',
content: res.result.msg,
showCancel: false
})
}
},
fail: err => {
wx.hideLoading()
console.error('移除成员失败', err)
wx.showToast({ title: '网络错误', icon: 'none' })
}
})
}
})

View File

@@ -0,0 +1,5 @@
{
"usingComponents": {},
"navigationBarTitleText": "聚会房间",
"enablePullDownRefresh": false
}

View File

@@ -0,0 +1,112 @@
<view class="container">
<!-- 用户信息填写弹窗 -->
<view class="user-info-modal" wx:if="{{showUserInfoModal}}">
<view class="modal-mask"></view>
<view class="modal-content">
<view class="modal-title">填写你的信息</view>
<view class="modal-body">
<view class="avatar-section">
<button class="avatar-wrapper" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
<image class="avatar" src="{{userInfo.avatarUrl || '/images/default-avatar.png'}}" mode="aspectFill"></image>
<view class="avatar-tip">点击选择头像</view>
</button>
</view>
<input type="nickname" class="nickname-input-modal" placeholder="请输入昵称" value="{{userInfo.nickName}}" bindchange="onNicknameChange" />
</view>
<view class="modal-footer">
<button class="modal-btn cancel" bindtap="onCancelUserInfo">取消</button>
<button class="modal-btn confirm" bindtap="onConfirmUserInfo">确定</button>
</view>
</view>
</view>
<!-- 聚会信息 -->
<view class="room-info">
<view class="room-title" bindtap="onEditRoomName">{{roomName}}</view>
<view class="room-tags">
<picker mode="selector" range="{{keywordOptions}}" value="{{keywordIndex}}" bindchange="onKeywordChange">
<view class="tag keyword-tag">{{keywords}}</view>
</picker>
<view class="time-tag-picker">
<picker mode="date" value="{{meetDate}}" bindchange="onDateChange" class="time-picker">
<view class="tag time-tag">
{{meetDate || '选择日期'}}
</view>
</picker>
<picker mode="time" value="{{meetTimeOnly}}" bindchange="onTimeChange" class="time-picker">
<view class="tag time-tag">
{{meetTimeOnly || '选择时间'}}
</view>
</picker>
</view>
</view>
<!-- 特殊需求直接编辑 -->
<view class="requirements-section {{requirements || editingRequirements ? '' : 'empty'}}" bindtap="onEditRequirements">
<view class="requirements-label" wx:if="{{requirements || editingRequirements}}">特殊需求</view>
<textarea
wx:if="{{editingRequirements}}"
class="requirements-textarea"
placeholder="如需要停车位、环境安静、有Wi-Fi等"
value="{{requirements}}"
bindinput="onRequirementsInput"
bindblur="onRequirementsBlur"
bindconfirm="onRequirementsConfirm"
maxlength="200"
auto-focus="{{true}}"
show-confirm-bar="{{false}}"
/>
<view class="requirements-text" wx:elif="{{requirements}}">{{requirements}}</view>
<view class="requirements-text" wx:else>点击添加特殊需求</view>
</view>
</view>
<!-- 顶部统计信息 -->
<view class="header">
<view class="stats">
<text class="stat-item">参与成员 {{members.length}}人</text>
<text class="stat-divider">|</text>
<text class="stat-item ready-count">已就位 {{readyCount}}人</text>
</view>
</view>
<!-- 成员列表 -->
<scroll-view scroll-y class="member-list">
<block wx:for="{{members}}" wx:key="openid">
<view class="member-item {{item.location ? 'ready' : 'waiting'}}"
bindtap="onAddMemberLocation"
bindlongpress="{{isCreator && item.openid !== currentUserOpenId ? 'onRemoveMember' : ''}}"
data-openid="{{item.openid}}"
data-nickname="{{item.nickName}}"
data-istest="{{item.isTestMember}}">
<image class="avatar" src="{{item.avatarUrl || '/images/default-avatar.png'}}" mode="aspectFill"></image>
<view class="info">
<view class="nickname">
{{item.nickName}}
<text wx:if="{{item.openid === currentUserOpenId}}" class="me-tag">(我)</text>
</view>
<view class="status">
{{item.location ? item.location.name || '已选定位置' : '等待添加位置...'}}
</view>
</view>
<view class="status-icon">{{item.location ? '√' : '○'}}</view>
</view>
</block>
</scroll-view>
<!-- 测试按钮(浮动) -->
<view class="test-float-btn" bindtap="onAddTestMember" wx:if="{{true}}">
<text>+</text>
</view>
<!-- 底部操作按钮 -->
<view class="footer-actions">
<button class="action-btn secondary-btn" bindtap="onAddLocation">添加/修改我的位置</button>
<button class="action-btn secondary-btn" open-type="share">邀请好友</button>
<button wx:if="{{!isCreator && hasJoined}}" class="action-btn warning-btn" bindtap="onLeaveRoom">退出聚会</button>
<button class="action-btn primary-btn" bindtap="onCalculate" loading="{{calculating}}" disabled="{{calculating}}">
{{calculating ? '计算中...' : '开始计算'}}
</button>
<button class="action-btn result-btn {{hasResult ? '' : 'disabled'}}" bindtap="onViewResult">查看结果</button>
</view>
</view>

View File

@@ -0,0 +1,392 @@
.container {
height: 100vh;
background-color: #f6f7f9;
display: flex;
flex-direction: column;
padding: 0;
align-items: stretch;
}
/* 聚会信息 */
.room-info {
background-color: transparent;
padding: 30rpx 80rpx;
}
.room-title {
font-size: 44rpx;
font-weight: bold;
color: #333;
text-align: center;
margin-bottom: 20rpx;
}
.room-tags {
display: flex;
justify-content: center;
align-items: center;
gap: 16rpx;
flex-wrap: wrap;
}
.time-tag-picker {
display: flex;
gap: 12rpx;
}
.time-picker {
display: inline-block;
}
.tag {
font-size: 26rpx;
background-color: #e8f7f0;
color: #07c160;
padding: 10rpx 24rpx;
border-radius: 30rpx;
white-space: nowrap;
}
.keyword-tag {
cursor: pointer;
user-select: none;
}
.time-tag {
background-color: #f0f0f0;
color: #666;
}
.requirements-section {
margin-top: 20rpx;
padding: 20rpx;
background-color: #fff;
border-radius: 12rpx;
border: 1rpx solid #e5e5e5;
}
.requirements-section.empty {
background-color: #f8f9fa;
border-style: dashed;
}
.requirements-label {
font-size: 24rpx;
color: #999;
margin-bottom: 8rpx;
}
.requirements-text {
font-size: 28rpx;
color: #333;
line-height: 1.6;
}
.requirements-section.empty .requirements-text {
color: #999;
text-align: center;
}
.requirements-textarea {
width: 100%;
min-height: 120rpx;
font-size: 28rpx;
color: #333;
line-height: 1.6;
padding: 0;
border: none;
background-color: transparent;
}
/* 顶部统计 */
.header {
padding: 10rpx 80rpx 30rpx;
background-color: transparent;
}
.stats {
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: 500;
color: #333;
}
.stat-item {
font-size: 32rpx;
}
.stat-divider {
margin: 0 20rpx;
color: #ddd;
}
.ready-count {
color: #07c160;
}
/* 成员列表 */
.member-list {
flex: 1;
padding: 20rpx 40rpx;
padding-bottom: 20rpx;
box-sizing: border-box;
}
.member-item {
display: flex;
align-items: center;
background-color: #fff;
padding: 24rpx;
border-radius: 20rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.04);
transition: all 0.3s;
box-sizing: border-box;
width: 100%;
}
.member-item.ready {
border-left: 4rpx solid #07c160;
}
.member-item.waiting {
border-left: 4rpx solid #ddd;
}
.avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
margin-right: 20rpx;
background-color: #f0f0f0;
flex-shrink: 0;
overflow: hidden;
}
.info {
flex: 1;
min-width: 0;
}
.nickname {
font-size: 32rpx;
font-weight: 500;
color: #333;
margin-bottom: 8rpx;
}
.me-tag {
font-size: 24rpx;
color: #07c160;
font-weight: normal;
margin-left: 8rpx;
}
.status {
font-size: 26rpx;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.status-icon {
font-size: 40rpx;
color: #07c160;
flex-shrink: 0;
}
.member-item.waiting .status-icon {
color: #ddd;
}
/* 测试浮动按钮 */
.test-float-btn {
position: fixed;
right: 30rpx;
bottom: 350rpx;
width: 80rpx;
height: 80rpx;
background-color: rgba(0, 0, 0, 0.1);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 40rpx;
z-index: 999;
}
/* 底部操作按钮 */
.footer-actions {
padding: 20rpx 80rpx;
padding-bottom: calc(20rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background-color: transparent;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20rpx;
}
.action-btn {
border-radius: 40rpx;
font-size: 28rpx;
padding: 20rpx 16rpx;
border: none;
display: flex;
align-items: center;
justify-content: center;
min-height: 80rpx;
box-sizing: border-box;
width: 100%;
}
.action-btn::after {
border: none;
}
.primary-btn {
background-color: #07c160;
color: white;
}
.secondary-btn {
background-color: #fff;
color: #07c160;
border: 2rpx solid #07c160;
}
.warning-btn {
background-color: #fff;
color: #fa5151;
border: 2rpx solid #fa5151;
}
.result-btn {
background-color: #fff;
color: #07c160;
border: 2rpx solid #07c160;
}
.result-btn.disabled {
background-color: #f5f5f5;
color: #999;
border: 2rpx solid #ddd;
}
/* 用户信息填写弹窗 */
.user-info-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
}
.modal-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
}
.modal-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 600rpx;
background-color: #fff;
border-radius: 24rpx;
padding: 40rpx;
box-sizing: border-box;
}
.modal-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
text-align: center;
margin-bottom: 40rpx;
}
.modal-body {
display: flex;
flex-direction: column;
align-items: center;
gap: 30rpx;
}
.avatar-section {
display: flex;
flex-direction: column;
align-items: center;
}
.user-info-modal .avatar-wrapper {
padding: 0;
margin: 0;
border: none;
background: none;
line-height: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
}
.user-info-modal .avatar-wrapper::after {
border: none;
}
.user-info-modal .avatar {
width: 160rpx;
height: 160rpx;
border-radius: 50%;
background-color: #f0f0f0;
border: 2rpx solid #e5e5e5;
}
.avatar-tip {
font-size: 24rpx;
color: #999;
}
.nickname-input-modal {
width: 100%;
font-size: 32rpx;
color: #333;
border: 1rpx solid #e5e5e5;
border-radius: 12rpx;
padding: 20rpx;
text-align: center;
box-sizing: border-box;
}
.modal-footer {
display: flex;
gap: 20rpx;
margin-top: 40rpx;
}
.modal-btn {
flex: 1;
font-size: 32rpx;
padding: 24rpx 0;
border-radius: 50rpx;
}
.modal-btn.cancel {
background-color: #f5f5f5;
color: #666;
}
.modal-btn.confirm {
background-color: #07c160;
color: white;
}

16
miniprogram/sitemap.json Normal file
View File

@@ -0,0 +1,16 @@
{
"desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
"rules": [{
"action": "allow",
"page": "pages/index/index"
}, {
"action": "disallow",
"page": "pages/room/room"
}, {
"action": "disallow",
"page": "pages/result/result"
}, {
"action": "disallow",
"page": "pages/create-room/create-room"
}]
}