2034 lines
47 KiB
Vue
2034 lines
47 KiB
Vue
<template>
|
||
<view class="container fullscreen">
|
||
<!-- 自定义导航栏 -->
|
||
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||
<view class="navbar-content" :style="{ height: navBarHeight + 'px' }">
|
||
<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" :bannerImages="bannerImages"
|
||
:hideMapOverlays="showGuidePopup || showNoticePopup || showActivityPopup"
|
||
@relocate="handleRelocate" @scan="handleScan" @showList="showLocationList" @markerTap="selectPosition"
|
||
@mapCenterChange="onMapCenterChange" @bannerClick="handleBannerClick" />
|
||
|
||
<!-- 地图加载状态 -->
|
||
<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="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">{{ $t('home.nearbyDevices') }}</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">{{ $t('home.useGuide') }}</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">{{ $t('home.scanToUse') }}</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">{{ $t('home.personalCenter') }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 场地列表弹窗组件 -->
|
||
<LocationListSheet :show="showLocationPopup" :expanded="isExpanded" :positions="filteredPositions"
|
||
:isLoading="isLoading" :title="$t('home.nearbyDeviceLocation')" @close="hideLocationList"
|
||
@select="selectPositionFromPopup" @navigate="navigateToPosition" />
|
||
|
||
<!-- 加载状态 -->
|
||
<view class="loading-overlay" v-if="isLoading">
|
||
<view class="loading-content">
|
||
<view class="loading-spinner"></view>
|
||
<text>{{ $t('common.loadingPosition') }}</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">{{ $t('auth.authTitle') }}</text>
|
||
</view>
|
||
<view class="popup-body">
|
||
<view class="auth-desc">
|
||
<text>{{ $t('auth.authDesc') }}</text>
|
||
</view>
|
||
<button class="auth-btn" open-type="getPhoneNumber" @getphonenumber="onGetPhoneNumber">
|
||
<text>{{ $t('auth.getPhoneNumber') }}</text>
|
||
</button>
|
||
<view class="auth-cancel" @click="showPhoneAuthPopup = false">
|
||
<text>{{ $t('auth.notNow') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 使用指南:居中弹出(ref 控制 open/close) -->
|
||
<uv-popup ref="guidePopup" mode="center" :overlay="true" :closeOnClickOverlay="false"
|
||
:safeAreaInsetBottom="false">
|
||
<view class="guide-popup">
|
||
<view class="guide-header">
|
||
<text class="guide-title">{{ $t('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">{{ $t('guide.step' + (idx + 1) + 'Title') }}</view>
|
||
<view class="step-desc">{{ $t('guide.step' + (idx + 1) + 'Desc') }}</view>
|
||
</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>
|
||
</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>
|
||
|
||
</view>
|
||
<view class="notice-actions">
|
||
<view class="primary-btn" @click="closeNoticePopup">
|
||
<uv-icon name="close" size="20"></uv-icon>
|
||
</view>
|
||
</view>
|
||
</uv-popup>
|
||
|
||
<!-- 活动弹窗:居中弹出,支持多个活动轮播 -->
|
||
<uv-popup ref="activityPopup" mode="center" round="16" :overlay="true" :closeOnClickOverlay="false"
|
||
:safeAreaInsetBottom="false" :customStyle="{ background: 'transparent' }">
|
||
<view class="activity-popup" v-if="activityList && activityList.length > 0">
|
||
<!-- 轮播图 -->
|
||
<swiper class="activity-swiper" :indicator-dots="activityList.length > 1" :autoplay="false"
|
||
:circular="false" @change="onActivitySwiperChange" :current="currentActivityIndex">
|
||
<swiper-item v-for="(activity, index) in activityList" :key="index">
|
||
<view class="activity-poster-wrapper">
|
||
<image :src="activity.coverImageUrl || activity.imageUrl || activity.posterUrl"
|
||
mode="aspectFit" class="activity-poster"></image>
|
||
</view>
|
||
</swiper-item>
|
||
</swiper>
|
||
|
||
<!-- 自定义指示器 -->
|
||
<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>
|
||
|
||
<script setup>
|
||
import {
|
||
ref,
|
||
computed,
|
||
onMounted,
|
||
onUnmounted
|
||
} from 'vue'
|
||
import {
|
||
getQueryString,
|
||
wxLogin
|
||
} from '../../util/index.js'
|
||
import {
|
||
URL
|
||
} from "../../config/url.js"
|
||
import {
|
||
getDeviceInfo,
|
||
getNearbyDevices,
|
||
transformDeviceData
|
||
} from '../../config/api/device.js'
|
||
import {
|
||
getPotionsDetail
|
||
} from '../../config/api/order.js'
|
||
import {
|
||
getActiveActivity
|
||
} from '../../config/api/system.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'
|
||
import {
|
||
useI18n
|
||
} from '../../utils/i18n.js'
|
||
|
||
// 开启右上角分享菜单(仅 mp-weixin 有效)
|
||
// #ifdef MP-WEIXIN
|
||
wx.showShareMenu({
|
||
withShareTicket: true,
|
||
menus: ['shareAppMessage', 'shareTimeline']
|
||
})
|
||
// #endif
|
||
|
||
const {
|
||
t: $t
|
||
} = useI18n()
|
||
|
||
// 响应式数据
|
||
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 isRelocating = ref(false) // 防抖标志:是否正在重新定位
|
||
const showGuidePopup = ref(false) // 使用指南弹窗显示状态
|
||
const showNoticePopup = ref(false) // 通知详情弹窗显示状态
|
||
const showActivityPopup = ref(false) // 活动弹窗显示状态
|
||
const activityList = ref([]) // 活动列表
|
||
const currentActivityIndex = ref(0) // 当前活动索引
|
||
const hasShownActivityThisSession = ref(false) // 本次会话是否已显示过活动(内存变量)
|
||
|
||
// 导航栏高度相关
|
||
const statusBarHeight = ref(0)
|
||
const navBarHeight = ref(44) // 默认导航栏内容高度
|
||
const noticeHeight = ref(0) // 通知栏高度
|
||
|
||
// 使用指南步骤
|
||
const guideSteps = ref([{
|
||
title: '扫码使用',
|
||
desc: '找到附近设备,扫描设备上的二维码'
|
||
},
|
||
{
|
||
title: '免押金支付',
|
||
desc: '无需支付押金,使用支付分免押即可完成租借'
|
||
},
|
||
{
|
||
title: '开始使用',
|
||
desc: '设备自动解锁,风扇弹出后取出即可开始使用'
|
||
},
|
||
{
|
||
title: '归还设备',
|
||
desc: '使用完毕后,按照设备规格要求将风扇还入即可结束订单'
|
||
}
|
||
])
|
||
|
||
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 bannerImages = ref([]) // 首页广告图片列表
|
||
const bannerImageList = ref([]) // 完整的广告配置列表(包含链接信息)
|
||
|
||
// 将语言代码转换为后端接受的格式
|
||
const convertLanguageCode = (lang) => {
|
||
// zh-CN -> zh_CN (转换下划线)
|
||
// en-US -> en_US (转换下划线)
|
||
return lang.replace(/-/g, '_')
|
||
}
|
||
|
||
// 获取公告内容(支持多语言)
|
||
const getNoticeText = async () => {
|
||
try {
|
||
// 获取当前语言设置
|
||
const currentLang = uni.getStorageSync('language') || 'zh-CN'
|
||
const languageCode = convertLanguageCode(currentLang)
|
||
|
||
console.log('加载公告,语言:', currentLang, '转换后:', languageCode)
|
||
|
||
// 调用接口获取公告内容
|
||
const res = await uni.request({
|
||
url: `${URL}/device/announcementConfig/current`,
|
||
method: 'GET',
|
||
header: {
|
||
'Content-Language': languageCode
|
||
},
|
||
data: {
|
||
type: 'wx_user_type' // 微信小程序用户端
|
||
}
|
||
})
|
||
|
||
console.log('公告响应:', res)
|
||
|
||
if (res.statusCode === 200 && res.data.code === 200 && res.data.data) {
|
||
// 使用后端自动解析的 announcement 字段
|
||
const announcement = res.data.data.announcement || ''
|
||
noticeText.value = announcement
|
||
|
||
// 设置通知栏高度
|
||
if (announcement) {
|
||
noticeHeight.value = 50 // 通知栏高度约50px
|
||
}
|
||
|
||
// 将通知内容存储到本地缓存
|
||
try {
|
||
uni.setStorageSync('noticeContent', announcement)
|
||
console.log('通知内容已缓存:', announcement)
|
||
} catch (e) {
|
||
console.warn('缓存通知内容失败:', e)
|
||
}
|
||
} else {
|
||
console.warn('获取公告失败:', res.data?.msg || '未知错误')
|
||
}
|
||
} catch (error) {
|
||
console.error('获取公告失败:', error)
|
||
}
|
||
}
|
||
|
||
// 获取首页广告图片(支持多语言)
|
||
const getBannerImages = async () => {
|
||
try {
|
||
// 获取当前语言设置
|
||
const currentLang = uni.getStorageSync('language') || 'zh-CN'
|
||
const languageCode = convertLanguageCode(currentLang)
|
||
|
||
console.log('加载首页广告,语言:', currentLang, '转换后:', languageCode)
|
||
|
||
// 调用接口获取广告内容
|
||
const res = await uni.request({
|
||
url: `${URL}/device/advertisementConfig/current`,
|
||
method: 'GET',
|
||
header: {
|
||
'Content-Language': languageCode
|
||
},
|
||
data: {
|
||
appPlatform: 'wechat', // 微信平台
|
||
appType: 'user' // 用户端
|
||
}
|
||
})
|
||
|
||
console.log('首页广告响应:', res)
|
||
|
||
if (res.statusCode === 200 && res.data.code === 200 && res.data.data) {
|
||
// 使用 imageList 字段(包含图片和链接信息)
|
||
const imageList = res.data.data.imageList || []
|
||
if (imageList.length > 0) {
|
||
bannerImageList.value = imageList
|
||
// 提取图片URL用于展示
|
||
bannerImages.value = imageList.map(item => item.imageUrl)
|
||
} else {
|
||
console.warn('未获取到广告图片')
|
||
}
|
||
} else {
|
||
console.warn('获取首页广告失败:', res.data?.msg || '未知错误')
|
||
}
|
||
} catch (error) {
|
||
console.error('获取首页广告失败:', error)
|
||
}
|
||
}
|
||
|
||
// 处理首页广告点击
|
||
const handleBannerClick = (index) => {
|
||
if (!bannerImageList.value || !bannerImageList.value[index]) {
|
||
console.warn('未找到对应的广告配置')
|
||
return
|
||
}
|
||
|
||
const config = bannerImageList.value[index]
|
||
console.log('点击首页广告:', index, config)
|
||
|
||
// 根据链接类型进行跳转
|
||
if (config.linkType === 'miniapp' && config.appId) {
|
||
// 跳转到外部小程序
|
||
// #ifdef MP-WEIXIN
|
||
uni.navigateToMiniProgram({
|
||
appId: config.appId,
|
||
path: config.linkUrl || '',
|
||
success: () => {
|
||
console.log('跳转小程序成功')
|
||
},
|
||
fail: (err) => {
|
||
console.error('跳转小程序失败:', err)
|
||
uni.showToast({
|
||
title: '跳转失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
})
|
||
// #endif
|
||
// #ifndef MP-WEIXIN
|
||
uni.showToast({
|
||
title: '请在微信小程序中使用',
|
||
icon: 'none'
|
||
})
|
||
// #endif
|
||
} else if (config.linkType === 'external' && config.linkUrl) {
|
||
// 跳转到外部链接(H5页面)
|
||
uni.navigateTo({
|
||
url: `/pages/webview/index?url=${encodeURIComponent(config.linkUrl)}`
|
||
})
|
||
} else if (config.linkType === 'internal' && config.linkUrl) {
|
||
// 跳转到内部页面
|
||
uni.navigateTo({
|
||
url: config.linkUrl
|
||
})
|
||
} else {
|
||
console.log('无有效的跳转配置')
|
||
}
|
||
}
|
||
|
||
// 查询最近的活动
|
||
const checkActiveActivity = async () => {
|
||
try {
|
||
// 检查本次会话是否已经显示过活动弹窗(使用内存变量)
|
||
if (hasShownActivityThisSession.value) {
|
||
console.log('本次会话已显示过活动弹窗,跳过');
|
||
return;
|
||
}
|
||
|
||
const res = await getActiveActivity();
|
||
console.log('活动接口返回:', res);
|
||
|
||
if (res.code === 200) {
|
||
let activities = [];
|
||
|
||
// 处理活动数据:兼容多种返回格式
|
||
if (res.rows && Array.isArray(res.rows)) {
|
||
// 格式1: { code: 200, rows: [...] }
|
||
activities = res.rows;
|
||
} else if (res.data && res.data.rows && Array.isArray(res.data.rows)) {
|
||
// 格式2: { code: 200, data: { rows: [...] } }
|
||
activities = res.data.rows;
|
||
} else if (Array.isArray(res.data)) {
|
||
// 格式3: { code: 200, data: [...] }
|
||
activities = res.data;
|
||
} else if (res.data && typeof res.data === 'object') {
|
||
// 格式4: { code: 200, data: {...} } 单个对象
|
||
activities = [res.data];
|
||
}
|
||
|
||
// 过滤有效的活动(必须有封面图)
|
||
activityList.value = activities.filter(item => {
|
||
return item && (item.coverImageUrl || item.imageUrl || item.posterUrl);
|
||
});
|
||
|
||
console.log('过滤后的活动列表:', activityList.value);
|
||
|
||
// 如果有活动数据,则显示弹窗
|
||
if (activityList.value.length > 0) {
|
||
currentActivityIndex.value = 0; // 重置索引
|
||
// 延迟一下显示,避免与其他弹窗冲突
|
||
setTimeout(() => {
|
||
openActivityPopup();
|
||
// 标记本次会话已显示过活动弹窗(内存变量,小程序重启后自动重置)
|
||
hasShownActivityThisSession.value = true;
|
||
}, 500);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.warn('查询活动失败:', e);
|
||
}
|
||
}
|
||
|
||
// 距离格式化函数
|
||
const formatDistance = (distanceInMeters) => {
|
||
if (distanceInMeters < 1000) {
|
||
return `${Math.round(distanceInMeters)}m`
|
||
} else {
|
||
return `${(distanceInMeters / 1000).toFixed(1)}km`
|
||
}
|
||
}
|
||
|
||
|
||
// 组件引用
|
||
const mapRef = ref(null)
|
||
const guidePopup = ref(null)
|
||
const noticePopup = ref(null)
|
||
const activityPopup = ref(null)
|
||
|
||
// 计算属性
|
||
const searchPlaceholder = computed(() => {
|
||
if (userLocation.value && userLocation.value.address) {
|
||
return `${userLocation.value.district || '当前位置'} - 搜索附近场地`
|
||
}
|
||
return '搜索附近场地'
|
||
})
|
||
|
||
// 计算导航栏高度
|
||
const initNavBarHeight = () => {
|
||
try {
|
||
const systemInfo = uni.getSystemInfoSync()
|
||
statusBarHeight.value = systemInfo.statusBarHeight || 0
|
||
|
||
// #ifdef MP-WEIXIN
|
||
// 获取胶囊按钮位置信息
|
||
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
|
||
// 计算导航栏内容高度:(胶囊底部坐标 - 状态栏高度) 确保与胶囊对齐
|
||
navBarHeight.value = (menuButtonInfo.top - systemInfo.statusBarHeight) * 2 + menuButtonInfo.height
|
||
// #endif
|
||
|
||
// #ifndef MP-WEIXIN
|
||
// 非微信小程序使用默认高度
|
||
navBarHeight.value = 44
|
||
// #endif
|
||
|
||
console.log('状态栏高度:', statusBarHeight.value)
|
||
console.log('导航栏内容高度:', navBarHeight.value)
|
||
} catch (error) {
|
||
console.error('获取导航栏高度失败:', error)
|
||
statusBarHeight.value = 20
|
||
navBarHeight.value = 44
|
||
}
|
||
}
|
||
|
||
// 生命周期
|
||
onMounted(() => {
|
||
initNavBarHeight()
|
||
init()
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
// 清理工作在子组件中处理
|
||
})
|
||
|
||
// 方法
|
||
const init = async () => {
|
||
isLoading.value = true
|
||
try {
|
||
// 清理旧版本的缓存键(兼容性处理)
|
||
try {
|
||
uni.removeStorageSync('hasShownActivityInSession');
|
||
uni.removeStorageSync('activityLastShownDate');
|
||
} catch (e) {
|
||
console.warn('清理旧缓存失败:', e);
|
||
}
|
||
|
||
// 开发环境测试距离计算
|
||
if (process.env.NODE_ENV === 'development') {
|
||
testDistanceCalculation()
|
||
}
|
||
|
||
// 并行加载公告和广告(不依赖定位)
|
||
await Promise.all([
|
||
getNoticeText(),
|
||
getBannerImages()
|
||
])
|
||
|
||
// 1. 先获取用户位置
|
||
await getUserLocationAndAddress()
|
||
|
||
// 2. 加载场地列表(依赖定位)
|
||
await loadPositions()
|
||
|
||
// 3. 查询活动并显示弹窗
|
||
await checkActiveActivity()
|
||
|
||
} catch (error) {
|
||
console.error('初始化失败:', error)
|
||
uni.showToast({
|
||
title: $t('home.getLocationFailed'),
|
||
icon: 'none'
|
||
})
|
||
} finally {
|
||
isLoading.value = false
|
||
}
|
||
}
|
||
|
||
const getUserLocationAndAddress = async () => {
|
||
// 使用腾讯地图SDK获取位置(若失败抛出,由调用方统一处理)
|
||
const location = await getUserLocation()
|
||
|
||
if (!location?.longitude || !location?.latitude) {
|
||
throw new Error('invalid location result')
|
||
}
|
||
|
||
// 保存用户位置
|
||
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) {
|
||
// 忽略地址信息错误,使用基础定位信息
|
||
}
|
||
|
||
return userLocation.value
|
||
}
|
||
|
||
const loadPositions = async () => {
|
||
try {
|
||
if (!userLocation.value || !userLocation.value.latitude || !userLocation.value.longitude) {
|
||
console.warn('用户位置信息不完整,无法查询附近设备')
|
||
return
|
||
}
|
||
|
||
const res = await getNearbyDevices({
|
||
userLatitude: userLocation.value.latitude,
|
||
userLongitude: userLocation.value.longitude,
|
||
queryType: 'rent', // 默认查询可租借设备
|
||
radiusKm: 5,
|
||
pageNum: 1,
|
||
pageSize: 100
|
||
})
|
||
|
||
console.log('查询附近设备结果:', res)
|
||
|
||
if (res.code === 200) {
|
||
// 新接口返回的是 data.records,需要适配字段名
|
||
const devices = res.data?.records || []
|
||
// 将设备数据转换为统一格式
|
||
positionList.value = devices.map(transformDeviceData)
|
||
calculateDistances()
|
||
filteredPositions.value = [...positionList.value]
|
||
} else {
|
||
console.error('获取设备列表失败:', res.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) => {
|
||
if (!center || !center.longitude || !center.latitude) {
|
||
console.warn('loadPositionsByCenter: 无效的中心点', center)
|
||
return
|
||
}
|
||
|
||
console.log('根据地图中心加载设备:', center)
|
||
|
||
try {
|
||
// 使用新的附近设备查询接口
|
||
const res = await getNearbyDevices({
|
||
userLatitude: center.latitude,
|
||
userLongitude: center.longitude,
|
||
queryType: 'rent', // 默认查询可租借设备
|
||
radiusKm: 5,
|
||
pageNum: 1,
|
||
pageSize: 100
|
||
})
|
||
|
||
if (res.code === 200) {
|
||
const devices = res.data?.records || []
|
||
console.log('加载到设备数量:', devices.length)
|
||
|
||
// 将设备数据转换为统一格式
|
||
positionList.value = devices.map(transformDeviceData)
|
||
|
||
// 基于地图中心计算距离
|
||
calculateDistances(center)
|
||
|
||
// 可以选择性过滤距离过远的设备(比如超过10km的)
|
||
const maxDistanceInMeters = 10000 // 最大显示距离,单位米(10km)
|
||
filteredPositions.value = positionList.value.filter(item => {
|
||
return !item.distanceInMeters || item.distanceInMeters <= maxDistanceInMeters
|
||
})
|
||
|
||
console.log('过滤后设备数量:', filteredPositions.value.length)
|
||
|
||
} else {
|
||
console.error('根据地图中心加载设备失败:', res.msg)
|
||
positionList.value = []
|
||
filteredPositions.value = []
|
||
}
|
||
} catch (error) {
|
||
console.error('根据地图中心加载设备异常:', error)
|
||
// 如果请求失败,保持当前的设备列表不变
|
||
}
|
||
}
|
||
|
||
const handleRelocate = async () => {
|
||
// 防抖:如果正在定位中,直接返回
|
||
if (isRelocating.value) {
|
||
console.log('正在定位中,请勿重复点击')
|
||
return
|
||
}
|
||
|
||
try {
|
||
isRelocating.value = true
|
||
|
||
uni.showLoading({
|
||
title: $t('home.relocating'),
|
||
mask: true
|
||
})
|
||
|
||
// 重新获取用户真实位置(不使用缓存)
|
||
const loc = await getUserLocation()
|
||
const newLocation = {
|
||
longitude: Number(loc.longitude),
|
||
latitude: Number(loc.latitude)
|
||
}
|
||
|
||
console.log('重新定位成功,新位置:', newLocation)
|
||
console.log('当前位置:', userLocation.value)
|
||
|
||
// 更新用户位置(触发地图组件的watch,自动移动地图)
|
||
userLocation.value = {
|
||
...newLocation
|
||
}
|
||
|
||
// 保存到本地缓存
|
||
try {
|
||
uni.setStorageSync('userLocation', newLocation)
|
||
} catch (e) {
|
||
console.warn('缓存位置失败:', e)
|
||
}
|
||
|
||
// 确保地图移动到新位置
|
||
if (mapRef.value && typeof mapRef.value.moveToLocation === 'function') {
|
||
mapRef.value.moveToLocation(newLocation)
|
||
}
|
||
|
||
// 延迟一下,等待地图移动完成后再查询场地
|
||
await new Promise(resolve => setTimeout(resolve, 300))
|
||
|
||
// 加载新位置的场地
|
||
await loadPositionsByCenter(newLocation)
|
||
|
||
uni.hideLoading()
|
||
|
||
uni.showToast({
|
||
title: $t('home.locateSuccess'),
|
||
icon: 'success',
|
||
duration: 1500
|
||
})
|
||
} catch (e) {
|
||
console.error('定位失败:', e)
|
||
uni.hideLoading()
|
||
|
||
uni.showToast({
|
||
title: e.errMsg || $t('home.locateFailed'),
|
||
icon: 'none',
|
||
duration: 2000
|
||
})
|
||
} finally {
|
||
// 1秒后解除防抖锁定
|
||
setTimeout(() => {
|
||
isRelocating.value = false
|
||
}, 1000)
|
||
}
|
||
}
|
||
|
||
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)
|
||
} else {
|
||
console.warn('onMapCenterChange: 无效的中心点', 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: $t('home.invalidQRCode'),
|
||
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 {
|
||
showGuidePopup.value = true
|
||
guidePopup.value && typeof guidePopup.value.open === 'function' && guidePopup.value.open()
|
||
} catch (e) {}
|
||
}
|
||
|
||
const closeGuidePopup = () => {
|
||
try {
|
||
showGuidePopup.value = false
|
||
guidePopup.value && typeof guidePopup.value.close === 'function' && guidePopup.value.close()
|
||
} catch (e) {}
|
||
}
|
||
|
||
// 通知弹窗控制
|
||
const openNoticePopup = () => {
|
||
try {
|
||
showNoticePopup.value = true
|
||
noticePopup.value && typeof noticePopup.value.open === 'function' && noticePopup.value.open()
|
||
} catch (e) {}
|
||
}
|
||
|
||
const closeNoticePopup = () => {
|
||
try {
|
||
showNoticePopup.value = false
|
||
noticePopup.value && typeof noticePopup.value.close === 'function' && noticePopup.value.close()
|
||
} catch (e) {}
|
||
}
|
||
|
||
// 活动弹窗控制
|
||
const openActivityPopup = () => {
|
||
try {
|
||
showActivityPopup.value = true
|
||
activityPopup.value && typeof activityPopup.value.open === 'function' && activityPopup.value.open()
|
||
} catch (e) {}
|
||
}
|
||
|
||
const closeActivityPopup = () => {
|
||
try {
|
||
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>
|
||
// 选用 Page 级分享钩子(uni-app 需普通 script 导出)
|
||
export default {
|
||
// 分享给朋友
|
||
onShareAppMessage() {
|
||
const $t = this.$t || ((key) => key)
|
||
return {
|
||
title: $t('share.title'),
|
||
path: '/pages/index/index',
|
||
// imageUrl: '/static/logo.png'
|
||
}
|
||
},
|
||
// 朋友圈
|
||
onShareTimeline() {
|
||
const $t = this.$t || ((key) => key)
|
||
return {
|
||
title: $t('share.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;
|
||
}
|
||
|
||
/* 自定义导航栏 */
|
||
.custom-navbar {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
background-color: #ffffff;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.navbar-content {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
padding: 0 20rpx;
|
||
}
|
||
|
||
.navbar-title {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
/* 主内容区域 */
|
||
.main-content {
|
||
flex: 1;
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100%;
|
||
padding-bottom: 180rpx;
|
||
/* 为底部按钮留出空间 */
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
/* 顶部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;
|
||
justify-content: center;
|
||
font-size: 24rpx;
|
||
font-weight: 500;
|
||
transition: all 0.3s ease;
|
||
|
||
&:active {
|
||
transform: scale(0.95);
|
||
opacity: 0.8;
|
||
}
|
||
|
||
&.primary {
|
||
background: #3EAB64;
|
||
color: #fff;
|
||
border-radius: 56rpx;
|
||
height: 112rpx;
|
||
flex: 1;
|
||
max-width: 400rpx;
|
||
padding: 0 24rpx;
|
||
// box-shadow: 0 8rpx 24rpx rgba(62, 171, 100, 0.3);
|
||
|
||
.icon-wrap {
|
||
width: 48rpx;
|
||
height: 48rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
}
|
||
|
||
&.secondary {
|
||
// background: rgba(255, 255, 255, 0.95);
|
||
color: #333;
|
||
border-radius: 24rpx;
|
||
height: 100rpx;
|
||
width: 140rpx;
|
||
flex-shrink: 0;
|
||
padding: 8rpx 12rpx;
|
||
// box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
|
||
// border: 1rpx solid #f0f0f0;
|
||
|
||
.icon-wrap {
|
||
width: 36rpx;
|
||
height: 36rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-bottom: 6rpx;
|
||
}
|
||
}
|
||
}
|
||
|
||
.btn-scan {
|
||
/* 中间扫码按钮:横向布局 */
|
||
flex-direction: row;
|
||
gap: 8rpx;
|
||
|
||
.icon-wrap {
|
||
width: 48rpx;
|
||
height: 48rpx;
|
||
margin: 0;
|
||
}
|
||
|
||
.action-icon {
|
||
width: 32rpx;
|
||
height: 32rpx;
|
||
filter: brightness(0) invert(1);
|
||
}
|
||
|
||
.primary-label {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 加载状态 */
|
||
.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: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 180rpx;
|
||
/* 为底部按钮留出空间 */
|
||
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: 36rpx;
|
||
height: 36rpx;
|
||
filter: none;
|
||
}
|
||
|
||
.action-label {
|
||
line-height: 1.2;
|
||
text-align: center;
|
||
}
|
||
|
||
.primary-label {
|
||
color: #ffffff;
|
||
line-height: 1;
|
||
}
|
||
|
||
/* 顶部信息区域 */
|
||
.top-info-section {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
z-index: 998;
|
||
background: transparent;
|
||
}
|
||
|
||
.notice-wrapper {
|
||
margin: 0 20rpx;
|
||
padding: 10rpx 0;
|
||
border-radius: 20rpx;
|
||
}
|
||
|
||
/* 使用指南弹窗样式 */
|
||
.guide-popup {
|
||
width: 640rpx;
|
||
max-width: 86vw;
|
||
background: #ffffff;
|
||
border-radius: 24rpx;
|
||
padding: 24rpx 24rpx 16rpx;
|
||
box-sizing: border-box;
|
||
// padding-bottom:100rpx;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
/* 通知详情弹窗样式 */
|
||
.notice-popup {
|
||
width: 640rpx;
|
||
max-width: 86vw;
|
||
background: #ffffff;
|
||
border-radius: 24rpx;
|
||
padding: 24rpx 24rpx 16rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.notice-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: relative;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.notice-title {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.notice-content {
|
||
max-height: 600rpx;
|
||
overflow-y: auto;
|
||
padding: 16rpx 12rpx;
|
||
box-sizing: border-box;
|
||
background: #F8F9FA;
|
||
border-radius: 12rpx;
|
||
}
|
||
|
||
.notice-text {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
line-height: 1.8;
|
||
word-break: break-all;
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
.notice-actions {
|
||
margin-top: 20rpx;
|
||
}
|
||
|
||
/* 活动弹窗样式 - 支持轮播 */
|
||
.activity-popup {
|
||
position: relative;
|
||
width: 600rpx;
|
||
max-width: 80vw;
|
||
background: transparent;
|
||
border-radius: 16rpx;
|
||
overflow: visible;
|
||
padding-bottom: 120rpx;
|
||
}
|
||
|
||
/* 覆盖 uv-popup 的默认背景色 */
|
||
:deep(.uv-popup__content) {
|
||
background: transparent !important;
|
||
}
|
||
|
||
.activity-swiper {
|
||
width: 100%;
|
||
height: 800rpx;
|
||
max-height: 70vh;
|
||
border-radius: 16rpx;
|
||
background: transparent;
|
||
|
||
:deep(swiper) {
|
||
background: transparent;
|
||
}
|
||
|
||
:deep(swiper-item) {
|
||
background: transparent;
|
||
}
|
||
}
|
||
|
||
.activity-poster-wrapper {
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 16rpx;
|
||
overflow: visible;
|
||
background: transparent;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.activity-poster {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: block;
|
||
object-fit: contain;
|
||
}
|
||
|
||
/* 自定义指示器 */
|
||
.activity-indicators {
|
||
position: absolute;
|
||
bottom: 140rpx;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
display: flex;
|
||
gap: 12rpx;
|
||
z-index: 10;
|
||
}
|
||
|
||
.indicator-dot {
|
||
width: 16rpx;
|
||
height: 16rpx;
|
||
border-radius: 50%;
|
||
background: rgba(255, 255, 255, 0.5);
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.indicator-dot.active {
|
||
width: 32rpx;
|
||
border-radius: 8rpx;
|
||
background: #ffffff;
|
||
}
|
||
|
||
.activity-close-btn {
|
||
position: absolute;
|
||
bottom: 20rpx;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
padding: 16rpx 48rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 999;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
background: rgba(0, 0, 0, 0.6);
|
||
border-radius: 40rpx;
|
||
|
||
.close-text {
|
||
font-size: 28rpx;
|
||
color: #ffffff;
|
||
font-weight: 500;
|
||
}
|
||
|
||
&:active {
|
||
transform: translateX(-50%) scale(0.95);
|
||
background: rgba(0, 0, 0, 0.8);
|
||
}
|
||
}
|
||
</style> |