Files
uni-fans-score/pages/index/index.vue
T
2025-10-15 14:56:34 +08:00

1455 lines
32 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="container fullscreen">
<view class="" style="font-size: 32rpx;font-weight: 600;margin: 15rpx 20rpx;">风电者共享风扇&充电宝</view>
<view class="map-notice" v-if="noticeText">
<uv-notice-bar :text="noticeText" :speed="50" :show-icon="true" color="#07c160" bg-color="#E8F8EF"
icon="volume"></uv-notice-bar>
</view>
<!-- 全屏地图组件 -->
<MapComponent v-if="!isLoading && userLocation" ref="mapRef" :userLocation="userLocation"
:positionList="positionList" :filteredPositions="filteredPositions" :searchKeyword="searchKeyword"
@relocate="handleRelocate" @scan="handleScan" @showList="showLocationList" @markerTap="selectPosition"
@mapCenterChange="onMapCenterChange" />
<!-- 底部操作栏附近设备 / 扫码使用 / 我的 -->
<view class="bottom-actions">
<!-- <view class="action-btn secondary small btn-nearby" @click="showLocationList">
<view class="icon-wrap">
<image class="action-icon" src="/static/map.png" mode="aspectFit" />
</view>
<text class="action-label">附近设备</text>
</view> -->
<view class="action-btn secondary small btn-nearby" @click="openPopup">
<view class="icon-wrap">
<image src="/static/use_help.png" class="action-icon" mode="aspectFit"></image>
</view>
<text class="action-label">使用指南</text>
</view>
<view class="action-btn primary btn-scan" @click="handleScan">
<view class="icon-wrap">
<image class="action-icon" src="/static/scan-icon.png" mode="aspectFill" />
</view>
<text class="primary-label">扫码使用</text>
</view>
<view class="action-btn secondary small btn-my" @click="goMy">
<view class="icon-wrap">
<image class="action-icon" src="/static/user.png" mode="aspectFit" />
</view>
<text class="action-label">个人中心</text>
</view>
</view>
<!-- 地图加载状态 -->
<view v-if="isLoading || !userLocation" class="map-loading-placeholder">
<view class="loading-content">
<view class="loading-spinner"></view>
<text>正在获取位置信息...</text>
</view>
</view>
<!-- 场地列表弹窗组件 -->
<LocationListSheet
:show="showLocationPopup"
:expanded="isExpanded"
:positions="filteredPositions"
:isLoading="isLoading"
title="附近设备场地"
@close="hideLocationList"
@select="selectPositionFromPopup"
@navigate="navigateToPosition"
/>
<!-- 加载状态 -->
<view class="loading-overlay" v-if="isLoading">
<view class="loading-content">
<view class="loading-spinner"></view>
<text>正在获取场地信息...</text>
</view>
</view>
<!-- 手机号授权弹窗 -->
<view class="phone-auth-popup" v-if="showPhoneAuthPopup">
<view class="popup-mask" @click.stop="showPhoneAuthPopup = false"></view>
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">授权获取手机号</text>
</view>
<view class="popup-body">
<view class="auth-desc">
<text>为了提供更好的服务和紧急联系需要授权获取您的手机号</text>
</view>
<button class="auth-btn" open-type="getPhoneNumber" @getphonenumber="onGetPhoneNumber">
<text>一键获取手机号</text>
</button>
<view class="auth-cancel" @click="showPhoneAuthPopup = false">
<text>暂不授权</text>
</view>
</view>
</view>
</view>
<!-- 使用指南居中弹出ref 控制 open/close -->
<uv-popup ref="guidePopup" mode="center" round="24" :overlay="true" :closeOnClickOverlay="false" :safeAreaInsetBottom="false">
<view class="guide-popup">
<view class="guide-header">
<text class="guide-title">使用指南</text>
<!-- <view class="guide-close" @click="closeGuidePopup">
<uv-icon name="close" size="20"></uv-icon>
</view> -->
</view>
<view class="guide-content">
<view class="guide-step" v-for="(step, idx) in guideSteps" :key="idx">
<view class="step-index">{{ idx + 1 }}</view>
<view class="step-info">
<view class="step-title">{{ step.title }}</view>
<view class="step-desc">{{ step.desc }}</view>
</view>
</view>
</view>
<view class="guide-actions">
<!-- <view class="primary-btn" @click="closeGuidePopup"></view> -->
<view class="primary-btn" @click="closeGuidePopup">
<uv-icon name="close" size="20"></uv-icon>
</view>
</view>
</view>
</uv-popup>
</view>
</template>
<script setup>
import {
ref,
computed,
onMounted,
onUnmounted
} from 'vue'
import {
getQueryString,
wxLogin
} from '../../util/index.js'
import {
URL
} from "../../config/url.js"
import {
getDeviceInfo,
getPotionsDetail,
getNoticeTextData
} from '../../config/user.js'
// 导入地图工具函数
import {
getUserLocation,
getRegeo,
calculateDistanceSync,
testDistanceCalculation
} from '../../utils/mapUtils.js'
// 同样需要使用相对路径引入组件
// 注意:从 pages/index/ 目录访问 components/ 需要使用 ../../components/ 路径
import MapComponent from '../../components/MapComponent.vue'
import LocationListSheet from '../../components/LocationListSheet.vue'
// 开启右上角分享菜单(仅 mp-weixin 有效)
// #ifdef MP-WEIXIN
wx.showShareMenu({
withShareTicket: true,
menus: ['shareAppMessage', 'shareTimeline']
})
// #endif
// 响应式数据
const searchKeyword = ref('')
const userLocation = ref(null)
const positionList = ref([])
const filteredPositions = ref([])
const isExpanded = ref(false)
const isLoading = ref(false)
const showPhoneAuthPopup = ref(false)
const isLocationInitialized = ref(false)
const showLocationPopup = ref(false)
// 使用指南步骤
const guideSteps = ref([
{ title: '扫码使用', desc: '找到附近设备,扫描设备上的二维码' },
{ title: '免押金支付', desc: '无需支付押金,使用支付分免押即可完成租借' },
{ title: '开始使用', desc: '设备自动解锁,风扇弹出后取出即可开始使用' },
{ title: '归还设备', desc: '使用完毕后,按照设备规格要求将风扇还入即可结束订单' }
])
// 使用指南步骤
// 使用指南已取消
// 滚动通知内容
// const noticeText = ref('消费规则:每小时5元,不足1小时按1小时计费,最高24小时封顶,请爱护设备,使用后请及时归还')
const redirectToLogin = () => {
try {
const pages = getCurrentPages()
const current = pages && pages.length ? pages[pages.length - 1] : null
const route = current && current.route ? ('/' + current.route) : '/pages/index/index'
const query = current && current.options ? Object.keys(current.options).map(k =>
`${k}=${encodeURIComponent(current.options[k])}`).join('&') : ''
const redirect = encodeURIComponent(query ? `${route}?${query}` : route)
uni.reLaunch({
url: `/pages/login/index?redirect=${redirect}`
})
} catch (e) {
uni.reLaunch({
url: '/pages/login/index'
})
}
}
const noticeText = ref('')
const getNoticeText = async () => {
const parasm = {
'title': '用户端公告'
}
const res = await getNoticeTextData(parasm);
noticeText.value = res.data.noticeContent;
// 将通知内容存储到本地缓存
try {
uni.setStorageSync('noticeContent', res.data.noticeContent);
console.log('通知内容已缓存:', res.data.noticeContent);
} catch (e) {
console.warn('缓存通知内容失败:', e);
}
}
// 距离格式化函数
const formatDistance = (distanceInMeters) => {
if (distanceInMeters < 1000) {
return `${Math.round(distanceInMeters)}m`
} else {
return `${(distanceInMeters / 1000).toFixed(1)}km`
}
}
// 组件引用
const mapRef = ref(null)
const guidePopup = ref(null)
// 计算属性
const searchPlaceholder = computed(() => {
if (userLocation.value && userLocation.value.address) {
return `${userLocation.value.district || '当前位置'} - 搜索附近场地`
}
return '搜索附近场地'
})
// 生命周期
onMounted(() => {
init()
})
onUnmounted(() => {
// 清理工作在子组件中处理
})
// 方法
const init = async () => {
isLoading.value = true
try {
// 开发环境测试距离计算
if (process.env.NODE_ENV === 'development') {
testDistanceCalculation()
}
await getNoticeText();
// 1. 先获取用户位置
await getUserLocationAndAddress()
// 2. 加载场地列表
await loadPositions()
// 3. 检查是否需要显示使用指南(可以根据用户行为记录来决定)
// 这里暂时默认显示,后续可以根据用户使用情况优化
} catch (error) {
console.error('初始化失败:', error)
// 即使失败也要加载场地列表
await loadPositions()
} finally {
isLoading.value = false
}
}
const getUserLocationAndAddress = async () => {
try {
// 使用腾讯地图SDK获取位置
const location = await getUserLocation()
// 保存用户位置
userLocation.value = {
longitude: location.longitude,
latitude: location.latitude
}
console.log(userLocation.value);
// 将经纬度写入本地缓存(基础信息)
try {
uni.setStorageSync('userLocation', {
longitude: location.longitude,
latitude: location.latitude
})
} catch (e) {
console.warn('缓存基础定位信息失败:', e)
}
// 只在首次初始化时设置标记
if (!isLocationInitialized.value) {
isLocationInitialized.value = true
}
// 获取详细地址信息
try {
const addressResult = await getRegeo(location.longitude, location.latitude)
if (addressResult.success) {
const addressInfo = addressResult.data
userLocation.value.address = addressInfo.formatted_address
userLocation.value.city = addressInfo.addressComponent.city
userLocation.value.district = addressInfo.addressComponent.district
// 更新本地缓存,包含地址信息
try {
uni.setStorageSync('userLocation', {
longitude: userLocation.value.longitude,
latitude: userLocation.value.latitude,
address: userLocation.value.address,
city: userLocation.value.city,
district: userLocation.value.district
})
} catch (e) {
console.warn('缓存带地址的定位信息失败:', e)
}
}
} catch (error) {
// 忽略地址信息错误,使用基础定位信息
}
// 等待地图组件响应位置变化后,重新加载场地列表
setTimeout(async () => {
await loadPositions()
uni.hideLoading()
}, 800)
} catch (error) {
console.error('获取位置失败:', error)
uni.showToast({
title: '获取位置失败,显示默认地图',
icon: 'none'
})
}
}
const loadPositions = async () => {
try {
const res = await uni.request({
url: `${URL}/device/position/app/list`,
method: 'GET',
header: {
'Authorization': "Bearer " + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id')
},
data: {
latitude: userLocation.value.latitude,
longitude: userLocation.value.longitude
}
})
console.log(res);
if (res.statusCode === 401 || res.data?.code === 401 || res.data?.code === 40101) {
redirectToLogin()
return
} else if (res.statusCode === 200 && res.data.code === 200) {
positionList.value = res.data.rows || []
calculateDistances()
filteredPositions.value = [...positionList.value]
} else {
console.error('获取场地列表失败:', res.data.msg)
}
} catch (error) {
console.error('获取场地列表异常:', error)
}
}
const calculateDistances = async (centerPoint = null) => {
// 如果没有指定中心点,优先使用用户位置,其次使用地图中心
const center = centerPoint || userLocation.value || (mapRef.value?.mapCenter)
// 严格检查center对象的有效性
if (!center || typeof center.longitude === 'undefined' || typeof center.latitude === 'undefined') {
return
}
positionList.value.forEach(item => {
if (item.longitude && item.latitude) {
try {
// 使用同步方法计算距离,避免await调用需要改动大量代码
const distanceInMeters = calculateDistanceSync(
center.latitude,
center.longitude,
parseFloat(item.latitude),
parseFloat(item.longitude)
)
// 使用智能距离格式化
item.distance = formatDistance(distanceInMeters)
// 同时保存原始米数用于排序和过滤
item.distanceInMeters = distanceInMeters
} catch (error) {
console.error('计算距离异常:', error, item)
item.distance = '999.0km' // 设置一个默认距离
item.distanceInMeters = 999000
}
}
})
// 按距离排序(使用米数进行排序)
positionList.value.sort((a, b) => {
return (a.distanceInMeters || 999000) - (b.distanceInMeters || 999000)
})
}
const loadPositionsByCenter = async (center) => {
try {
// 使用原有接口获取所有场地
const res = await uni.request({
url: `${URL}/device/position/app/list`,
method: 'GET',
header: {
'Authorization': "Bearer " + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id')
}
})
if (res.statusCode === 401 || res.data?.code === 401 || res.data?.code === 40101) {
redirectToLogin()
return
} else if (res.statusCode === 200 && res.data.code === 200) {
positionList.value = res.data.rows || []
// 基于地图中心计算距离
calculateDistances(center)
// 可以选择性过滤距离过远的场地(比如超过10km的)
const maxDistanceInMeters = 10000 // 最大显示距离,单位米(10km)
filteredPositions.value = positionList.value.filter(item => {
return !item.distanceInMeters || item.distanceInMeters <= maxDistanceInMeters
})
} else {
console.error('根据地图中心加载场地失败:', res.data.msg)
positionList.value = []
filteredPositions.value = []
}
} catch (error) {
console.error('根据地图中心加载场地异常:', error)
// 如果请求失败,保持当前的场地列表不变
}
}
const handleRelocate = async () => {
try {
uni.showLoading({
title: '定位中...'
})
const loc = await getUserLocation()
const center = {
longitude: Number(loc.longitude),
latitude: Number(loc.latitude)
}
userLocation.value = center
try {
uni.setStorageSync('userLocation', center)
} catch (_) {}
if (mapRef.value && typeof mapRef.value.moveToLocation === 'function') {
mapRef.value.moveToLocation(center)
}
await loadPositionsByCenter(center)
} catch (e) {
uni.showToast({
title: '定位失败',
icon: 'none'
})
} finally {
uni.hideLoading()
}
}
const onMapCenterChange = (center) => {
if (center && typeof center.longitude !== 'undefined' && typeof center.latitude !== 'undefined') {
userLocation.value = {
longitude: Number(center.longitude),
latitude: Number(center.latitude)
}
try {
uni.setStorageSync('userLocation', userLocation.value)
} catch (_) {}
}
loadPositionsByCenter(center)
}
const selectPosition = (position) => {
uni.showActionSheet({
itemList: ['扫码使用', '导航前往'],
success: (res) => {
switch (res.tapIndex) {
case 0:
handleScan()
break
case 1:
navigateToPosition(position)
break
}
}
})
}
const goMy = () => {
uni.navigateTo({
url: '/pages/my/index'
})
}
const selectPositionFromPopup = (position) => {
// 先关闭弹窗
hideLocationList()
// 延迟一点时间再显示选择菜单,让弹窗关闭动画完成
setTimeout(() => {
selectPosition(position)
}, 200)
}
const navigateToPosition = (position) => {
const latitude = parseFloat(position.latitude)
const longitude = parseFloat(position.longitude)
uni.openLocation({
latitude,
longitude,
name: position.name,
address: position.location
})
}
const toggleSheet = () => {
isExpanded.value = !isExpanded.value
}
const onSearchInput = () => {
if (!searchKeyword.value.trim()) {
filteredPositions.value = [...positionList.value]
} else {
filteredPositions.value = positionList.value.filter(item => {
return item.name.includes(searchKeyword.value) ||
item.describe.includes(searchKeyword.value) ||
item.location.includes(searchKeyword.value)
})
}
}
const handleScan = async () => {
try {
const scanResult = await new Promise((resolve, reject) => {
uni.scanCode({
success: resolve,
fail: reject
})
})
let deviceNo = getQueryString(scanResult.path, 'deviceNo')
if (!deviceNo) {
uni.showToast({
title: '无效的设备二维码',
icon: 'none'
})
return
}
// 检查是否有使用中的订单
const inUseRes = await uni.request({
url: `${URL}/app/order/inUse`,
method: 'GET',
header: {
'Authorization': "Bearer " + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id')
}
})
if (inUseRes.statusCode === 401 || inUseRes.data?.code === 401 || inUseRes.data?.code === 40101) {
redirectToLogin()
return
} else if (inUseRes.statusCode == 200 && inUseRes.data.code == 200 && inUseRes.data.data) {
const inUseOrder = inUseRes.data.data
uni.reLaunch({
url: `/pages/order/detail?orderId=${inUseOrder.orderId}&deviceId=${deviceNo || inUseOrder.deviceNo}`
})
return
}
// 检查是否有待支付订单
const orderRes = await uni.request({
url: `${URL}/app/order/unpaid`,
method: 'GET',
header: {
'Authorization': "Bearer " + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id')
}
})
if (orderRes.statusCode === 401 || orderRes.data?.code === 401 || orderRes.data?.code === 40101) {
redirectToLogin()
return
} else if (orderRes.statusCode == 200 && orderRes.data.code == 200 && orderRes.data.data) {
const unpaidOrder = orderRes.data.data
uni.navigateTo({
url: `/pages/order/payment?orderId=${unpaidOrder.orderId}`
})
} else {
try {
const deviceInfoRes = await getDeviceInfo(deviceNo)
if (deviceInfoRes.code == 200 && deviceInfoRes.data && deviceInfoRes.data.device) {
const deviceInfo = deviceInfoRes.data.device
if (deviceInfo.feeConfig) {
try {
const feeConfig = JSON.parse(deviceInfo.feeConfig)
uni.navigateTo({
url: `/pages/device/detail?deviceNo=${deviceNo}&feeConfig=${encodeURIComponent(deviceInfo.feeConfig)}`
})
} catch (e) {
uni.navigateTo({
url: `/pages/device/detail?deviceNo=${deviceNo}`
})
}
} else {
uni.navigateTo({
url: `/pages/device/detail?deviceNo=${deviceNo}`
})
}
} else {
uni.showToast({
title: '获取设备信息失败',
icon: 'none'
})
uni.navigateTo({
url: `/pages/device/detail?deviceNo=${deviceNo}`
})
}
} catch (error) {
console.error('获取设备信息异常:', error)
uni.navigateTo({
url: `/pages/device/detail?deviceNo=${deviceNo}`
})
}
}
} catch (error) {
console.error('扫码处理失败:', error)
// uni.showToast({
// title: '扫码失败',
// icon: 'none'
// })
}
}
const showLocationList = () => {
showLocationPopup.value = true
}
const hideLocationList = () => {
showLocationPopup.value = false
}
const onGetPhoneNumber = (e) => {
if (e.detail.errMsg === 'getPhoneNumber:ok') {
showPhoneAuthPopup.value = false
}
}
// 使用指南弹窗控制
const openPopup = () => {
try {
guidePopup.value && typeof guidePopup.value.open === 'function' && guidePopup.value.open()
} catch (e) {}
}
const closeGuidePopup = () => {
try {
guidePopup.value && typeof guidePopup.value.close === 'function' && guidePopup.value.close()
} catch (e) {}
}
</script>
<script>
// 选用 Page 级分享钩子(uni-app 需普通 script 导出)
export default {
// 分享给朋友
onShareAppMessage() {
return {
title: '风电者 - 共享风扇暖手充电宝',
path: '/pages/index/index',
// imageUrl: '/static/logo.png'
}
},
// 朋友圈
onShareTimeline() {
return {
title: '风电者 - 共享风扇暖手充电宝',
query: '',
// imageUrl: '/static/logo.png'
}
}
}
</script>
<style lang="scss" scoped>
.container {
height: 100vh;
width: 100vw;
background-color: #fff;
display: flex;
flex-direction: column;
}
.fullscreen {
padding: 0;
}
/* 顶部Logo和通知栏 */
.header-section {
width: 92%;
margin: 0 auto 20rpx;
}
.logo-container {
display: flex;
align-items: center;
// margin-bottom: 16rpx;
.logo-image {
width: 80rpx;
height: 80rpx;
margin-right: 8rpx;
}
.app-name {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
}
/* 地图标题 */
.map-title {
width: 92%;
margin: 0 auto 10rpx;
padding: 10rpx 0;
text {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
}
/* 顶部搜索栏 */
.header-search {
padding: 20rpx 30rpx;
background: #ffffff;
border-bottom: 1px solid #f0f0f0;
z-index: 10;
.search-box {
display: flex;
align-items: center;
background: #f8f9fa;
border-radius: 50rpx;
padding: 0 20rpx;
height: 80rpx;
.search-icon {
width: 32rpx;
height: 32rpx;
margin-right: 16rpx;
}
.search-input {
flex: 1;
font-size: 28rpx;
color: #333;
}
.location-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
background: #2196F3;
border-radius: 50%;
margin-left: 16rpx;
.location-icon {
width: 24rpx;
height: 24rpx;
}
}
}
}
/* 场地列表弹窗 */
.location-popup {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 2000;
display: flex;
align-items: flex-end;
justify-content: center;
.popup-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
}
.location-sheet {
background: #ffffff;
border-radius: 32rpx 32rpx 0 0;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.1);
max-height: 70vh;
transition: all 0.3s ease;
z-index: 1;
position: relative;
width: 100%;
display: flex;
flex-direction: column;
animation: slideUp 0.3s ease-out;
&.expanded {
max-height: 85vh;
}
.sheet-handle {
display: flex;
justify-content: center;
padding: 20rpx 0;
cursor: pointer;
.handle-bar {
width: 80rpx;
height: 8rpx;
background: #e0e0e0;
border-radius: 4rpx;
}
}
.sheet-header {
padding: 20rpx 30rpx;
border-bottom: 1px solid #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
.sheet-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.close-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
background: #f0f0f0;
border-radius: 50%;
transition: all 0.2s ease;
&:active {
background: #e0e0e0;
transform: scale(0.95);
}
.close-icon {
width: 24rpx;
height: 24rpx;
}
}
}
.sheet-content {
padding: 20rpx 0;
height: 60vh;
/* 固定高度以保证小程序端 scroll-view 正常滚动 */
}
}
}
@keyframes slideUp {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
/* 场地列表项 */
.position-item {
display: flex;
align-items: center;
padding: 24rpx 30rpx;
border-bottom: 1px solid #f8f9fa;
.position-info {
flex: 1;
.position-name {
font-size: 32rpx;
font-weight: 500;
color: #333;
margin-bottom: 8rpx;
}
.position-desc {
font-size: 26rpx;
color: #666;
margin-bottom: 8rpx;
}
.position-location {
display: flex;
align-items: center;
margin-bottom: 8rpx;
.location-icon-small {
width: 24rpx;
height: 24rpx;
margin-right: 8rpx;
}
text {
font-size: 24rpx;
color: #999;
}
}
.position-time {
font-size: 24rpx;
color: #999;
}
}
.position-actions {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8rpx;
.distance-info {
font-size: 24rpx;
color: #2196F3;
font-weight: 500;
}
.status-tag {
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 22rpx;
&.online {
background: #e8f5e8;
color: #4caf50;
}
&.offline {
background: #ffeaea;
color: #f44336;
}
}
.nav-btn {
padding: 12rpx 20rpx;
background: #2196F3;
border-radius: 20rpx;
font-size: 24rpx;
color: #ffffff;
}
}
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx 0;
.empty-icon {
width: 120rpx;
height: 120rpx;
margin-bottom: 24rpx;
opacity: 0.5;
}
.empty-text {
font-size: 28rpx;
color: #999;
}
}
.status-tag {
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 22rpx;
width: fit-content;
&.online {
background: #e8f5e8;
color: #4caf50;
}
&.offline {
background: #ffeaea;
color: #f44336;
}
&.wait {
background: #ffeaea;
color: #f44336;
}
}
/* 底部操作栏 */
.bottom-actions {
position: fixed;
left: 20rpx;
right: 20rpx;
bottom: 40rpx;
z-index: 1200;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
// gap: 16rpx;
padding: 0;
.action-btn {
display: flex;
flex-direction: column;
align-items: center;
align-content: center;
justify-content: center;
font-size: 26rpx;
font-weight: 600;
&.primary {
background: #3EAB64;
color: #fff;
border-radius: 64rpx;
height: 100rpx;
min-width: 320rpx;
// box-shadow: 0 16rpx 40rpx rgba(7, 193, 96, 0.35);
padding: 12rpx 24rpx;
.icon-wrap {
width: 50rpx;
height: 50rpx;
border-radius: 50%;
// background: rgba(255, 255, 255, 0.25);
display: flex;
align-items: center;
align-content: center;
justify-content: center;
// margin-bottom: 10rpx;
}
}
&.secondary {
// background: rgba(255, 255, 255, 0.95);
color: #333;
border-radius: 24rpx;
height: 100rpx;
min-width: 180rpx;
// box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
padding: 12rpx 16rpx;
.icon-wrap {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
// background: #f7f8fa;
// border: 2rpx solid #eaeaea;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10rpx;
}
}
&.small {
height: 120rpx;
min-width: 180rpx;
border-radius: 28rpx;
}
}
.btn-nearby,
.btn-my {
gap: 10rpx;
}
.btn-scan {
/* 尺寸与布局(覆盖 primary 的默认尺寸) */
height: 112rpx;
min-width: 360rpx;
padding: 0 32rpx;
border-radius: 56rpx;
flex-direction: row;
// gap: 16rpx;
align-items: center;
justify-content: center;
/* 左侧图标容器 */
.icon-wrap {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
margin: 0;
margin-right: 12rpx;
// background: rgba(255, 255, 255, 0.18);
display: flex;
align-items: center;
justify-content: center;
box-shadow: none;
}
/* 图标大小与反色 */
.action-icon {
width: 40rpx;
height: 40rpx;
filter: brightness(0) invert(1);
}
}
}
/* 加载状态 */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
.loading-content {
background: #ffffff;
border-radius: 16rpx;
padding: 40rpx;
display: flex;
flex-direction: column;
align-items: center;
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid #f0f0f0;
border-top: 4rpx solid #2196F3;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 24rpx;
}
text {
font-size: 28rpx;
color: #666;
}
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* 地图加载状态 */
.map-loading-placeholder {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #f6f7fb;
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
.loading-content {
background: #ffffff;
border-radius: 16rpx;
padding: 40rpx;
display: flex;
flex-direction: column;
align-items: center;
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid #f0f0f0;
border-top: 4rpx solid #2196F3;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 24rpx;
}
text {
font-size: 28rpx;
color: #666;
}
}
}
/* 手机号授权弹窗 */
.phone-auth-popup {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 2000;
display: flex;
align-items: center;
justify-content: center;
.popup-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
}
.popup-content {
background: #ffffff;
border-radius: 24rpx;
margin: 0 60rpx;
padding: 40rpx;
position: relative;
z-index: 1;
.popup-header {
text-align: center;
margin-bottom: 30rpx;
.popup-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
}
.popup-body {
.auth-desc {
text-align: center;
margin-bottom: 40rpx;
text {
font-size: 28rpx;
color: #666;
line-height: 1.6;
}
}
.auth-btn {
width: 100%;
height: 88rpx;
background: #2196F3;
border-radius: 44rpx;
border: none;
color: #ffffff;
font-size: 32rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20rpx;
}
.auth-cancel {
text-align: center;
padding: 20rpx;
text {
font-size: 28rpx;
color: #999;
}
}
}
}
}
.action-icon {
width: 42rpx;
height: 42rpx;
filter: none;
box-shadow: none;
}
.btn-scan .action-icon {
filter: brightness(0) invert(1);
}
.action-label {
line-height: 1;
}
.map-notice {
margin: 0 20rpx;
// position: absolute;
// left: 20rpx;
// right: 20rpx;
// top: 20rpx;
border-radius: 20rpx;
z-index: 15;
}
/* 使用指南弹窗样式 */
.guide-popup {
width: 640rpx;
max-width: 86vw;
background: #ffffff;
border-radius: 24rpx;
padding: 24rpx 24rpx 16rpx;
box-sizing: border-box;
}
.guide-header {
display: flex;
align-items: center;
justify-content: center;
position: relative;
margin-bottom: 8rpx;
}
.guide-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.guide-close {
position: absolute;
right: 0;
top: 0;
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
border-radius: 50%;
}
.guide-content {
max-height: 600rpx;
padding: 8rpx 4rpx 4rpx;
box-sizing: border-box;
}
.guide-step {
display: flex;
align-items: flex-start;
gap: 16rpx;
padding: 16rpx 0;
border-bottom: 1px solid #f5f5f5;
}
.step-index {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
background: #07c160;
color: #ffffff;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
}
.step-info {
flex: 1;
}
.step-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 6rpx;
}
.step-desc {
font-size: 24rpx;
color: #666;
line-height: 1.6;
}
.guide-actions {
margin-top:20rpx;
}
.primary-btn {
// width: 100%;
width: 80rpx;
height: 80rpx;
padding: 20rpx;
border-radius: 40rpx;
background: #DDEFE3;
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
margin: auto;
font-size: 28rpx;
font-weight: 600;
}
.primary-label{
color:#ffffff;
font-size: 32rpx;
font-weight: 600;
}
</style>