first commit
This commit is contained in:
179
miniprogram/pages/create-room/create-room.js
Normal file
179
miniprogram/pages/create-room/create-room.js
Normal 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' })
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
3
miniprogram/pages/create-room/create-room.json
Normal file
3
miniprogram/pages/create-room/create-room.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "创建聚会"
|
||||
}
|
||||
69
miniprogram/pages/create-room/create-room.wxml
Normal file
69
miniprogram/pages/create-room/create-room.wxml
Normal 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>
|
||||
156
miniprogram/pages/create-room/create-room.wxss
Normal file
156
miniprogram/pages/create-room/create-room.wxss
Normal 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;
|
||||
}
|
||||
234
miniprogram/pages/index/index.js
Normal file
234
miniprogram/pages/index/index.js
Normal 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' })
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
4
miniprogram/pages/index/index.json
Normal file
4
miniprogram/pages/index/index.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"navigationBarTitleText": "我们去哪聚"
|
||||
}
|
||||
55
miniprogram/pages/index/index.wxml
Normal file
55
miniprogram/pages/index/index.wxml
Normal 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>
|
||||
114
miniprogram/pages/index/index.wxss
Normal file
114
miniprogram/pages/index/index.wxss
Normal 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;
|
||||
}
|
||||
128
miniprogram/pages/result/result.js
Normal file
128
miniprogram/pages/result/result.js
Normal 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) { // 推荐点 Marker(ID 从 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 }
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
4
miniprogram/pages/result/result.json
Normal file
4
miniprogram/pages/result/result.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"navigationBarTitleText": "推荐结果"
|
||||
}
|
||||
36
miniprogram/pages/result/result.wxml
Normal file
36
miniprogram/pages/result/result.wxml
Normal 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>
|
||||
141
miniprogram/pages/result/result.wxss
Normal file
141
miniprogram/pages/result/result.wxss
Normal 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;
|
||||
}
|
||||
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