first commit
This commit is contained in:
610
miniprogram/pages/room/room.js
Normal file
610
miniprogram/pages/room/room.js
Normal 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' })
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
5
miniprogram/pages/room/room.json
Normal file
5
miniprogram/pages/room/room.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"navigationBarTitleText": "聚会房间",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
112
miniprogram/pages/room/room.wxml
Normal file
112
miniprogram/pages/room/room.wxml
Normal 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>
|
||||
392
miniprogram/pages/room/room.wxss
Normal file
392
miniprogram/pages/room/room.wxss
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user