修复地图定位bug

This commit is contained in:
2025-11-28 09:21:37 +08:00
parent 089b8d708e
commit 2a249d04da
11 changed files with 602 additions and 441 deletions
+6 -27
View File
@@ -19,15 +19,12 @@
<cover-view class="map-side-controls" v-if="!props.hideControls && !props.hideMapOverlays"> <cover-view class="map-side-controls" v-if="!props.hideControls && !props.hideMapOverlays">
<cover-view class="side-btn locate" @tap="handleRelocate"> <cover-view class="side-btn locate" @tap="handleRelocate">
<cover-image class="side-icon" src="/static/location.png"></cover-image> <cover-image class="side-icon" src="/static/location.png"></cover-image>
<!-- <cover-view class="side-text">定位</cover-view> -->
</cover-view> </cover-view>
<cover-view class="side-btn service" @tap="handleService"> <cover-view class="side-btn service" @tap="handleService">
<cover-image class="side-icon" src="/static/customer-service.png"></cover-image> <cover-image class="side-icon" src="/static/customer-service.png"></cover-image>
<!-- <cover-view class="side-text">客服</cover-view> -->
</cover-view> </cover-view>
<cover-view class="side-btn search" @tap="handleSearch"> <cover-view class="side-btn search" @tap="handleSearch">
<cover-image class="side-icon" src="/static/other_device.png"></cover-image> <cover-image class="side-icon" src="/static/other_device.png"></cover-image>
<!-- <cover-view class="side-text">搜索</cover-view> -->
</cover-view> </cover-view>
</cover-view> </cover-view>
@@ -180,7 +177,6 @@
// 移动地图到指定位置(直接更新地图中心坐标) // 移动地图到指定位置(直接更新地图中心坐标)
const moveToLocation = (location) => { const moveToLocation = (location) => {
if (!location || !location.longitude || !location.latitude) { if (!location || !location.longitude || !location.latitude) {
console.warn('moveToLocation: 无效的位置参数', location)
return return
} }
@@ -188,9 +184,7 @@
longitude: Number(location.longitude), longitude: Number(location.longitude),
latitude: Number(location.latitude) latitude: Number(location.latitude)
} }
console.log('移动地图到:', newCenter)
// 直接更新地图中心,触发地图组件的视图更新 // 直接更新地图中心,触发地图组件的视图更新
mapCenter.value = newCenter mapCenter.value = newCenter
@@ -207,7 +201,6 @@
oldLocation.latitude !== newLocation.latitude oldLocation.latitude !== newLocation.latitude
if (isChanged) { if (isChanged) {
console.log('用户位置变化:', newLocation)
mapCenter.value = { mapCenter.value = {
longitude: newLocation.longitude, longitude: newLocation.longitude,
latitude: newLocation.latitude latitude: newLocation.latitude
@@ -234,25 +227,15 @@
// 地图区域变化事件(带防抖优化) // 地图区域变化事件(带防抖优化)
const onMapRegionChange = (e) => { const onMapRegionChange = (e) => {
console.log('regionchange事件:', e)
// 只处理结束事件 // 只处理结束事件
if (!e || e.type !== 'end') { if (!e || e.type !== 'end') {
return return
} }
// 获取事件原因
// 微信小程序:e.causedBy 可能的值:
// - 'gesture': 手势拖动
// - 'scale': 缩放
// - 'update': 调用更新接口(如 setCenterOffset 等)
const causedBy = e.causedBy || e.detail?.causedBy const causedBy = e.causedBy || e.detail?.causedBy
console.log('地图变化原因:', causedBy, '完整事件:', e) if (causedBy === 'gesture' || causedBy === 'scale' || causedBy === 'drag'||causedBy==='update') {
// 只在用户手动操作(拖动或缩放)结束时触发查询
// 排除程序调用(update)导致的变化,避免定位按钮触发多余查询
if (causedBy === 'gesture' || causedBy === 'scale' || causedBy === 'drag') {
// 清除之前的定时器 // 清除之前的定时器
if (regionChangeTimer) { if (regionChangeTimer) {
clearTimeout(regionChangeTimer) clearTimeout(regionChangeTimer)
@@ -269,14 +252,13 @@
latitude: Number(centerLocation.latitude) latitude: Number(centerLocation.latitude)
} }
mapCenter.value = newCenter mapCenter.value = newCenter;
console.log('地图中心变化,触发查询:', newCenter, '原因:', causedBy)
// 触发父组件查询新位置的场地 // 触发父组件查询新位置的场地
emit('mapCenterChange', newCenter) emit('mapCenterChange', newCenter)
}, 500) }, 500)
} else { } else {
console.warn('未能从事件中获取中心点位置,尝试使用API获取')
// 兜底方案:如果事件中没有centerLocation,才使用API获取 // 兜底方案:如果事件中没有centerLocation,才使用API获取
regionChangeTimer = setTimeout(() => { regionChangeTimer = setTimeout(() => {
if (mapContext.value) { if (mapContext.value) {
@@ -288,7 +270,6 @@
latitude: res.latitude latitude: res.latitude
} }
mapCenter.value = newCenter mapCenter.value = newCenter
console.log('地图中心变化(API获取):', newCenter, '原因:', causedBy)
emit('mapCenterChange', newCenter) emit('mapCenterChange', newCenter)
} }
}, },
@@ -299,9 +280,7 @@
} }
}, 500) }, 500)
} }
} else { }
console.log('跳过查询,原因:', causedBy)
}
} }
// 标记点点击事件 // 标记点点击事件
@@ -505,7 +484,7 @@ const handleSearch = () => {
.center-marker-icon { .center-marker-icon {
width: 60rpx; width: 60rpx;
height: 80rpx; height: 60rpx;
display: block; display: block;
} }
+2 -1
View File
@@ -1,4 +1,5 @@
// export const URL = "https://my.gxfs123.com/api" //正式服务器 // export const URL = "https://my.gxfs123.com/api" //正式服务器-弃用
// export const URL = "https://manager.fdzpower.com/api" //正式服务器
export const URL = "https://fansdev.gxfs123.com/api" //测试服务器 export const URL = "https://fansdev.gxfs123.com/api" //测试服务器
// export const URL = "http://192.168.5.149:8080" //本地调试 // export const URL = "http://192.168.5.149:8080" //本地调试
// export const URL = "http://127.0.0.1:8080" //本地调试 // export const URL = "http://127.0.0.1:8080" //本地调试
+9
View File
@@ -298,6 +298,15 @@ export default {
authCodeFailed: 'Auth failed' authCodeFailed: 'Auth failed'
}, },
permission: {
locationTitle: 'Location Permission',
locationNeed: 'We need your location to show nearby devices. Please enable location access in “Settings > Permissions”.',
locationDenied: 'Location access is disabled. You can enable it later in “Settings > Permissions” to continue.',
goToSettings: 'Open Settings',
later: 'Not now',
gotIt: 'Got it'
},
payment: { payment: {
paymentAmount: 'Amount', paymentAmount: 'Amount',
paymentMethod: 'Method', paymentMethod: 'Method',
+9
View File
@@ -298,6 +298,15 @@ export default {
authCodeFailed: '获取授权码失败' authCodeFailed: '获取授权码失败'
}, },
permission: {
locationTitle: '位置信息授权',
locationNeed: '需要获取您的位置信息以展示附近设备,请在“设置-权限管理”中开启定位权限。',
locationDenied: '未授权定位,将无法展示附近设备。您可以稍后在“设置-权限管理”中重新开启定位权限。',
goToSettings: '去设置',
later: '暂不',
gotIt: '知道了'
},
payment: { payment: {
paymentAmount: '支付金额', paymentAmount: '支付金额',
paymentMethod: '支付方式', paymentMethod: '支付方式',
+7 -8
View File
@@ -384,9 +384,12 @@
// 获取价格单位文本 // 获取价格单位文本
const getPriceUnit = () => { const getPriceUnit = () => {
console.log(deviceInfo.value);
// 按分钟计费 // 按分钟计费
if (deviceInfo.value && deviceInfo.value.feeType === 'minute') { if (deviceInfo.value && deviceInfo.value.feeType === 'minute') {
return '分钟' return '分钟'
}else if(deviceInfo.value && deviceFeeConfig.value.hourPrice == '0.5'){
return '30分钟'
} }
// 按小时计费(默认) // 按小时计费(默认)
return $t('time.hour') return $t('time.hour')
@@ -417,7 +420,7 @@
// 按小时计费 // 按小时计费
const hourPrice = parseFloat(deviceFeeConfig.value.hourPrice || 1) const hourPrice = parseFloat(deviceFeeConfig.value.hourPrice || 1)
const unitPrice = maxHourPrice * hourPrice const unitPrice = maxHourPrice
return unitPrice.toFixed(2) return unitPrice.toFixed(2)
} }
@@ -439,11 +442,7 @@
// 按小时计费 // 按小时计费
const unitMinutes = getBillingUnitMinutes() const unitMinutes = getBillingUnitMinutes()
if (unitMinutes === 30) { return deviceInfo.value.remark;
return `${unitPrice}元/${unitMinutes}分钟,${maxHourPrice}元/小时`
} else {
return `${maxHourPrice}元/小时`
}
} }
// 生成详细说明文本 // 生成详细说明文本
@@ -454,11 +453,11 @@
// 按分钟计费 // 按分钟计费
if (deviceInfo.value && deviceInfo.value.feeType === 'minute') { if (deviceInfo.value && deviceInfo.value.feeType === 'minute') {
return `${freeMinutes}分钟内归还免费,不足${unitMinutes}分钟按${unitMinutes}分钟计费,封顶${depositAmount}元,持续计费至${depositAmount}元视为买断` return `不足${unitMinutes}分钟按${unitMinutes}分钟计费,封顶${depositAmount}元,持续计费至${depositAmount}元视为买断`
} }
// 按小时计费 // 按小时计费
return `${freeMinutes}分钟内归还免费,不足${unitMinutes}分钟按${unitMinutes}分钟计费,封顶${depositAmount}元,持续计费至${depositAmount}元视为买断` return `不足${unitMinutes}分钟按${unitMinutes}分钟计费,封顶${depositAmount}元,持续计费至${depositAmount}元视为买断`
} }
// 提交租借订单 // 提交租借订单
+43 -24
View File
@@ -4,13 +4,13 @@
<!-- 问题类型选择 --> <!-- 问题类型选择 -->
<view class="type-section"> <view class="type-section">
<view class="section-title">{{ $t('feedback.issueType') }}</view> <view class="section-title">{{ $t('feedback.issueType') }}</view>
<view class="type-grid"> <view class="type-grid">
<view v-for="(type, index) in types" :key="index" class="type-item" <view v-for="(type, index) in types" :key="index" class="type-item"
:class="{ active: selectedType === index }" @click="selectType(index)"> :class="{ active: selectedType === index }" @click="selectType(index)">
{{ $t(type) }} {{ $t(type) }}
</view>
</view> </view>
</view> </view>
</view>
<!-- 问题描述 --> <!-- 问题描述 -->
<view class="description-section"> <view class="description-section">
@@ -23,23 +23,23 @@
<!-- 图片上传 --> <!-- 图片上传 -->
<view class="upload-section"> <view class="upload-section">
<view class="section-title">{{ $t('feedback.imageUpload') }}</view> <view class="section-title">{{ $t('feedback.imageUpload') }}</view>
<view class="upload-grid"> <view class="upload-grid">
<view class="upload-item" v-for="(img, index) in images" :key="index"> <view class="upload-item" v-for="(img, index) in images" :key="index">
<image :src="img" mode="aspectFill" /> <image :src="img" mode="aspectFill" />
<view class="delete-btn" @click="deleteImage(index)">×</view> <view class="delete-btn" @click="deleteImage(index)">×</view>
</view> </view>
<view class="upload-btn" @click="chooseImage" v-if="images.length < 3"> <view class="upload-btn" @click="chooseImage" v-if="images.length < 3">
<text class="plus">+</text> <text class="plus">+</text>
<text class="tip">{{ $t('feedback.uploadImage') }}</text> <text class="tip">{{ $t('feedback.uploadImage') }}</text>
</view> </view>
</view>
</view> </view>
</view>
<!-- 联系方式 --> <!-- 联系方式 -->
<view class="contact-section"> <view class="contact-section">
<view class="section-title">{{ $t('feedback.contactInfo') }}</view> <view class="section-title">{{ $t('feedback.contactInfo') }}</view>
<input class="contact-input" v-model="contact" :placeholder="$t('feedback.contactPlaceholder')" type="number" <input class="contact-input" v-model="contact" :placeholder="$t('feedback.contactPlaceholder')"
maxlength="11" name="contact" /> type="number" maxlength="11" name="contact" />
</view> </view>
<!-- 提交按钮 --> <!-- 提交按钮 -->
@@ -55,6 +55,9 @@
ref, ref,
onMounted onMounted
} from 'vue' } from 'vue'
import {
onLoad
} from "@dcloudio/uni-app"
import { import {
URL, URL,
appid appid
@@ -65,9 +68,12 @@
import { import {
addUserFeedback addUserFeedback
} from '../../config/api/feedback' } from '../../config/api/feedback'
import { useI18n } from '@/utils/i18n.js' import {
useI18n
const { t: $t } = useI18n() } from '@/utils/i18n.js'
const {
t: $t
} = useI18n()
onMounted(() => { onMounted(() => {
uni.setNavigationBarTitle({ uni.setNavigationBarTitle({
@@ -75,6 +81,12 @@
}) })
}) })
onLoad(() => {
if (uni.getStorageSync("userInfo").phone) {
contact.value = uni.getStorageSync('userInfo').phone;
}
})
// 响应式数据 // 响应式数据
const types = ref(['feedback.deviceFault', 'feedback.chargingIssue', 'feedback.usageSuggestion', 'feedback.other']) const types = ref(['feedback.deviceFault', 'feedback.chargingIssue', 'feedback.usageSuggestion', 'feedback.other'])
const selectedType = ref(-1) const selectedType = ref(-1)
@@ -89,6 +101,7 @@
selectedType.value = index selectedType.value = index
} }
const chooseImage = () => { const chooseImage = () => {
uni.chooseImage({ uni.chooseImage({
count: 3 - images.value.length, count: 3 - images.value.length,
@@ -109,7 +122,10 @@
if (idx !== -1) { if (idx !== -1) {
images.value.splice(idx, 1) images.value.splice(idx, 1)
} }
uni.showToast({ title: '图片上传失败', icon: 'none' }) uni.showToast({
title: '图片上传失败',
icon: 'none'
})
} }
} }
} }
@@ -145,11 +161,12 @@
return return
} }
if (types.value[selectedType.value] === 'feedback.deviceFault' || types.value[selectedType.value] === 'feedback.chargingIssue') { if (types.value[selectedType.value] === 'feedback.deviceFault' || types.value[selectedType.value] ===
paramsType.value = 'complain' 'feedback.chargingIssue') {
} else { paramsType.value = 'complain'
paramsType.value = 'suggestion' } else {
} paramsType.value = 'suggestion'
}
// 构建反馈数据 // 构建反馈数据
const feedbackData = { const feedbackData = {
@@ -172,7 +189,8 @@
dataType: 'json', dataType: 'json',
success: (res) => { success: (res) => {
// 兼容后端返回 { code: 200 } 或 HTTP 200 情况 // 兼容后端返回 { code: 200 } 或 HTTP 200 情况
if ((res.statusCode === 200) && ((res.data && res.data.code === 200) || res.data === true || res.data?.success === true)) { if ((res.statusCode === 200) && ((res.data && res.data.code === 200) || res.data ===
true || res.data?.success === true)) {
uni.showToast({ uni.showToast({
title: $t('feedback.submitSuccess'), title: $t('feedback.submitSuccess'),
icon: 'success' icon: 'success'
@@ -183,7 +201,8 @@
return return
} }
uni.showToast({ uni.showToast({
title: (res.data && (res.data.msg || res.data.message)) || $t('feedback.submitFailed'), title: (res.data && (res.data.msg || res.data.message)) || $t(
'feedback.submitFailed'),
icon: 'none' icon: 'none'
}) })
}, },
+290 -297
View File
@@ -1,41 +1,40 @@
<template> <template>
<view class="container fullscreen"> <view class="container fullscreen">
<!-- 自定义导航栏 --> <!-- 自定义导航栏 -->
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }"> <view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="navbar-content" :style="{ height: navBarHeight + 'px' }"> <view class="navbar-content" :style="{ height: navBarHeight + 'px' }">
<text class="navbar-title">{{ $t('home.title') }}</text> <text class="navbar-title">{{ $t('home.title') }}</text>
</view>
</view>
<!-- 顶部信息区域通知招商等 -->
<view class="top-info-section" :style="{ top: (statusBarHeight + navBarHeight) + 'px' }">
<!-- 通知栏 -->
<view class="notice-wrapper" v-if="noticeText" @click="openNoticePopup">
<uv-notice-bar :text="noticeText" :speed="50" :show-icon="true" color="#07c160" bg-color="#E8F8EF"
icon="volume"></uv-notice-bar>
</view>
</view>
<!-- 内容区域 -->
<view class="main-content" :style="{ paddingTop: (statusBarHeight + navBarHeight + noticeHeight) + 'px' }">
<!-- 全屏地图组件 -->
<MapComponent v-if="!isLoading && userLocation" ref="mapRef" :userLocation="userLocation"
:positionList="positionList" :filteredPositions="filteredPositions" :searchKeyword="searchKeyword"
:enableMarkers="true"
:hideMapOverlays="showGuidePopup || showNoticePopup || showActivityPopup"
@relocate="handleRelocate" @scan="handleScan" @showList="showLocationList" @markerTap="selectPosition"
@mapCenterChange="onMapCenterChange" />
<!-- 地图加载状态 -->
<view v-if="isLoading || !userLocation" class="map-loading-placeholder">
<view class="loading-content">
<view class="loading-spinner"></view>
<text>{{ $t('common.loadingLocation') }}</text>
</view> </view>
</view> </view>
</view>
<!-- 底部操作栏附近设备 / 扫码使用 / 我的 --> <!-- 顶部信息区域通知招商等 -->
<view class="top-info-section" :style="{ top: (statusBarHeight + navBarHeight) + 'px' }">
<!-- 通知栏 -->
<view class="notice-wrapper" v-if="noticeText" @click="openNoticePopup">
<uv-notice-bar :text="noticeText" :speed="50" :show-icon="true" color="#07c160" bg-color="#E8F8EF"
icon="volume"></uv-notice-bar>
</view>
</view>
<!-- 内容区域 -->
<view class="main-content" :style="{ paddingTop: (statusBarHeight + navBarHeight + noticeHeight) + 'px' }">
<!-- 全屏地图组件 -->
<MapComponent v-if="!isLoading && userLocation" ref="mapRef" :userLocation="userLocation"
:positionList="positionList" :filteredPositions="filteredPositions" :searchKeyword="searchKeyword"
:enableMarkers="true" :hideMapOverlays="showGuidePopup || showNoticePopup || showActivityPopup"
@relocate="handleRelocate" @scan="handleScan" @showList="showLocationList" @markerTap="selectPosition"
@mapCenterChange="onMapCenterChange" />
<!-- 地图加载状态 -->
<view v-if="isLoading || !userLocation" class="map-loading-placeholder">
<view class="loading-content">
<view class="loading-spinner"></view>
<text>{{ $t('common.loadingLocation') }}</text>
</view>
</view>
</view>
<!-- 底部操作栏附近设备 / 扫码使用 / 我的 -->
<view class="bottom-actions"> <view class="bottom-actions">
<!-- <view class="action-btn secondary small btn-nearby" @click="showLocationList"> <!-- <view class="action-btn secondary small btn-nearby" @click="showLocationList">
<view class="icon-wrap"> <view class="icon-wrap">
@@ -67,16 +66,9 @@
</view> </view>
<!-- 场地列表弹窗组件 --> <!-- 场地列表弹窗组件 -->
<LocationListSheet <LocationListSheet :show="showLocationPopup" :expanded="isExpanded" :positions="filteredPositions"
:show="showLocationPopup" :isLoading="isLoading" :title="$t('home.nearbyDeviceLocation')" @close="hideLocationList"
:expanded="isExpanded" @select="selectPositionFromPopup" @navigate="navigateToPosition" />
:positions="filteredPositions"
:isLoading="isLoading"
:title="$t('home.nearbyDeviceLocation')"
@close="hideLocationList"
@select="selectPositionFromPopup"
@navigate="navigateToPosition"
/>
<!-- 加载状态 --> <!-- 加载状态 -->
<view class="loading-overlay" v-if="isLoading"> <view class="loading-overlay" v-if="isLoading">
@@ -107,90 +99,82 @@
</view> </view>
</view> </view>
<!-- 使用指南居中弹出ref 控制 open/close --> <!-- 使用指南居中弹出ref 控制 open/close -->
<uv-popup ref="guidePopup" mode="center" round="24" :overlay="true" :closeOnClickOverlay="false" :safeAreaInsetBottom="false"> <uv-popup ref="guidePopup" mode="center" :overlay="true" :closeOnClickOverlay="false"
<view class="guide-popup"> :safeAreaInsetBottom="false">
<view class="guide-header"> <view class="guide-popup">
<text class="guide-title">{{ $t('guide.title') }}</text> <view class="guide-header">
<!-- <view class="guide-close" @click="closeGuidePopup"> <text class="guide-title">{{ $t('guide.title') }}</text>
<!-- <view class="guide-close" @click="closeGuidePopup">
<uv-icon name="close" size="20"></uv-icon> <uv-icon name="close" size="20"></uv-icon>
</view> --> </view> -->
</view> </view>
<view class="guide-content"> <view class="guide-content">
<view class="guide-step" v-for="(step, idx) in guideSteps" :key="idx"> <view class="guide-step" v-for="(step, idx) in guideSteps" :key="idx">
<view class="step-index">{{ idx + 1 }}</view> <view class="step-index">{{ idx + 1 }}</view>
<view class="step-info"> <view class="step-info">
<view class="step-title">{{ $t('guide.step' + (idx + 1) + 'Title') }}</view> <view class="step-title">{{ $t('guide.step' + (idx + 1) + 'Title') }}</view>
<view class="step-desc">{{ $t('guide.step' + (idx + 1) + 'Desc') }}</view> <view class="step-desc">{{ $t('guide.step' + (idx + 1) + 'Desc') }}</view>
</view>
</view> </view>
</view> </view>
</view> </view>
<view class="guide-actions"> <view class="guide-actions">
<!-- <view class="primary-btn" @click="closeGuidePopup"></view> --> <!-- <view class="primary-btn" @click="closeGuidePopup"></view> -->
<view class="primary-btn" @click="closeGuidePopup"> <view class="primary-btn" @click="closeGuidePopup">
<uv-icon name="close" size="20"></uv-icon> <uv-icon name="close" size="20"></uv-icon>
</view> </view>
</view> </view>
</view> </uv-popup>
</uv-popup>
<!-- 通知详情弹窗居中弹出 -->
<uv-popup ref="noticePopup" mode="center" :overlay="true" :closeOnClickOverlay="true"
:safeAreaInsetBottom="false">
<view class="notice-popup">
<view class="notice-header">
<text class="notice-title">{{ $t('home.noticeTitle') }}</text>
</view>
<view class="notice-content">
<text class="notice-text">{{ noticeText }}</text>
</view>
<!-- 通知详情弹窗居中弹出 -->
<uv-popup ref="noticePopup" mode="center" round="24" :overlay="true" :closeOnClickOverlay="true" :safeAreaInsetBottom="false">
<view class="notice-popup">
<view class="notice-header">
<text class="notice-title">{{ $t('home.noticeTitle') }}</text>
</view>
<view class="notice-content">
<text class="notice-text">{{ noticeText }}</text>
</view> </view>
<view class="notice-actions"> <view class="notice-actions">
<view class="primary-btn" @click="closeNoticePopup"> <view class="primary-btn" @click="closeNoticePopup">
<uv-icon name="close" size="20"></uv-icon> <uv-icon name="close" size="20"></uv-icon>
</view> </view>
</view> </view>
</view> </uv-popup>
</uv-popup>
<!-- 活动弹窗居中弹出支持多个活动轮播 --> <!-- 活动弹窗居中弹出支持多个活动轮播 -->
<uv-popup ref="activityPopup" mode="center" round="16" :overlay="true" :closeOnClickOverlay="false" :safeAreaInsetBottom="false" :customStyle="{ background: 'transparent' }"> <uv-popup ref="activityPopup" mode="center" round="16" :overlay="true" :closeOnClickOverlay="false"
<view class="activity-popup" v-if="activityList && activityList.length > 0"> :safeAreaInsetBottom="false" :customStyle="{ background: 'transparent' }">
<!-- 轮播图 --> <view class="activity-popup" v-if="activityList && activityList.length > 0">
<swiper <!-- 轮播图 -->
class="activity-swiper" <swiper class="activity-swiper" :indicator-dots="activityList.length > 1" :autoplay="false"
:indicator-dots="activityList.length > 1" :circular="false" @change="onActivitySwiperChange" :current="currentActivityIndex">
:autoplay="false" <swiper-item v-for="(activity, index) in activityList" :key="index">
:circular="false" <view class="activity-poster-wrapper">
@change="onActivitySwiperChange" <image :src="activity.coverImageUrl || activity.imageUrl || activity.posterUrl"
:current="currentActivityIndex" mode="aspectFit" class="activity-poster"></image>
> </view>
<swiper-item v-for="(activity, index) in activityList" :key="index"> </swiper-item>
<view class="activity-poster-wrapper"> </swiper>
<image
:src="activity.coverImageUrl || activity.imageUrl || activity.posterUrl" <!-- 自定义指示器 -->
mode="aspectFit" <view class="activity-indicators" v-if="activityList.length > 1">
class="activity-poster" <view v-for="(item, index) in activityList" :key="index" class="indicator-dot"
></image> :class="{ active: index === currentActivityIndex }"></view>
</view>
<!-- 关闭按钮 -->
<view class="activity-close-btn" @click="closeActivityPopup">
<text class="close-text">{{ $t('common.close') }}</text>
</view>
</view> </view>
</swiper-item> </uv-popup>
</swiper> </view>
<!-- 自定义指示器 -->
<view class="activity-indicators" v-if="activityList.length > 1">
<view
v-for="(item, index) in activityList"
:key="index"
class="indicator-dot"
:class="{ active: index === currentActivityIndex }"
></view>
</view>
<!-- 关闭按钮 -->
<view class="activity-close-btn" @click="closeActivityPopup">
<text class="close-text">{{ $t('common.close') }}</text>
</view>
</view>
</uv-popup>
</view>
</template> </template>
<script setup> <script setup>
@@ -230,8 +214,10 @@
// 注意:从 pages/index/ 目录访问 components/ 需要使用 ../../components/ 路径 // 注意:从 pages/index/ 目录访问 components/ 需要使用 ../../components/ 路径
import MapComponent from '../../components/MapComponent.vue' import MapComponent from '../../components/MapComponent.vue'
import LocationListSheet from '../../components/LocationListSheet.vue' import LocationListSheet from '../../components/LocationListSheet.vue'
import { useI18n } from '../../utils/i18n.js' import {
useI18n
} from '../../utils/i18n.js'
// 开启右上角分享菜单(仅 mp-weixin 有效) // 开启右上角分享菜单(仅 mp-weixin 有效)
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
wx.showShareMenu({ wx.showShareMenu({
@@ -240,7 +226,9 @@
}) })
// #endif // #endif
const { t: $t } = useI18n() const {
t: $t
} = useI18n()
// 响应式数据 // 响应式数据
const searchKeyword = ref('') const searchKeyword = ref('')
@@ -251,26 +239,37 @@
const isLoading = ref(false) const isLoading = ref(false)
const showPhoneAuthPopup = ref(false) const showPhoneAuthPopup = ref(false)
const isLocationInitialized = ref(false) const isLocationInitialized = ref(false)
const showLocationPopup = ref(false) const showLocationPopup = ref(false)
const isRelocating = ref(false) // 防抖标志:是否正在重新定位 const isRelocating = ref(false) // 防抖标志:是否正在重新定位
const showGuidePopup = ref(false) // 使用指南弹窗显示状态 const showGuidePopup = ref(false) // 使用指南弹窗显示状态
const showNoticePopup = ref(false) // 通知详情弹窗显示状态 const showNoticePopup = ref(false) // 通知详情弹窗显示状态
const showActivityPopup = ref(false) // 活动弹窗显示状态 const showActivityPopup = ref(false) // 活动弹窗显示状态
const activityList = ref([]) // 活动列表 const activityList = ref([]) // 活动列表
const currentActivityIndex = ref(0) // 当前活动索引 const currentActivityIndex = ref(0) // 当前活动索引
const hasShownActivityThisSession = ref(false) // 本次会话是否已显示过活动(内存变量) const hasShownActivityThisSession = ref(false) // 本次会话是否已显示过活动(内存变量)
// 导航栏高度相关 // 导航栏高度相关
const statusBarHeight = ref(0) const statusBarHeight = ref(0)
const navBarHeight = ref(44) // 默认导航栏内容高度 const navBarHeight = ref(44) // 默认导航栏内容高度
const noticeHeight = ref(0) // 通知栏高度 const noticeHeight = ref(0) // 通知栏高度
// 使用指南步骤 // 使用指南步骤
const guideSteps = ref([ const guideSteps = ref([{
{ title: '扫码使用', desc: '找到附近设备,扫描设备上的二维码' }, title: '扫码使用',
{ title: '免押金支付', desc: '无需支付押金,使用支付分免押即可完成租借' }, desc: '找到附近设备,扫描设备上的二维码'
{ title: '开始使用', desc: '设备自动解锁,风扇弹出后取出即可开始使用' }, },
{ title: '归还设备', desc: '使用完毕后,按照设备规格要求将风扇还入即可结束订单' } {
title: '免押金支付',
desc: '无需支付押金,使用支付分免押即可完成租借'
},
{
title: '开始使用',
desc: '设备自动解锁,风扇弹出后取出即可开始使用'
},
{
title: '归还设备',
desc: '使用完毕后,按照设备规格要求将风扇还入即可结束订单'
}
]) ])
const redirectToLogin = () => { const redirectToLogin = () => {
@@ -298,12 +297,12 @@ const statusBarHeight = ref(0)
} }
const res = await getNoticeTextData(parasm); const res = await getNoticeTextData(parasm);
noticeText.value = res.data.noticeContent; noticeText.value = res.data.noticeContent;
// 设置通知栏高度 // 设置通知栏高度
if (res.data.noticeContent) { if (res.data.noticeContent) {
noticeHeight.value = 50 // 通知栏高度约50px noticeHeight.value = 50 // 通知栏高度约50px
} }
// 将通知内容存储到本地缓存 // 将通知内容存储到本地缓存
try { try {
uni.setStorageSync('noticeContent', res.data.noticeContent); uni.setStorageSync('noticeContent', res.data.noticeContent);
@@ -312,7 +311,7 @@ const statusBarHeight = ref(0)
console.warn('缓存通知内容失败:', e); console.warn('缓存通知内容失败:', e);
} }
} }
// 查询最近的活动 // 查询最近的活动
const checkActiveActivity = async () => { const checkActiveActivity = async () => {
try { try {
@@ -321,13 +320,13 @@ const statusBarHeight = ref(0)
console.log('本次会话已显示过活动弹窗,跳过'); console.log('本次会话已显示过活动弹窗,跳过');
return; return;
} }
const res = await getActiveActivity(); const res = await getActiveActivity();
console.log('活动接口返回:', res); console.log('活动接口返回:', res);
if (res.code === 200) { if (res.code === 200) {
let activities = []; let activities = [];
// 处理活动数据:兼容多种返回格式 // 处理活动数据:兼容多种返回格式
if (res.rows && Array.isArray(res.rows)) { if (res.rows && Array.isArray(res.rows)) {
// 格式1: { code: 200, rows: [...] } // 格式1: { code: 200, rows: [...] }
@@ -342,14 +341,14 @@ const statusBarHeight = ref(0)
// 格式4: { code: 200, data: {...} } 单个对象 // 格式4: { code: 200, data: {...} } 单个对象
activities = [res.data]; activities = [res.data];
} }
// 过滤有效的活动(必须有封面图) // 过滤有效的活动(必须有封面图)
activityList.value = activities.filter(item => { activityList.value = activities.filter(item => {
return item && (item.coverImageUrl || item.imageUrl || item.posterUrl); return item && (item.coverImageUrl || item.imageUrl || item.posterUrl);
}); });
console.log('过滤后的活动列表:', activityList.value); console.log('过滤后的活动列表:', activityList.value);
// 如果有活动数据,则显示弹窗 // 如果有活动数据,则显示弹窗
if (activityList.value.length > 0) { if (activityList.value.length > 0) {
currentActivityIndex.value = 0; // 重置索引 currentActivityIndex.value = 0; // 重置索引
@@ -376,11 +375,11 @@ const statusBarHeight = ref(0)
} }
// 组件引用 // 组件引用
const mapRef = ref(null) const mapRef = ref(null)
const guidePopup = ref(null) const guidePopup = ref(null)
const noticePopup = ref(null) const noticePopup = ref(null)
const activityPopup = ref(null) const activityPopup = ref(null)
// 计算属性 // 计算属性
const searchPlaceholder = computed(() => { const searchPlaceholder = computed(() => {
@@ -395,19 +394,19 @@ const activityPopup = ref(null)
try { try {
const systemInfo = uni.getSystemInfoSync() const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight || 0 statusBarHeight.value = systemInfo.statusBarHeight || 0
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
// 获取胶囊按钮位置信息 // 获取胶囊按钮位置信息
const menuButtonInfo = uni.getMenuButtonBoundingClientRect() const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
// 计算导航栏内容高度:(胶囊底部坐标 - 状态栏高度) 确保与胶囊对齐 // 计算导航栏内容高度:(胶囊底部坐标 - 状态栏高度) 确保与胶囊对齐
navBarHeight.value = (menuButtonInfo.top - systemInfo.statusBarHeight) * 2 + menuButtonInfo.height navBarHeight.value = (menuButtonInfo.top - systemInfo.statusBarHeight) * 2 + menuButtonInfo.height
// #endif // #endif
// #ifndef MP-WEIXIN // #ifndef MP-WEIXIN
// 非微信小程序使用默认高度 // 非微信小程序使用默认高度
navBarHeight.value = 44 navBarHeight.value = 44
// #endif // #endif
console.log('状态栏高度:', statusBarHeight.value) console.log('状态栏高度:', statusBarHeight.value)
console.log('导航栏内容高度:', navBarHeight.value) console.log('导航栏内容高度:', navBarHeight.value)
} catch (error) { } catch (error) {
@@ -438,7 +437,7 @@ const activityPopup = ref(null)
} catch (e) { } catch (e) {
console.warn('清理旧缓存失败:', e); console.warn('清理旧缓存失败:', e);
} }
// 开发环境测试距离计算 // 开发环境测试距离计算
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
testDistanceCalculation() testDistanceCalculation()
@@ -446,90 +445,82 @@ const activityPopup = ref(null)
await getNoticeText(); await getNoticeText();
// 1. 先获取用户位置 // 1. 先获取用户位置
await getUserLocationAndAddress() await getUserLocationAndAddress()
// 2. 加载场地列表 // 2. 加载场地列表(依赖定位)
await loadPositions() await loadPositions()
// 3. 查询活动并显示弹窗 // 3. 查询活动并显示弹窗
await checkActiveActivity() await checkActiveActivity()
} catch (error) { } catch (error) {
console.error('初始化失败:', error) console.error('初始化失败:', error)
// 即使失败也要加载场地列表 uni.showToast({
await loadPositions() title: $t('home.getLocationFailed'),
icon: 'none'
})
} finally { } finally {
isLoading.value = false isLoading.value = false
} }
} }
const getUserLocationAndAddress = async () => { const getUserLocationAndAddress = async () => {
try { // 使用腾讯地图SDK获取位置(若失败抛出,由调用方统一处理)
// 使用腾讯地图SDK获取位置 const location = await getUserLocation()
const location = await getUserLocation()
// 保存用户位置 if (!location?.longitude || !location?.latitude) {
userLocation.value = { throw new Error('invalid location result')
longitude: location.longitude, }
latitude: location.latitude
}
console.log(userLocation.value); // 保存用户位置
// 将经纬度写入本地缓存(基础信息) 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 { try {
uni.setStorageSync('userLocation', { uni.setStorageSync('userLocation', {
longitude: location.longitude, longitude: userLocation.value.longitude,
latitude: location.latitude latitude: userLocation.value.latitude,
address: userLocation.value.address,
city: userLocation.value.city,
district: userLocation.value.district
}) })
} catch (e) { } catch (e) {
console.warn('缓存基础定位信息失败:', 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: $t('home.getLocationFailed'),
icon: 'none'
})
} }
} catch (error) {
// 忽略地址信息错误,使用基础定位信息
}
return userLocation.value
} }
const loadPositions = async () => { const loadPositions = async () => {
@@ -547,7 +538,7 @@ const activityPopup = ref(null)
pageNum: 1, pageNum: 1,
pageSize: 100 pageSize: 100
}) })
console.log('查询附近设备结果:', res) console.log('查询附近设备结果:', res)
if (res.code === 200) { if (res.code === 200) {
@@ -607,9 +598,9 @@ const activityPopup = ref(null)
console.warn('loadPositionsByCenter: 无效的中心点', center) console.warn('loadPositionsByCenter: 无效的中心点', center)
return return
} }
console.log('根据地图中心加载设备:', center) console.log('根据地图中心加载设备:', center)
try { try {
// 使用新的附近设备查询接口 // 使用新的附近设备查询接口
const res = await getNearbyDevices({ const res = await getNearbyDevices({
@@ -624,10 +615,10 @@ const activityPopup = ref(null)
if (res.code === 200) { if (res.code === 200) {
const devices = res.data?.records || [] const devices = res.data?.records || []
console.log('加载到设备数量:', devices.length) console.log('加载到设备数量:', devices.length)
// 将设备数据转换为统一格式 // 将设备数据转换为统一格式
positionList.value = devices.map(transformDeviceData) positionList.value = devices.map(transformDeviceData)
// 基于地图中心计算距离 // 基于地图中心计算距离
calculateDistances(center) calculateDistances(center)
@@ -636,7 +627,7 @@ const activityPopup = ref(null)
filteredPositions.value = positionList.value.filter(item => { filteredPositions.value = positionList.value.filter(item => {
return !item.distanceInMeters || item.distanceInMeters <= maxDistanceInMeters return !item.distanceInMeters || item.distanceInMeters <= maxDistanceInMeters
}) })
console.log('过滤后设备数量:', filteredPositions.value.length) console.log('过滤后设备数量:', filteredPositions.value.length)
} else { } else {
@@ -656,50 +647,50 @@ const activityPopup = ref(null)
console.log('正在定位中,请勿重复点击') console.log('正在定位中,请勿重复点击')
return return
} }
try { try {
isRelocating.value = true isRelocating.value = true
uni.showLoading({ uni.showLoading({
title: $t('home.relocating'), title: $t('home.relocating'),
mask: true mask: true
}) })
// 重新获取用户真实位置(不使用缓存) // 重新获取用户真实位置(不使用缓存)
const loc = await getUserLocation() const loc = await getUserLocation()
const newLocation = { const newLocation = {
longitude: Number(loc.longitude), longitude: Number(loc.longitude),
latitude: Number(loc.latitude) latitude: Number(loc.latitude)
} }
console.log('重新定位成功,新位置:', newLocation) console.log('重新定位成功,新位置:', newLocation)
console.log('当前位置:', userLocation.value) console.log('当前位置:', userLocation.value)
// 更新用户位置(触发地图组件的watch,自动移动地图) // 更新用户位置(触发地图组件的watch,自动移动地图)
userLocation.value = { userLocation.value = {
...newLocation ...newLocation
} }
// 保存到本地缓存 // 保存到本地缓存
try { try {
uni.setStorageSync('userLocation', newLocation) uni.setStorageSync('userLocation', newLocation)
} catch (e) { } catch (e) {
console.warn('缓存位置失败:', e) console.warn('缓存位置失败:', e)
} }
// 确保地图移动到新位置 // 确保地图移动到新位置
if (mapRef.value && typeof mapRef.value.moveToLocation === 'function') { if (mapRef.value && typeof mapRef.value.moveToLocation === 'function') {
mapRef.value.moveToLocation(newLocation) mapRef.value.moveToLocation(newLocation)
} }
// 延迟一下,等待地图移动完成后再查询场地 // 延迟一下,等待地图移动完成后再查询场地
await new Promise(resolve => setTimeout(resolve, 300)) await new Promise(resolve => setTimeout(resolve, 300))
// 加载新位置的场地 // 加载新位置的场地
await loadPositionsByCenter(newLocation) await loadPositionsByCenter(newLocation)
uni.hideLoading() uni.hideLoading()
uni.showToast({ uni.showToast({
title: $t('home.locateSuccess'), title: $t('home.locateSuccess'),
icon: 'success', icon: 'success',
@@ -708,7 +699,7 @@ const activityPopup = ref(null)
} catch (e) { } catch (e) {
console.error('定位失败:', e) console.error('定位失败:', e)
uni.hideLoading() uni.hideLoading()
uni.showToast({ uni.showToast({
title: e.errMsg || $t('home.locateFailed'), title: e.errMsg || $t('home.locateFailed'),
icon: 'none', icon: 'none',
@@ -723,8 +714,7 @@ const activityPopup = ref(null)
} }
const onMapCenterChange = (center) => { const onMapCenterChange = (center) => {
console.log('onMapCenterChange 被调用,中心点:', center)
if (center && typeof center.longitude !== 'undefined' && typeof center.latitude !== 'undefined') { if (center && typeof center.longitude !== 'undefined' && typeof center.latitude !== 'undefined') {
userLocation.value = { userLocation.value = {
longitude: Number(center.longitude), longitude: Number(center.longitude),
@@ -733,7 +723,7 @@ const activityPopup = ref(null)
try { try {
uni.setStorageSync('userLocation', userLocation.value) uni.setStorageSync('userLocation', userLocation.value)
} catch (_) {} } catch (_) {}
// 调用加载场地方法 // 调用加载场地方法
loadPositionsByCenter(center) loadPositionsByCenter(center)
} else { } else {
@@ -830,16 +820,16 @@ const activityPopup = ref(null)
} }
}) })
if (inUseRes.statusCode === 401 || inUseRes.data?.code === 401 || inUseRes.data?.code === 40101) { if (inUseRes.statusCode === 401 || inUseRes.data?.code === 401 || inUseRes.data?.code === 40101) {
redirectToLogin() redirectToLogin()
return return
} else if (inUseRes.statusCode == 200 && inUseRes.data.code == 200 && inUseRes.data.data) { } else if (inUseRes.statusCode == 200 && inUseRes.data.code == 200 && inUseRes.data.data) {
const inUseOrder = inUseRes.data.data const inUseOrder = inUseRes.data.data
uni.reLaunch({ uni.reLaunch({
url: `/pages/order/detail?orderId=${inUseOrder.orderId}&deviceId=${deviceNo || inUseOrder.deviceNo}` url: `/pages/order/detail?orderId=${inUseOrder.orderId}&deviceId=${deviceNo || inUseOrder.deviceNo}`
}) })
return return
} }
// 检查是否有待支付订单 // 检查是否有待支付订单
const orderRes = await uni.request({ const orderRes = await uni.request({
@@ -920,58 +910,58 @@ const activityPopup = ref(null)
showPhoneAuthPopup.value = false showPhoneAuthPopup.value = false
} }
} }
// 使用指南弹窗控制
const openPopup = () => {
try {
showGuidePopup.value = true
guidePopup.value && typeof guidePopup.value.open === 'function' && guidePopup.value.open()
} catch (e) {}
}
const closeGuidePopup = () => { // 使用指南弹窗控制
try { const openPopup = () => {
showGuidePopup.value = false try {
guidePopup.value && typeof guidePopup.value.close === 'function' && guidePopup.value.close() showGuidePopup.value = true
} catch (e) {} guidePopup.value && typeof guidePopup.value.open === 'function' && guidePopup.value.open()
} } catch (e) {}
}
// 通知弹窗控制 const closeGuidePopup = () => {
const openNoticePopup = () => { try {
try { showGuidePopup.value = false
showNoticePopup.value = true guidePopup.value && typeof guidePopup.value.close === 'function' && guidePopup.value.close()
noticePopup.value && typeof noticePopup.value.open === 'function' && noticePopup.value.open() } catch (e) {}
} catch (e) {} }
}
const closeNoticePopup = () => { // 通知弹窗控制
try { const openNoticePopup = () => {
showNoticePopup.value = false try {
noticePopup.value && typeof noticePopup.value.close === 'function' && noticePopup.value.close() showNoticePopup.value = true
} catch (e) {} noticePopup.value && typeof noticePopup.value.open === 'function' && noticePopup.value.open()
} } catch (e) {}
}
// 活动弹窗控制 const closeNoticePopup = () => {
const openActivityPopup = () => { try {
try { showNoticePopup.value = false
showActivityPopup.value = true noticePopup.value && typeof noticePopup.value.close === 'function' && noticePopup.value.close()
activityPopup.value && typeof activityPopup.value.open === 'function' && activityPopup.value.open() } catch (e) {}
} catch (e) {} }
}
const closeActivityPopup = () => { // 活动弹窗控制
try { const openActivityPopup = () => {
showActivityPopup.value = false try {
activityPopup.value && typeof activityPopup.value.close === 'function' && activityPopup.value.close() showActivityPopup.value = true
// 重置索引 activityPopup.value && typeof activityPopup.value.open === 'function' && activityPopup.value.open()
currentActivityIndex.value = 0 } catch (e) {}
} catch (e) {} }
}
// 活动轮播图切换 const closeActivityPopup = () => {
const onActivitySwiperChange = (e) => { try {
currentActivityIndex.value = e.detail.current showActivityPopup.value = false
} activityPopup.value && typeof activityPopup.value.close === 'function' && activityPopup.value.close()
// 重置索引
currentActivityIndex.value = 0
} catch (e) {}
}
// 活动轮播图切换
const onActivitySwiperChange = (e) => {
currentActivityIndex.value = e.detail.current
}
</script> </script>
<script> <script>
@@ -1040,7 +1030,8 @@ const onActivitySwiperChange = (e) => {
position: relative; position: relative;
width: 100%; width: 100%;
height: 100%; height: 100%;
padding-bottom: 180rpx; /* 为底部按钮留出空间 */ padding-bottom: 180rpx;
/* 为底部按钮留出空间 */
box-sizing: border-box; box-sizing: border-box;
} }
@@ -1509,7 +1500,8 @@ const onActivitySwiperChange = (e) => {
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
bottom: 180rpx; /* 为底部按钮留出空间 */ bottom: 180rpx;
/* 为底部按钮留出空间 */
background: #f6f7fb; background: #f6f7fb;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -1630,12 +1622,12 @@ const onActivitySwiperChange = (e) => {
line-height: 1.2; line-height: 1.2;
text-align: center; text-align: center;
} }
.primary-label { .primary-label {
color: #ffffff; color: #ffffff;
line-height: 1; line-height: 1;
} }
/* 顶部信息区域 */ /* 顶部信息区域 */
.top-info-section { .top-info-section {
position: fixed; position: fixed;
@@ -1659,6 +1651,7 @@ const onActivitySwiperChange = (e) => {
border-radius: 24rpx; border-radius: 24rpx;
padding: 24rpx 24rpx 16rpx; padding: 24rpx 24rpx 16rpx;
box-sizing: border-box; box-sizing: border-box;
// padding-bottom:100rpx;
} }
.guide-header { .guide-header {
@@ -1732,7 +1725,7 @@ const onActivitySwiperChange = (e) => {
} }
.guide-actions { .guide-actions {
margin-top:20rpx; margin-top: 20rpx;
} }
.primary-btn { .primary-btn {
@@ -1752,8 +1745,8 @@ const onActivitySwiperChange = (e) => {
} }
.primary-label{ .primary-label {
color:#ffffff; color: #ffffff;
font-size: 32rpx; font-size: 32rpx;
font-weight: 600; font-weight: 600;
} }
@@ -1813,7 +1806,7 @@ const onActivitySwiperChange = (e) => {
overflow: visible; overflow: visible;
padding-bottom: 120rpx; padding-bottom: 120rpx;
} }
/* 覆盖 uv-popup 的默认背景色 */ /* 覆盖 uv-popup 的默认背景色 */
:deep(.uv-popup__content) { :deep(.uv-popup__content) {
background: transparent !important; background: transparent !important;
@@ -1825,11 +1818,11 @@ const onActivitySwiperChange = (e) => {
max-height: 70vh; max-height: 70vh;
border-radius: 16rpx; border-radius: 16rpx;
background: transparent; background: transparent;
:deep(swiper) { :deep(swiper) {
background: transparent; background: transparent;
} }
:deep(swiper-item) { :deep(swiper-item) {
background: transparent; background: transparent;
} }
@@ -1892,13 +1885,13 @@ const onActivitySwiperChange = (e) => {
transition: all 0.3s ease; transition: all 0.3s ease;
background: rgba(0, 0, 0, 0.6); background: rgba(0, 0, 0, 0.6);
border-radius: 40rpx; border-radius: 40rpx;
.close-text { .close-text {
font-size: 28rpx; font-size: 28rpx;
color: #ffffff; color: #ffffff;
font-weight: 500; font-weight: 500;
} }
&:active { &:active {
transform: translateX(-50%) scale(0.95); transform: translateX(-50%) scale(0.95);
background: rgba(0, 0, 0, 0.8); background: rgba(0, 0, 0, 0.8);
+89 -60
View File
@@ -1,9 +1,9 @@
<template> <template>
<view class="search-page"> <view class="search-page">
<view class="map-wrap"> <view class="map-wrap">
<MapComponent :userLocation="userLocation" :filteredPositions="filteredPositions" <MapComponent :userLocation="userLocation" :filteredPositions="filteredPositions" :enableMarkers="true"
:enableMarkers="true" :customHeight="'100%'" :hideControls="true" :fullWidth="true" :customHeight="'100%'" :hideControls="true" :fullWidth="true" @mapCenterChange="onMapCenterChange"
@mapCenterChange="onMapCenterChange" @relocate="init" @markerTap="goToPositionDetail" /> @relocate="init" @markerTap="goToPositionDetail" />
<!-- 定位按钮 --> <!-- 定位按钮 -->
<view class="relocate-btn" @click="init"> <view class="relocate-btn" @click="init">
<image src="/static/location.png" class="relocate-icon" mode="aspectFit"></image> <image src="/static/location.png" class="relocate-icon" mode="aspectFit"></image>
@@ -12,15 +12,19 @@
<view class="list-wrap"> <view class="list-wrap">
<view class="panel"> <view class="panel">
<view class="filter-tabs"> <view class="filter-tabs">
<view class="tab" :class="{ active: activeTab === 'rent' }" @click="setTab('rent')">{{ $t('location.rent') }}</view> <view class="tab" :class="{ active: activeTab === 'rent' }" @click="setTab('rent')">
<view class="tab" :class="{ active: activeTab === 'return' }" @click="setTab('return')">{{ $t('location.return') }}</view> {{ $t('location.rent') }}</view>
<view class="tab" :class="{ active: activeTab === 'return' }" @click="setTab('return')">
{{ $t('location.return') }}</view>
</view> </view>
<scroll-view class="list-scroll" scroll-y="true"> <scroll-view class="list-scroll" scroll-y="true">
<view class="card" :class="{ available: isRentable(item), invalid: !isValidCoordinate(item.latitude, item.longitude) }" <view class="card"
:class="{ available: isRentable(item), invalid: !isValidCoordinate(item.latitude, item.longitude) }"
v-for="(item, index) in filteredPositions" :key="item.positionId || index" v-for="(item, index) in filteredPositions" :key="item.positionId || index"
@click="goToPositionDetail(item)"> @click="goToPositionDetail(item)">
<view class="thumb"> <view class="thumb">
<image v-if="item.deviceImg" :src="item.deviceImg" class="thumb-img" mode="aspectFill"></image> <image v-if="item.deviceImg" :src="item.deviceImg" class="thumb-img" mode="aspectFill">
</image>
<image v-else src="/static/device-info.png" class="thumb-img" mode="aspectFit"></image> <image v-else src="/static/device-info.png" class="thumb-img" mode="aspectFit"></image>
</view> </view>
<view class="info"> <view class="info">
@@ -31,41 +35,44 @@
<view class="row sub" v-if="item.location"> <view class="row sub" v-if="item.location">
<text class="addr">{{ item.location }}</text> <text class="addr">{{ item.location }}</text>
</view> </view>
<view class="row meta" v-if="item.workTime && item.workTime !== '0'"> <view class="row meta" v-if="item.workTime && item.workTime !== '0'">
<text class="time">{{ $t('location.businessHours') }}{{ item.workTime }}</text> <text class="time">{{ $t('location.businessHours') }}{{ item.workTime }}</text>
</view> </view>
<view class="row meta" v-if="!isValidCoordinate(item.latitude, item.longitude)"> <view class="row meta" v-if="!isValidCoordinate(item.latitude, item.longitude)">
<text class="time" style="color: #ff6b6b;">{{ $t('location.coordinateError') }}</text> <text class="time" style="color: #ff6b6b;">{{ $t('location.coordinateError') }}</text>
</view> </view>
<view class="row meta" v-if="item.availablePowerBankCount !== undefined && item.availablePowerBankCount !== null"> <view class="row meta"
<text class="time">可租借{{ item.availablePowerBankCount }} </text> v-if="item.availablePowerBankCount !== undefined && item.availablePowerBankCount !== null">
</view> <text class="time">可租借{{ item.availablePowerBankCount }} </text>
<view class="row meta" v-if="item.availableEmptyGridCount !== undefined && item.availableEmptyGridCount !== null"> </view>
<text class="time">可归还{{ item.availableEmptyGridCount }} 个空位</text> <view class="row meta"
</view> v-if="item.availableEmptyGridCount !== undefined && item.availableEmptyGridCount !== null">
<view class="row meta remark-info" v-if="item.remark"> <text class="time">可归还{{ item.availableEmptyGridCount }} 个空位</text>
<text class="time">💰 {{ item.remark }}</text> </view>
</view> <view class="row meta remark-info" v-if="item.remark">
<view class="tags"> <text class="time">💰 {{ item.remark }}</text>
<view class="tag rent" v-if="isRentable(item)">{{ $t('location.rent') }}</view> </view>
<view class="tag return" v-if="isReturnable(item)">{{ $t('location.return') }}</view> <view class="tags">
</view> <view class="tag rent" v-if="isRentable(item)">{{ $t('location.rent') }}</view>
<view class="tag return" v-if="isReturnable(item)">{{ $t('location.return') }}</view>
</view>
</view> </view>
<view class="actions"> <view class="actions">
<view class="nav" <view class="nav" :class="{ disabled: !isValidCoordinate(item.latitude, item.longitude) }"
:class="{ disabled: !isValidCoordinate(item.latitude, item.longitude) }"
@click.stop="navigateToPosition(item)"> @click.stop="navigateToPosition(item)">
<image src="/static/luxian.png" class="action-icon" mode="aspectFit"></image> <image src="/static/luxian.png" class="action-icon" mode="aspectFit"></image>
</view> </view>
<view class="distance" v-if="item.distance && isValidCoordinate(item.latitude, item.longitude)">{{ item.distance }}</view> <view class="distance"
v-if="item.distance && isValidCoordinate(item.latitude, item.longitude)">
{{ item.distance }}</view>
</view> </view>
</view> </view>
<view class="empty-state" v-if="!isLoading && (!positionList || positionList.length === 0)"> <view class="empty-state" v-if="!isLoading && (!positionList || positionList.length === 0)">
<image class="empty-icon" src="/static/scan-icon.png" mode="aspectFit" /> <image class="empty-icon" src="/static/scan-icon.png" mode="aspectFit" />
<text class="empty-text">{{ $t('home.noNearbyDevice') }}</text> <text class="empty-text">{{ $t('home.noNearbyDevice') }}</text>
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
</view> </view>
@@ -88,15 +95,25 @@
getRegeo, getRegeo,
calculateDistanceSync calculateDistanceSync
} from '../../utils/mapUtils.js' } from '../../utils/mapUtils.js'
import { useI18n } from '@/utils/i18n.js' import {
useI18n
} from '@/utils/i18n.js'
const { t: $t } = useI18n() const {
t: $t
} = useI18n()
onMounted(() => { onMounted(() => {
uni.setNavigationBarTitle({ uni.setNavigationBarTitle({
title: $t('search.title') title: $t('search.title')
}) })
// uni.showLoading({
// title:'11111',
// mask:true
// })
init() init()
// uni.hideLoading();
}) })
const userLocation = ref(null) const userLocation = ref(null)
@@ -173,7 +190,7 @@
const loadPositions = async (center) => { const loadPositions = async (center) => {
try { try {
isLoading.value = true isLoading.value = true
if (!center || !center.latitude || !center.longitude) { if (!center || !center.latitude || !center.longitude) {
console.warn('中心点位置信息不完整,无法查询附近设备') console.warn('中心点位置信息不完整,无法查询附近设备')
return return
@@ -187,11 +204,11 @@
pageNum: 1, pageNum: 1,
pageSize: 100 pageSize: 100
}) })
if (res.code === 200) { if (res.code === 200) {
// 新接口返回的是 data.records // 新接口返回的是 data.records
const devices = res.data?.records || [] const devices = res.data?.records || []
// 将设备数据转换为统一格式 // 将设备数据转换为统一格式
positionList.value = devices.map(device => { positionList.value = devices.map(device => {
const transformed = transformDeviceData(device) const transformed = transformDeviceData(device)
@@ -199,10 +216,11 @@
return { return {
...transformed, ...transformed,
canRent: activeTab.value === 'rent' ? true : (device.availablePowerBankCount > 0), canRent: activeTab.value === 'rent' ? true : (device.availablePowerBankCount > 0),
canReturn: activeTab.value === 'return' ? true : (device.availableEmptyGridCount > 0) canReturn: activeTab.value === 'return' ? true : (device.availableEmptyGridCount >
0)
} }
}) })
calculateDistances(center) calculateDistances(center)
} else { } else {
positionList.value = [] positionList.value = []
@@ -217,24 +235,35 @@
} }
} }
const init = async () => { const init = async () => {
isLoading.value = true isLoading.value = true
try { uni.showLoading({
const loc = await getUserLocation() mask: true
userLocation.value = { })
longitude: loc.longitude, try {
latitude: loc.latitude const loc = await getUserLocation()
} if (!loc?.longitude || !loc?.latitude) {
await loadPositions(userLocation.value) throw new Error('invalid location result')
} catch (e) {
await loadPositions(userLocation.value || {
longitude: 0,
latitude: 0
})
} finally {
isLoading.value = false
} }
userLocation.value = {
longitude: loc.longitude,
latitude: loc.latitude
}
await loadPositions(userLocation.value)
} catch (e) {
console.error('初始化定位失败:', e)
userLocation.value = null
positionList.value = []
filteredPositions.value = []
uni.showToast({
title: $t('home.getLocationFailed'),
icon: 'none'
})
} finally {
isLoading.value = false
uni.hideLoading();
} }
}
const onMapCenterChange = (center) => { const onMapCenterChange = (center) => {
if (center && typeof center.longitude !== 'undefined' && typeof center.latitude !== 'undefined') { if (center && typeof center.longitude !== 'undefined' && typeof center.latitude !== 'undefined') {
@@ -276,7 +305,6 @@
url: `/pages/position/detail?positionId=${position.positionId}` url: `/pages/position/detail?positionId=${position.positionId}`
}) })
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -391,7 +419,7 @@
border-radius: 16rpx; border-radius: 16rpx;
background: #F0F2F5; background: #F0F2F5;
overflow: hidden; overflow: hidden;
.thumb-img { .thumb-img {
width: 100%; width: 100%;
height: 100%; height: 100%;
@@ -427,6 +455,7 @@
.addr { .addr {
display: -webkit-box; display: -webkit-box;
line-clamp: 1;
-webkit-line-clamp: 1; -webkit-line-clamp: 1;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
@@ -435,11 +464,11 @@
.row.meta { .row.meta {
color: #999; color: #999;
font-size: 22rpx; font-size: 22rpx;
&.remark-info { &.remark-info {
color: #FF6B35; color: #FF6B35;
font-weight: 500; font-weight: 500;
.time { .time {
color: #FF6B35; color: #FF6B35;
} }
+2 -2
View File
@@ -44,12 +44,12 @@
</view> </view>
</view> </view>
<view class="form-item" v-if="userInfo.balanceAmount !== undefined"> <!-- <view class="form-item" v-if="userInfo.balanceAmount !== undefined">
<view class="label">{{ $t('userProfile.balance') }}</view> <view class="label">{{ $t('userProfile.balance') }}</view>
<view class="value"> <view class="value">
<text class="value-text amount">¥{{ userInfo.balanceAmount || '0.00' }}</text> <text class="value-text amount">¥{{ userInfo.balanceAmount || '0.00' }}</text>
</view> </view>
</view> </view> -->
</view> </view>
</view> </view>
</template> </template>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

+145 -22
View File
@@ -1,5 +1,41 @@
// 地图工具函数 - 内联腾讯地图SDK核心代码 // 地图工具函数 - 内联腾讯地图SDK核心代码
const DEFAULT_LOCALE = 'zh-CN'
const permissionTexts = {
'zh-CN': {
locationTitle: '位置信息授权',
locationNeed: '需要获取您的位置信息以展示附近设备,请在“设置-权限管理”中开启定位权限。',
locationDenied: '未授权定位,将无法展示附近设备。您可以稍后在“设置-权限管理”中重新开启定位权限。',
goToSettings: '去设置',
later: '暂不',
gotIt: '知道了'
},
'en-US': {
locationTitle: 'Location Permission',
locationNeed: 'We need your location to show nearby devices. Please enable location in “Settings > Permissions”.',
locationDenied: 'Location access is disabled. You can re-enable it later in “Settings > Permissions”.',
goToSettings: 'Set',
later: 'Skip',
gotIt: 'OK'
}
}
const getCurrentLocale = () => {
try {
const saved = uni.getStorageSync('language')
if (saved && permissionTexts[saved]) {
return saved
}
} catch (_) {}
return DEFAULT_LOCALE
}
const getPermissionText = (key) => {
const locale = getCurrentLocale()
const texts = permissionTexts[locale] || permissionTexts[DEFAULT_LOCALE]
return texts[key] || permissionTexts[DEFAULT_LOCALE][key] || ''
}
// 腾讯地图Key // 腾讯地图Key
const QQMAP_KEY = 'RO5BZ-ECZ63-7US3C-RT5QW-TIDZE-2FF35'; const QQMAP_KEY = 'RO5BZ-ECZ63-7US3C-RT5QW-TIDZE-2FF35';
@@ -408,16 +444,120 @@ function getQQMapInstance() {
return qqmapInstance || initQQMap(); return qqmapInstance || initQQMap();
} }
// 获取用户位置(使用微信的接口获取位置 // 获取用户位置(统一处理小程序定位授权 && 实际定位
function getUserLocation() { function getUserLocation() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
wx.getLocation({ // 小程序端优先通过 getSetting 判断权限状态
// 这里只考虑 mp-weixin 场景,其它平台回退到直接调用 getLocation
// #ifdef MP-WEIXIN
uni.getSetting({
success: (settingRes) => {
const authSetting = settingRes.authSetting || {};
// 兼容多种定位 scope(微信近几次版本变更比较多)
const hasUserLocation = Object.prototype.hasOwnProperty.call(authSetting, 'scope.userLocation');
const userLocationAuth = authSetting['scope.userLocation'];
const fuzzyLocationAuth = authSetting['scope.userFuzzyLocation'];
const bgLocationAuth = authSetting['scope.userLocationBackground'];
// 已明确拒绝定位
if (
userLocationAuth === false ||
fuzzyLocationAuth === false ||
bgLocationAuth === false
) {
uni.showModal({
title: getPermissionText('locationTitle'),
content: getPermissionText('locationNeed'),
confirmText: getPermissionText('goToSettings'),
cancelText: getPermissionText('later'),
success: (modalRes) => {
if (modalRes.confirm) {
uni.openSetting({});
}
}
});
reject({
code: 'LOCATION_DENIED',
errMsg: 'user denied location permission'
});
return;
}
const doGetLocation = () => {
wx.getLocation({
type: 'gcj02',
success: (res) => {
const longitude = parseFloat(res.longitude.toFixed(5));
const latitude = parseFloat(res.latitude.toFixed(5));
console.log('地址获取成功');
resolve({
longitude,
latitude
});
},
fail: (error) => {
console.error('获取位置失败:', error);
reject(error);
}
});
};
// 未显式授权时,主动申请一次权限(老版本 scope 为 scope.userLocation
if (!hasUserLocation || userLocationAuth === undefined) {
uni.authorize({
scope: 'scope.userLocation',
success: () => {
doGetLocation();
},
fail: (authErr) => {
console.error('定位授权失败:', authErr);
uni.showModal({
title: getPermissionText('locationTitle'),
content: getPermissionText('locationDenied'),
confirmText: getPermissionText('gotIt'),
showCancel: false
});
reject({
code: 'LOCATION_AUTH_FAIL',
errMsg: authErr.errMsg || 'authorize location fail'
});
}
});
} else {
// 已授权,直接获取定位
doGetLocation();
}
},
fail: (err) => {
console.warn('获取授权设置失败,直接尝试定位:', err);
wx.getLocation({
type: 'gcj02',
success: (res) => {
const longitude = parseFloat(res.longitude.toFixed(5));
const latitude = parseFloat(res.latitude.toFixed(5));
console.log('地址获取成功');
resolve({
longitude,
latitude
});
},
fail: (error) => {
console.error('获取位置失败:', error);
reject(error);
}
});
}
});
// #endif
// 非微信小程序平台:使用 uni.getLocation 做一个尽量兼容的兜底
// #ifndef MP-WEIXIN
uni.getLocation({
type: 'gcj02', type: 'gcj02',
success: (res) => { success: (res) => {
// 对经度和纬度进行四舍五入,保留小数点后五位
const longitude = parseFloat(res.longitude.toFixed(5)); const longitude = parseFloat(res.longitude.toFixed(5));
const latitude = parseFloat(res.latitude.toFixed(5)); const latitude = parseFloat(res.latitude.toFixed(5));
console.log('地址获取成功');
resolve({ resolve({
longitude, longitude,
latitude latitude
@@ -428,26 +568,9 @@ function getUserLocation() {
reject(error); reject(error);
} }
}); });
// #endif
}); });
} }
//TODO : 修改getUserLocation函数,使其返回Promise(暂时弃用)
// function getUserLocation() {
// return new Promise((resolve, reject) => {
// wx.getLocation({
// type: 'gcj02',
// success: (res) => {
// resolve({
// longitude: res.longitude,
// latitude: res.latitude
// });
// },
// fail: (error) => {
// console.error('获取位置失败:', error);
// reject(error);
// }
// });
// });
// }
// 逆地理编码 - 根据经纬度获取地址信息 // 逆地理编码 - 根据经纬度获取地址信息
function getRegeo(longitude, latitude) { function getRegeo(longitude, latitude) {