1447 lines
32 KiB
Vue
1447 lines
32 KiB
Vue
<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;
|
||
}
|
||
|
||
// 距离格式化函数
|
||
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> |